i've had the same conversation a dozen times. an engineer ships something cool. demo goes great. the team celebrates. three weeks later they're spending half their time fielding bugs, edge cases, and "why is this slow?" tickets. the feature is technically alive but the team is dying.
the problem isn't the code. the problem is that they shipped something they can't operate.
shipping is the easy part. operating is the whole game.
what "shipped" actually means
most engineers think "shipped" means "deployed to prod and the demo worked". that's not shipping. that's launching. those are different things.
a thing is shipped when:
- you can debug it at 2am without paging the original author
- a new engineer can answer support questions about it without learning the codebase
- you can change it next quarter without rereading the whole module
- it has been tested by users you don't know personally
- the failure modes are documented
- the on-call rotation knows what alerts mean and how to handle them
if any of those isn't true, you've launched a feature. you haven't shipped one. the difference is whether the system can survive without your continuous attention.
the maintenance multiplier
here's the part nobody tells you when you start writing production code: every line you add today is a line you'll maintain for the rest of the project's life.
maintenance isn't just bug fixes. it's:
- debugging when something seems wrong
- explaining it to a new team member
- changing it when an upstream dep changes
- migrating it when the framework upgrades
- removing it when the use case changes
- writing tests when a regression appears
i think of every feature i ship as a small recurring tax. not the cost to build it, the cost to keep it alive. some features pay back the tax with usage. most don't. the ones that don't are dead weight that compounds quietly until the team can't move.
the math is brutal. if you ship one new feature per week and each feature costs 30 minutes per week to maintain, after a year you're spending 26 hours per week on maintenance. by year two, you have no time left for new features. by year three, you're rewriting things just to dig out of the hole.
the demo trap
the demo trap is when you optimize a feature for the moment of presentation instead of the rest of its life.
demos reward:
- happy path flows
- no errors
- one user at a time
- pre-loaded data
- staging environments
- you in the room to handle questions
production rewards:
- error handling for edge cases you didn't think of
- concurrent users
- empty states and loading states
- partial failures
- data that doesn't match your assumptions
- 3am incidents with no one in the room
if you optimize for the first list, you'll get a great demo and an unmaintainable product. if you optimize for the second list, you'll get a mediocre demo and a system that survives.
i built a real-time pipeline status feature in mailpilot once. an sse broker pushing live updates to every connected client. beautiful in the demo. when i shipped it, i learned that some users have flaky wifi and the connection drops made the entire feed feel broken every few seconds. the demo never showed that case. production found it within hours.
the fix wasn't more code. it was admitting that "real-time" had to mean something different in production than it did in the demo. graceful degradation, optimistic fallback to polling, stale-state badges. all the boring stuff that wasn't in the spec.
the runbook test
here's a concrete way to know if you've shipped a feature or just launched one. write the runbook before you merge.
the runbook should answer:
- what does this feature do?
- what are the most likely failure modes?
- how do you check if it's working?
- how do you check if it's broken?
- what alerts fire when it breaks?
- who do you page?
- what's the rollback plan?
- what data does it touch and where is the schema?
if you can't write that runbook in 15 minutes, the feature isn't done. you don't understand its failure modes well enough to operate it. ship the runbook before you ship the code.
this sounds like overkill. it isn't. it's the difference between a codebase you can leave for two weeks and one you have to babysit indefinitely.
design for handoff
i write code with a specific person in mind: me, six months from now, with no context.
future-me has forgotten everything. future-me doesn't remember why i picked redis over postgres for that one queue. future-me doesn't remember which env var controls the rate limiter. future-me will be debugging this at midnight with one cup of coffee and a half-remembered slack thread.
every comment, every variable name, every function boundary is for that person. not for the original author. not for the technically curious. for the person who has to fix something and has no time to learn the system from scratch.
this single shift in mindset changes how i write code. it makes me:
- name things obviously, not cleverly
- write down the why, not the what
- prefer one weird function over a chain of cute abstractions
- leave the surprising decisions in a comment block at the top of the file
- delete dead code aggressively because future-me will assume it's important
future-me is real. future-me is me. and future-me is going to be the operator, not the original author. write for them.
the operations budget
every team has an unspoken operations budget: the total amount of attention available for "keeping the lights on". if you spend more than your budget, you starve everything else. you can't build new features. you can't fix bugs. you can't help customers. all your time goes to keeping the existing thing alive.
most teams don't measure their operations budget. they should. mine is roughly: i can spend about 6 hours per week on operations across all my live systems before everything else suffers. that's my budget. every new feature needs to fit in that budget.
so when i ship a feature, i estimate its operational cost. how often will this break? how often will users ask about it? how often will i need to update it when the underlying lib changes? if the answer is "more than 30 minutes per week forever", i don't ship it without a really good reason.
this turns "should we build this feature?" from a creative question into a budget question. and budget questions have honest answers.
the second-day features
there's a class of features that nobody asks for in the kickoff but everyone needs the second day after launch:
- a way to see what the system did with their data
- a way to undo a recent action
- a way to export everything they've put in
- a way to know if the system is currently working
- a way to contact a human when it isn't
these are not glamorous. they don't show up in the launch announcement. they're often the second sprint after launch, when the support tickets start coming in. but if you build them on day one, you save weeks of operational fire later. and your users trust you in a way they never will if they have to ask "what happened?" about something they just did.
i call these "second-day features" and i build them by default now, even when they're not in the spec. because the second day is when the real product begins.
what to actually do
if you want to stop launching things you can't operate, do these five things on every project:
-
write the runbook before you write the code. force yourself to know the failure modes before you commit to the happy path.
-
estimate the operational cost. every feature has a per-week maintenance number. write it down. add it up. if the total exceeds your team's capacity, something has to go.
-
build the second-day features on day one. undo, export, status, support. don't ship without them.
-
write code for future-you. name things obviously. document the surprising decisions. delete the clever stuff.
-
measure operations time honestly. track how long you spend on "keeping the lights on" each week. if it's growing, something is broken.
none of this is exciting. none of this will go in your launch tweet. but it's the difference between a system that earns its keep and a system that drowns the team that built it.
the final reframe
shipping isn't about getting something into production. shipping is about handing something over. to your future self, to your team, to the on-call rotation, to the users who'll find every edge case you missed.
if you can hand it over and walk away, you've shipped it.
if you have to babysit it forever, you've launched a project. you'll be married to it until you delete it.
the engineers i admire most aren't the ones who launch the most features. they're the ones whose features keep working three years later, with no one paying attention, doing the boring job they were built to do.
that's what shipping looks like. not the demo. not the celebration. the quiet competence of a system that runs itself.