Every Insane Codebase Was Once Rational

Architecture certainty isn't on the menu. Decide for the horizon you can see — three to six months — and know which doors only open one way.

TL;DR: A new grad running his first project asked me how to know whether his architecture decisions are right. They aren’t right or wrong — they’re trade-offs, and certainty was never available at any level of seniority. The reframe that helps: every codebase you’ve inherited and cursed was a sane response to constraints that are now invisible to you. Which means your decisions get judged the same way, by someone who won’t have your constraints either. So decide for the horizon you can actually see — three to six months — and stop paying interest on a future you’re guessing at. The exception matters and I’ll name it: one-way doors get the long view, and there are fewer of them than you think.

The question underneath the question

A new grad reached out a few weeks ago. About a year in, he’s running an entire project — front end, back end, plus a younger developer reporting to him. He asked whether I’d mentor him, and when I asked what specifically he was stuck on, the answer was the one I expected:

He’s not sure he’s making the right architecture decisions.

Then, a few days later, the more honest version: he assumes there’s some looming factor he hasn’t considered — something a more experienced person would have seen coming — and that assumption is what stalls him. Lately more of his week goes to fires than to building. He’s worried he’s already built an insane codebase.

I want to answer the second version, because the first one has no answer.

There is no right decision, only trade-offs you can defend

You can never be certain you’re making the right architecture decision. Not at one year, not at thirteen. Most things in software aren’t correct-or-incorrect, they’re trade-offs with different bills arriving at different times. Postgres or a queue. One service or three. Fat model or thin. Every one of those choices buys something and mortgages something else, and which side of the trade you want depends entirely on facts about your company that will change.

That’s not a comforting non-answer. It’s the actual mechanism, and once you see it as a mechanism you can work with it instead of waiting for a certainty that isn’t coming.

The reframe: every insane codebase was once rational

Here’s the sentence I keep coming back to, and the one I gave him.

Every insane codebase is a perfectly sane response to constraints that existed when it was written.

Think about the worst codebase you’ve worked in. The one with the god object, the three overlapping auth paths, the config system nobody can explain. It feels like it was built by people who weren’t paying attention.

It almost never was. It was built by people making reasonable calls against constraints you can’t see from where you’re standing:

  • A deadline tied to a funding round, where shipping in six weeks was the difference between a company and no company.
  • A team of two who couldn’t operate five services, so everything went in one, correctly.
  • A library that was the obvious choice in 2019 and got abandoned in 2022.
  • A customer contract that required a specific behavior, signed before anyone knew what it would cost to build.
  • A person who understood the whole thing and left, taking the reasoning with them and leaving only the artifact.

Strip the constraints out and the code looks insane. Put them back and most of it looks like what you’d have done. The insanity isn’t in the decisions. It’s in the gap between the decisions and the context that’s no longer attached to them. I go through this exercise in detail whenever I take over someone else’s work — the questions I ask when I inherit a vibe-coded app are mostly archaeology, trying to recover the constraints before I judge the code.

Now turn it around, because this is the part that’s actually useful to a nervous first-time lead. Your decisions will be judged the same way. Someone will open your repo in three years without your deadline, your headcount, your customer, or your 2026 tooling, and they will think you weren’t paying attention. That’s not a failure you can avoid by being smarter. It’s the default outcome of time passing.

Which means optimizing to look good to that future person is optimizing for an audience whose constraints you cannot know. It’s not achievable. Aim somewhere else.

Aim at three to six months

The horizon you can actually see is about a quarter or two. That’s the window where you have real information: the customers you have now, the team you have now, the roadmap that’s been committed to, the tools that exist today. Inside that window, your predictions are grounded. Outside it, you’re doing fiction with extra steps.

So make the decision that’s right for the next three to six months, and let the version after that be made by someone — probably you — who has information you don’t have yet.

This is agile’s actual point, before it got turned into a meeting schedule. Not “move fast.” Not “two-week sprints.” The original manifesto put it as responding to change over following a plan — you change direction as information arrives, which only works if you haven’t already spent the budget pre-building for a direction you guessed.

That horizon has gotten shorter, not longer, and I say this as someone who mostly builds with agents now. The tooling under your project is changing faster than the project. Model capabilities, the agent harnesses around them, and the pricing of the whole thing — which is currently an arbitrage with an unknown shelf life. An architecture chosen in January to work around a limitation that no longer exists in July is not a wise long-term bet. It’s a fossil.

What “optimize for the near term” actually means in code

This is where the advice usually goes vague, so let me make it concrete. Optimizing for the visible horizon is not “write it sloppy.” It’s a specific set of preferences:

