Guide

When the unit of change is a system, not a function, a big-bang rewrite usually fails. Features freeze for months. The new system rediscovers edge cases the old one encoded in scars. The business cannot wait. Prefer increments that each leave production safer.

The patterns share one idea: old and new run together until the old path has no traffic.

Concept

Start from characterization tests and seams. If you cannot pin today's behavior or route gradually, you are not ready to migrate — you are ready to add tests and seams first.

The patterns

PatternIdeaUse when
Strangler figNew implementation grows around the old; route traffic graduallyReplacing a monolith module or service
Expand/contractAdd new fields/paths, dual-write, move readers, remove oldSchema and API evolution
Parallel runOld and new compute; compare outputs; cut over when they matchHigh-risk billing or pricing
Feature flagsGate the new path per cohortDark launch and abort
CodemodsMechanical AST transformsRenames and API upgrades at scale
Strangler sketch:
  Client --> Facade
               |-- /v1 legacy handlers  (shrinking)
               |-- /v2 new handlers     (growing)
  Cut routes one by one; delete legacy when traffic is zero.

Extraction across services is the same pattern, not a free lunch. See microservices for the trade-offs.

Walking through expand/contract

Schema and API migrations fail when you try to remove old and add new atomically under live traffic.

Expand: add the new column, endpoint, or field; dual-write or backfill; keep old readers working.

Migrate: move readers to the new shape; verify with metrics.

Contract: remove old writes, then old fields, once unused.

Renaming user_name → display_name on a hot table:

  1. Add display_name nullable; backfill in batches; the app writes both.
  2. Flip readers to display_name behind a flag; watch error rates.
  3. Stop writing user_name; confirm no queries remain (slow-query logs, proxy SQL logs, or code search).
  4. Drop user_name in a low-traffic window.

Each step is deployable and reversible.

The PricingEngine story is the system-sized version. After characterization and a seam, week 3 puts a new engine behind a flag for 5% of shops and parallel-runs the totals. Week 4 ramps to 100% and deletes dead branches. Same calendar time as a rewrite attempt, but revenue never paused.

Codemods

When an API rename touches hundreds of call sites, a reviewed codemod plus CI is safer than a weekend of hand edits. Write the transform narrowly, run it on a branch, spot-check diffs, and keep a manual escape hatch for weird call sites. Dynamic calls the AST cannot see still need human judgment. Pair the transform with characterization tests on the hottest paths so a bug fails in CI, not silently in production.

Common mistakes

  • Stop-the-world for six months. Ship increments that each reduce risk.
  • Flip DNS on day 180 with fingers crossed. Even a justified rewrite sits behind a facade with parallel run.
  • Deleting legacy before traffic is zero. Deletion is the victory lap, not the start.
  • Codemod without a pin on the hottest paths. Mechanical change still ships bugs.

Check yourself

  1. You must rename a hot column. What is the first deployable step? Add the new column, backfill, and dual-write. Do not drop the old name yet.

  2. Parallel-run diffs on pricing are still non-zero. Do you ramp to 100%? No. Cut over when old and new match, then delete the old path.

Key takeaways

  • Prefer strangler, expand/contract, parallel run, flags, and codemods over rewrites.
  • Every step should be deployable and reversible.
  • Delete legacy only after traffic is gone.
  • If you cannot characterize or route gradually, you are not ready to rewrite.

Further learning

StranglerFigApplication (Martin Fowler)Strangler Figmartinfowler.com

Next, fund the work in selling tech debt.