All writing
Software EngineeringMay 28, 2026·12 min read

Why My Code Looks Dirty in 6 Months

It's not that you got worse. It's that 'clean' is a moving target, and your past self was working with less information and more deadlines.

Open a file you wrote six months ago. Go on. I'll wait.

Feel that? That little wince? The quiet "who *wrote* this" followed by the horror of git blame confirming it was, in fact, you? Congratulations. That wince is the most reliable sign that you've grown as an engineer. Code that still looks perfect after six months usually means you stopped learning, not that you nailed it.

01

Your code didn't rot. The world moved.

When you wrote that file, it was correct *for what you knew then*. It was a snapshot of your understanding, your constraints, and your deadline on one specific Tuesday. Then reality did what reality does:

  • The requirements changed three times. The abstraction you picked was perfect for requirement #1 and actively hostile to #4.
  • You learned things. The patterns that now feel obvious were invisible to you in February.
  • A deadline happened. You made a deal: ship now, clean later. 'Later' is a mythical land, like Atlantis or inbox zero.
  • Three other people touched it, each leaving the campsite slightly messier, none of them with the full picture.
Clean code isn't a state you reach. It's a direction you keep walking while the ground shifts under you.
02

The two kinds of rot

It helps to separate them, because they have different cures.

Local rot

A single function that grew weird. A variable named temp that's now load-bearing. This is cheap to fix — you just need five minutes and the will to do it while you're already in there.

Structural rot

The architecture itself no longer matches the problem. Your data model assumed one thing; the business now needs another. This is the expensive kind, and no amount of tidying individual functions fixes it. You have to change the shape.

03

Tech debt is a loan, and the interest is real

The metaphor gets thrown around so much it's lost its teeth, so let's make it concrete. Every shortcut is borrowed time. You get speed *now* and pay it back *later* — with interest, in the currency of slower changes and weirder bugs. The interest is the part people forget. Debt you never repay doesn't disappear; it just quietly taxes every future change.

Here's what the interest payment looks like in practice:

orders.ts
// v1: shipped in a hurry. "we only have one payment provider, it's fine"
function charge(order) {
  return stripe.charge(order.total, order.card);
}

// 6 months later: three providers, two currencies, one refund flow,
// and a compliance rule bolted on by someone who has since quit.
function charge(order) {
  if (order.region === 'EU') { /* ...40 lines... */ }
  else if (order.provider === 'legacy') { /* ...do not touch... */ }
  // TODO: nobody knows what this branch does. it stays.
}

Heads up

The dangerous debt isn't the ugly code you can see. It's the assumption baked in so deep that nobody remembers making it — like "there will only ever be one provider."

04

Not all debt is bad debt

Some debt is smart. Shipping a scrappy v1 to learn what users actually want is a *great* trade — you avoid lovingly engineering the wrong thing for six months. The problem isn't borrowing. It's borrowing silently and then acting surprised when the bill arrives.

Think of it like a 2x2: deliberate vs. reckless, prudent vs. inadvertent. "We know this is a hack, here's the ticket" is deliberate and prudent — fine. "What's a layered architecture?" is inadvertent and reckless — that's the debt that sinks teams.

Make the debt visible

Write it down where the next person will trip over it, not in a Jira ticket that dies in the backlog:

cache.ts
// DEBT: in-memory cache, single instance only. Breaks the moment we scale
// horizontally. Swap for Redis before we add a second node. Owner: me. Due: Q3.
const cache = new Map<string, Result>();
05

The rewrite will not save you

I know. The big rewrite is *so* tempting. A clean slate. No legacy. This time you'll do it right. Let me gently break the news: the rewrite will accumulate its own crust within the year, except now you have *two* systems to maintain and a migration that outlives your patience.

Joel Spolsky famously called rewrites "the single worst strategic mistake" a software company can make, and he had a point. The old code looks bad partly because it's *survived* — it's full of bug fixes for edge cases you've already forgotten. Throw it out and you get to rediscover every one of them. Personally. In production.

Tip

Prefer the strangler-fig pattern: wrap the old system, route new functionality through the new code, and migrate the old paths one at a time until the legacy vine is dead. Less heroic, far less likely to get you fired.

06

A refactor that's actually safe

The reason refactoring scares people is that they do it without a net. The net is tests. Pin the current behavior first, *then* change the internals freely:

refactor.test.ts
// 1. characterization test: lock in what it does NOW, warts and all
test('charge: legacy EU order', () => {
  expect(charge(legacyEuOrder)).toEqual({ status: 'ok', fee: 0.029 });
});

// 2. now refactor charge() with confidence — the test screams if you break it
// 3. only once it's clean do you change behavior, one test at a time
07

How to keep the rot in check

  1. 01Budget for it. Spend a slice of every sprint on cleanup. If it's not on the calendar, it doesn't exist.
  2. 02Refactor *while* you're in there. You already have the context loaded — leave the campsite cleaner than you found it.
  3. 03Write tests before you change scary code, so future-you can refactor without praying.
  4. 04Delete aggressively. The best-maintained code is the code that no longer exists.
  5. 05Track the big structural debts explicitly, with an owner and a rough date. Invisible debt never gets paid.
  6. 06Make peace with the wince. It's a feature.

Your code looking dirty in six months isn't failure. It's evidence that you, the requirements, and the world all kept moving. The goal was never code that stays clean forever. It's code you can keep *changing* without dread. Aim for that, and the wince becomes a quiet kind of pride.

Key takeaways

  • 01Code rots because requirements change and you learn — not because you're bad.
  • 02Tech debt is a real loan; make it visible and pay it down on a schedule.
  • 03The big rewrite almost never wins. Refactor in small, tested steps instead.

FAQ

EngineeringTech DebtArchitecture

Related reading