Prefer decisions you can reverse over decisions that are correct. A choice you can undo in a day doesn’t need to be right. It needs to be undoable. Spend your certainty budget on the ones you can’t unwind.

Prefer deletable code over extensible code. Extensibility is a bet on a specific future shape; deletability is a bet that you’ll be wrong about the shape, which is the safer bet. A module with two callers and no config surface can be ripped out on a Tuesday — a plugin system with one plugin cannot. This is the same reason the abstraction you add for a second use case that doesn’t exist yet is usually wrong about it. Generalizing from two real examples is easy. Guessing one is not.

Write down why, not just what. This is the one that pays back the most, and almost nobody does it. Most of the pain of inheriting a codebase is missing constraints, not missing docs. A three-line comment or ADR saying “single service because two people and a March deadline; split when we’re past four engineers” converts a future insane decision into a legible one. That’s the entire difference.

Let the pain point tell you when to change. Don’t restructure on a schedule or because a blog post said to. Restructure when something concrete hurts — deploys are slow, this module breaks every time that one changes, onboarding takes two weeks. Then fix that. Then stop.

The exception: one-way doors get the long view

I’d be selling you something if I stopped there, because “decide for six months” is genuinely bad advice for a small set of decisions. Some doors only open one way, and the whole reason the three-to-six-month rule works is that you’ve correctly sorted out the ones it doesn’t apply to.

The one-way doors, roughly:

  • Data model and storage semantics. Not which database — you can move databases. What you promise about your data: uniqueness, ordering, what a record means, what you kept and what you threw away. Discarded data is unrecoverable, and a wrong meaning propagates into every report and integration built on it.
  • Your public interface. API shapes, webhook payloads, URL structure, anything a customer or partner has written code against. Once someone else depends on it, changing it is their project, not yours.
  • Authentication and identity. Migrating identity providers with live users is a genuinely hard project — I’ve done one across 225K+ users without forcing a mass logout, and the reason it was hard is that every edge case in every auth flow had to be handled, not just the happy path.
  • Anything touching money or compliance-relevant records. Ledgers, audit trails, retention. Getting these wrong is a legal problem, not a refactor.

For these, take the long view, think hard, and be slower than feels comfortable. For everything else — service boundaries, folder structure, which queue, which state library, how you organize your tests — decide fast, on near-term evidence, and stay ready to change your mind. The pattern I see most often is the inversion: weeks of agonizing over reversible layout decisions, and a permanent data-semantics call made in an afternoon because it didn’t feel like a big deal.

The looming factor usually isn’t there

Back to what he actually said, because it’s the part that matters most and it isn’t technical: he assumes there’s some factor he hasn’t considered, and the assumption itself is what’s slowing him down.

Sometimes there is one. Usually there isn’t. What’s actually there is the ordinary gap between the decision and the outcome — the thing everyone experiences and almost nobody says out loud, because senior engineers narrate their past decisions with the confidence of hindsight and it reads like they knew.

They didn’t know either. They found out, then told the story backwards.

You never know as much as you will in hindsight. That’s not a seniority problem you can study your way out of; it’s the shape of the work. The senior engineer’s real advantage isn’t foresight, it’s a bigger library of failure modes they’ve personally hit — which is exactly what you’re building right now, in the fires you’re embarrassed about putting out. The path to senior always ran through that pile, and I’ve written elsewhere about how the work nobody wants is what builds the map fastest. Those fires aren’t evidence you’re doing it wrong. They’re the mechanism.

The right posture isn’t confidence and it isn’t paralysis. It’s making the call, writing down why you made it, and staying willing to be wrong out loud in three months. Which, incidentally, is also the thing that makes people trust you as a lead — certainty theater is a much worse look than a documented trade-off.

What to do this week

  1. Sort your open decisions into two-way and one-way doors. Two-way: decide today, note the reasoning, move. One-way: slow down, get a second opinion, and be sure you’re right about what it means, not just what it is.
  2. Find the oldest decision you’re still paying for and ask what constraint produced it. If the constraint is gone, you’re allowed to change the decision. If you can’t find the constraint, that’s your first thing to write down.
  3. Add a why line to your next non-obvious commit or PR. Constraint, horizon, trigger to revisit. Three lines. It’s the highest-leverage documentation you’ll ever write.
  4. Stop pre-building for the second use case. Find one abstraction you added for a case that never arrived, and delete it.
  5. Name your next architecture decision’s expiry date out loud. “Single service until we’re past four engineers.” A decision with a stated horizon stops feeling like a permanent verdict on your judgment.

If you’re leading a small team and trying to figure out which decisions deserve the long view — or you’ve inherited something that looks insane and you need to know what’s salvageable — let’s talk.