If you haven’t watched Stranger Things, stop reading this and go watch it. I’ll wait.
Back? Good.
For the rest of us: Hawkins, Indiana looks like a perfectly normal American town. Diners, bicycles, bad haircuts. Everything where it should be. Except underneath it — literally underneath it — there’s the Upside Down. A mirror world where everything looks familiar but nothing behaves the way it should. The lights flicker. The air is wrong. And the longer you stay in it, the harder it is to remember what normal felt like.
I’ve worked on codebases like that.
They run. They deploy. The monitoring dashboards are green and the stakeholders are happy and the Jenkins pipeline has that smug little checkmark. And then a new developer joins, clones the repo, and messages you forty minutes later asking if the README is supposed to reference a Python version that the official docs list under “historical releases.”
The lights were flickering. Nobody had been watching the lights.
This is not an article about how to write clean code. You already know you should write clean code. This is about that specific moment when you’re standing in front of a codebase, coffee in hand, and you have to make the call: do we update this thing, or do we burn it down and rebuild it properly?
Because those are not the same decision. And confusing them is how you end up spending six months in the Upside Down wondering where all your sprints went.
What “Legacy” Actually Means
Everyone throws the word “legacy” around like it means “old.” It doesn’t. Age is irrelevant. I’ve seen three-year-old codebases that were already archaeological sites and fifteen-year-old systems that were a pleasure to work in.
Legacy means the codebase stopped being able to explain itself.
The comments describe what the code used to do. The variable names reflect a product vision that got pivoted away from sometime around the third sprint. The git blame points at a developer who left the company, deleted their LinkedIn, and is presumably living off the grid somewhere in northern Europe. And the features — the features are where it gets interesting.
Every product has a shape. An original intent. A set of assumptions baked into its architecture about what it will and won’t need to do. And then the business happens to it.
The PO discovers a new market segment. Someone promises a client a feature on a call before checking with engineering. The app that was supposed to handle two hundred concurrent users is now handling twenty thousand and nobody had a meeting about it — it just sort of happened and everyone celebrated and moved on.
Each of those moments left sediment. A feature added as an outlier. A workaround that became permanent. A service that was “temporary” and is now six years old and responsible for processing forty percent of your revenue. The architecture absorbed all of it, silently, the way a city absorbs bad urban planning — one variance at a time until you look up and wonder how you ended up with a highway running through the middle of downtown.
That’s legacy. Not age. Accumulated decisions that nobody would make today, fossilized into the structure of the system.
The Update Trap
Here’s the thing about choosing to update when you should rewrite: it doesn’t feel wrong. Not immediately.
The app runs. You bump a few dependencies, fix the deprecation warnings, maybe upgrade the framework a minor version. Jenkins goes green. You ship it. Everyone moves on. Problem solved, right?
Right.
Until six months later you need to upgrade again and the package that was fine on version 4 has opinions about version 5 that it didn’t mention in the changelog. So you fix that. And fixing that breaks something adjacent that was quietly depending on a behavior that got changed in a patch release that nobody noticed. So you fix that too. And now you’re not shipping features — you’re playing a dependency version of Whack-a-Mole where every mole you hit reveals two more moles underneath.
This is the Update Trap. It doesn’t announce itself. It accumulates.
The real problem is that updating preserves the Upside Down. You’re repainting the walls of Hawkins without closing the gate. The flickering lights get a fresh coat of paint over them. The underlying physics of the place — the bad design decisions, the features that were bolted on sideways because the product scope shifted three times in eighteen months — all of that stays exactly where it is, just slightly newer looking.
And then you hit a major version hop. Not a minor bump — a major one. And suddenly everything that was held together by the particular quirks of the old version stops holding. The framework changed how it handles something fundamental. The ORM has new opinions about relationships. The authentication library was rewritten by someone who clearly had strong feelings about the previous implementation.
Major version gaps compound. Each hop works until one doesn’t. And when one doesn’t, you’re not debugging your app anymore — you’re debugging your dependencies’ relationship with each other. Which is a special kind of miserable that no standup meeting has adequate vocabulary for.
The Rewrite Trap
Now the other direction. And I promise this one hurts just as much.
The rewrite that didn’t need to happen.
You know this story. New tech lead joins. Spends two weeks reading the codebase, emerges from the experience looking like someone who just watched a documentary they can’t un-watch, and announces that the whole thing needs to be rewritten. From scratch. In a different framework. Possibly a different language. Because the current one is unmaintainable and also apparently an affront to everything they learned at their last company.
Or the quieter version: the team that’s been staring at the same codebase for three years and is simply, cosmically bored. The rewrite gets proposed in a retrospective, framed carefully as “technical debt,” ratified by people who are also bored, and suddenly six months of engineering capacity is pointed at rebuilding something that worked fine because working fine wasn’t interesting enough.
This has a name. CV-driven development. The architecture decision that looks suspiciously good on a LinkedIn post. We migrated from X to Y and reduced latency by Z percent — never mind that X was fine and the latency was never actually a problem anyone complained about.
The tell is always the same: the proposed rewrite has a much cleaner story about what’s being gained than about what specific problem made the current system impossible to continue with. If someone can’t articulate the specific breaking point — not “it’s messy” or “it doesn’t scale” in the abstract, but the concrete thing that will eventually detonate — then you’re not looking at a rewrite. You’re looking at expensive interior decoration.
Closing the gate is a serious operation. You don’t do it because you’re bored of Hawkins.
So When Is a Rewrite Actually Justified?
No framework. No decision matrix. Just the smells.
The framework is EOL and the gate is actually closing. Not “this version is old.” Old is fine. EOL means no security patches, no bug fixes, and a dependency ecosystem that starts quietly abandoning you one package at a time. This is not a vibe — it’s a timeline. And timelines have a way of becoming production incidents if you wait long enough to see how serious you actually were about them.
You’re collapsing services and the current structure can’t absorb it. Sometimes the rewrite isn’t about the code being bad — it’s about the topology changing. You’re merging two apps. You’re reducing microservices because it turned out twelve services for a team of four was an idea that looked better in the architecture diagram than in the on-call rotation. The existing structure was built for a different shape of system. Updating it won’t change its shape. Only a rewrite does that.
The original scale assumptions are so wrong they became the architecture. When the app was designed for two hundred users and now serves twenty thousand, the workarounds aren’t workarounds anymore. They’re features. They have dependencies. People have built things on top of them. At this point you’re not maintaining a system — you’re maintaining the accumulated scar tissue of every time the system was pushed past what it was designed for. That scar tissue has opinions. And they’re all wrong for where you are now.
Major version gaps that stopped being hops and became chasms. There’s a point where the distance between where you are and where you need to be is so large that the update path stops being an update path and becomes a rewrite with extra steps and worse outcomes. If you’re doing that much work anyway, at least do it with intention.
The Satellite Problem Nobody Talks About
Every rewrite conversation eventually hits the same wall.
“But we’ll lose all the institutional knowledge.”
And look — it’s not a completely unreasonable concern. It just gets applied to the wrong people.
If you’re moving from Flask to FastAPI, the core engineering team is fine. Python is Python. The patterns are different, the conventions shift a little, but nobody is starting from zero. The knowledge that matters — how the domain works, why certain decisions were made, where the bodies are buried — that survives a framework change. It lives in the people, not in the decorators.
Where the knowledge loss argument actually holds water is the teams nobody mentions in the rewrite proposal.
DevOps has pipelines built around very specific assumptions about how the current system builds, packages, and deploys. Those assumptions are now wrong. The Dockerfiles, the CI configuration, the deployment scripts that one person wrote eighteen months ago and which everyone has been carefully avoiding touching ever since — all of it needs to change. And “needs to change” in DevOps time means “will temporarily break in production at the worst possible moment.”
Observability loses its historical baselines. Your dashboards were calibrated against the behavior of the old system. The new system behaves differently — different response time distributions, different error patterns, different memory profiles. For a while, and it can be a long while, you won’t know if what you’re seeing is a bug or just the new system being different. That’s an uncomfortable place to operate from.
Analytics has assumptions baked into dashboards and reports that nobody documented because they seemed obvious at the time. Funnels that depend on specific event names. Metrics that assume a particular data shape. Reports that someone looks at every Monday morning that are quietly built on top of something that no longer exists.
Closing the gate is the right call when it’s the right call. But the Upside Down doesn’t just affect the people who went in. It affects everyone who built their world around the assumption that it would stay open.
Know who’s standing near the gate before you close it.
The Lights Are Flickering
No verdict. No checklist.
Just this: the scariest part of the Upside Down isn’t the monsters. It’s that the people inside it stopped noticing the lights were flickering. The app became the new normal. The workarounds became features. The major version gap became a problem for future you. And future you is now present you, standing in a codebase that looks like it works, holding a coffee that’s getting cold, wondering how it got like this.
Your codebase might be fine.
Or it might be Hawkins in Season 2.
The difference is whether anyone has actually checked lately — or whether everyone has just been very carefully not looking at the lights.
Member discussion: