Rewrites are tempting because old code is annoying. You open a file, wince, and a quiet voice says "I'd do this so much better from scratch."

Sometimes that voice is right. Often it isn't. This post is a heuristic I use to decide, a list of traps, and an example from my own work where not rewriting was the right call.

Why rewrites seduce us

  • You understand the problem better now than when you first wrote it, so the old design looks naive.
  • Old code is visible, new code is imaginary. You see every flaw in what exists and none in what doesn't yet.
  • Early progress is fast. The first 80% of a rewrite always feels great.

The trap

Rewrites feel productive because the early days are fast. The last 20% is where they stall: the edge cases the old code already handled. Those edge cases lived in bug fixes and odd conditionals that look ugly because they encode reality. A rewrite throws them away and has to rediscover them one bug report at a time.

My heuristic

Refactor when

  • The code works and has users. Working code is an asset. Breaking it has a real cost.
  • The problems are local. One messy module, a confusing name, a duplicated function.
  • I can improve it in small, shippable steps. If every step leaves the project working, risk stays low.

Rewrite when

  • The core design makes the next feature impossible, not just awkward.
  • I understand the problem much better than when I started, and I can say exactly what I'd do differently.
  • It's small enough to finish before I lose interest, ideally in days, not months.

The test I apply first

Before any rewrite, write down, specifically, what the old design can't do.

Old design can't: 1. ... 2. ... Because: ... New design would allow: ... 

If I can't fill that in with concrete statements, it's a refactor. "It's messy" isn't a reason. "It can't support two users editing at once because state lives in a single global" is.

A middle path: copy, don't rewrite

There's a third option I use more than either extreme: copy the project, then change the copy.

When I turned my portfolio design into a template, I didn't touch the live site. I made a separate copy and refactored that until every owner-specific fact lived in one config file. The live site stayed stable; the copy became the template. I'll cover the details on Day 27.

This approach works well when:

  • the original must keep running,
  • you want to try an ambitious change without risk,
  • or you're generalising something built for one case.

Strangler-style change

For bigger systems, the safest pattern is to replace pieces gradually:

  1. Put a thin seam in front of the old piece.
  2. Build the new piece behind the same seam.
  3. Route a little traffic to it.
  4. Grow, then retire the old one.

You always have a working system, and each step can be reverted.

A decision that stayed written down

For Job Scheduler, I used Postgres as the queue instead of a message broker, and the trade-off is written down in its case study: one dependency instead of two, with a latency floor from polling and the jobs table as the throughput ceiling, acceptable at the scale I was targeting, with LISTEN/NOTIFY as the known next step.

Writing a trade-off down like that means that when the "this is limiting" itch appears, you can check it against what you already decided to accept.

Questions to ask yourself

  1. What specifically can the old design not do?
  2. How long would the rewrite really take (multiply my first guess)?
  3. What edge cases does the old code handle that I'd have to rediscover?
  4. Can I deliver value in the middle, or only at the end?
  5. Who depends on the current behaviour?
  6. Am I rewriting because it's needed, or because I'm bored?

That last one is worth asking out loud.

My rule

Before a rewrite, write down specifically what the old design can't do. If I can't, it's a refactor.

A rewrite-or-refactor scoring sheet

Score each statement 0 (false), 1 (somewhat) or 2 (true). Total at the end.

Points toward refactoring

  • The code works and people use it.
  • Problems are concentrated in a few places.
  • I can change it in steps of a day or less.
  • There are some tests, or I can add them first.
  • The team understands the current behaviour.

Points toward rewriting

  • The core design blocks a feature we need.
  • The technology is end-of-life or unmaintained.
  • I can state exactly what the old version can't do.
  • The scope is small enough to finish in days.
  • I can run old and new in parallel and compare.

If the refactoring score is clearly higher, refactor. If rewriting wins and you can define the gap precisely, consider a rewrite, ideally incrementally. If the scores are close, default to the smaller change.

Small refactoring moves

Big refactors are made of small, safe steps. Four worth knowing:

1. Extract function. Pull a block into a named function so the name explains it.

// before if (tx.status === 'FUNDED' && tx.cardholder_id === user.id && !tx.disputed_at) { /* ... */ } // after const canMarkPurchased = (tx: Tx, user: User) => tx.status === 'FUNDED' && tx.cardholder_id === user.id && !tx.disputed_at; if (canMarkPurchased(tx, user)) { /* ... */ } 

2. Introduce a seam. Put an interface between your code and a dependency so you can swap the implementation.

interface PaymentProvider { charge(amount: number): Promise<string> } class StubProvider implements PaymentProvider { async charge() { return 'stub-id'; } } // later: class RazorpayProvider implements PaymentProvider { ... } 

CardBridge's stubbed escrow and KYC are the sort of thing this makes replaceable.

3. Replace conditionals with data. A long if/else over states becomes a lookup table (like the legal-transitions map).

4. Rename for truth. If a variable says data and holds a list of claims, call it claims. Cheap and surprisingly effective.

Always have a safety net

Before changing structure, make sure something will tell you if behaviour changed:

  • an existing test suite,
  • a handful of characterisation tests that record what the code does today (even if it's odd),
  • or manual checks written down in advance.

Refactoring without a safety net is rewriting in slow motion.

Signs a rewrite is going wrong

  • You keep discovering behaviour the old code had.
  • The deadline keeps moving.
  • The old system keeps getting patched meanwhile, so you're chasing a moving target.
  • No one can say what "done" is.
  • You're excited about the new tech more than the problem.

When two or three show up, stop and check whether a refactor or a gradual migration would do.

Questions to ask a teammate

  1. If we did nothing, what's the actual cost?
  2. What's the smallest change that removes most of the pain?
  3. How will we know it worked?
  4. What's our rollback plan?

Key takeaways from day 22

What's your worst rewrite story? And did the new version end up better?

☕ Enjoyed this post? You can support my writing on Ko-fi.

Support me on Ko-fi