writing code you'd be comfortable maintaining in three years

jun 2025

how i think about code i'd still want to work on years later, and what i learned from the times i didn't.

there's a version of software development that treats handoff as the finish line. you ship the feature. the tests pass. the ticket closes. whatever comes next is someone else's problem, or your future problem, which is a problem so abstract it barely registers as real.

i stopped believing in that version the first time i opened a codebase i'd handed off a year earlier and found myself reading my own code with the mild horror of meeting a stranger's opinions. not broken code. functioning code. code that worked but no longer made any obvious sense, that had accumulated the sediment of fast decisions, unresolved compromises, and clever shortcuts whose cleverness had expired with the context that produced them.

the question i started asking after that. if i came back to this in three years, having forgotten everything about the project, could i understand what it does and why? not just what it does, any motivated engineer can reverse-engineer that. why. what problem this particular approach was solving, what alternatives i considered and discarded, what the code knows about the domain that isn't visible in the domain itself.

the cleverness problem

clever code is almost always a liability. not immediately, immediately it's satisfying in a way other kinds of writing aren't. you've compressed something complex into something compact. you've used the language in a way that shows you understand it. there's real pleasure in that, and i'm not saying there shouldn't be.

but cleverness optimises for the moment of writing rather than the years of reading, and software is read far more often than it's written. the clever solution that took twenty minutes to produce takes forty to understand every later time someone meets it. if it's in a hot path, or near a fragile integration, or in the logic that decides who can see what, and clever things tend to end up in exactly those places, because that's where the complexity lives, the tax adds up fast.

what i've found works. when i write something that makes me feel clever, i flag it. not for deletion, sometimes it's genuinely the right call, but for a second look the next day, once the satisfaction has worn off and i can judge it on the merits. the question isn't "is this correct?" but "is this the simplest correct thing?" those are different questions, and answering the second one honestly takes a certain amount of ego suspension.

comments as design documents

code comments have a bad reputation that's mostly deserved. comment-as-narration, // increment i by 1 above i++, is worse than no comment, because it adds noise without signal and creates the impression that commenting is happening when it isn't. these comments describe the code instead of illuminating it, and they age badly as the code changes around them.

the comments i write are different in kind. they explain the why, the decision, not the mechanism. they describe what i considered and didn't choose, and what would break if someone refactored past the constraint the code is working around. they're asynchronous messages to whoever works on this next, including the version of me who'll have forgotten everything by then.

a comment that says // this limit exists because the upstream api rate-limits at 100 requests per minute and there's no retry budget in this path. do not remove without checking that budget first is a design document compressed into one line. it makes the codebase safer not by describing what the code does but by preserving the knowledge that makes it possible to change the code correctly. that kind of comment ages well. it's also harder to write than // increment i, which is why it gets written less often.

testing as a form of specification

the tests i write before i ship aren't primarily for catching bugs, though they do that, and it's worth it on those grounds alone. they're primarily a specification. a description of what the code is supposed to do that's precise enough to be executable, durable enough to survive refactors, and explicit enough that a new engineer can read the suite and understand the intended behaviour without reading every line of implementation.

tests written this way, tests that describe outcomes in terms of user-visible behaviour rather than implementation details, are harder to invalidate when the implementation changes. a test that checks whether a user with expired credentials gets redirected to the login page survives a complete rewrite of the auth system. a test that checks whether a specific internal function is called with specific arguments is brittle in proportion to its specificity, and needs rewriting every time the implementation evolves.

the suite i want to inherit is one that gives me confidence without constraining me. it tells me what must be true about the system. it doesn't tell me how the system must achieve it. that's the specification, and it's worth writing carefully.

on handoff as craft

the most important check i run before i call something done isn't automated. it's a reading. someone who wasn't the primary author going through the code, the documentation, the deployment config, and asking out loud wherever something is unclear. not to gatekeep, but to surface the gaps that familiarity hides. the author always knows more than the code says. the question is how much of that knowledge is in their head and how much is in the repository.

a handoff that lets the next person own the code, extend it, fix it, and eventually rewrite parts of it with confidence, is the end product of this. it's also, i've found, a decent proxy for quality overall. code that hands off cleanly is usually code that was built with discipline. code that resists handoff is usually code that accumulated complexity without ever being cleaned up, and will keep accumulating it until someone pays the debt.

i pay it before i move on. it's not always fast. it's always worth it.