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:
- Put a thin seam in front of the old piece.
- Build the new piece behind the same seam.
- Route a little traffic to it.
- 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
- What specifically can the old design not do?
- How long would the rewrite really take (multiply my first guess)?
- What edge cases does the old code handle that I'd have to rediscover?
- Can I deliver value in the middle, or only at the end?
- Who depends on the current behaviour?
- 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
- If we did nothing, what's the actual cost?
- What's the smallest change that removes most of the pain?
- How will we know it worked?
- What's our rollback plan?
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.


Top comments (0)