Skip to main content
All guides

Platform modernization

Rewrite or refactor: choosing a modernization path

The question is almost never whether to rewrite or refactor the whole system. It is which parts belong in which category, and what evidence should decide it.

Updated September 22, 2025 · 8 min read

The short answer

  • Refactor when the code is hard to read but the architecture can still express what the business needs; rewrite when the architecture itself blocks the capability.
  • Treat the decision per capability, not per system. Most real modernization programs refactor the majority of a codebase and rewrite two or three parts of it.
  • The strongest signal for a rewrite is not code quality. It is a business requirement the current structure cannot represent at any price.
  • Test coverage, remaining domain knowledge, and team size change the answer more than the age of the technology does.
  • The safest version of a rewrite replaces one capability behind a stable interface while the rest of the system keeps running unchanged.

Rewrite or refactor is usually framed as a single, system-wide choice, and framed that way it has no good answer. A full rewrite is the most reliable way to spend two years producing nothing a customer can use. Indefinite refactoring is the most reliable way to spend the same two years and still be unable to ship the one feature the business is asking for. Both failure modes come from treating the system as one indivisible thing.

The useful question is narrower: for this capability, is the existing structure able to express what we now need it to do? Ask it capability by capability and the answer stops being ideological. Most systems we assess end up with a majority of code that should be refactored in place, a small number of areas that genuinely need replacing, and a handful of modules that should simply be deleted because nobody has used them in years.

When refactoring is genuinely sufficient

Refactoring is the right call when the problem is comprehension and coupling rather than capability. The system can do what you need; it is just expensive and frightening to change. That is a real cost, and it is usually fixable without replacing anything.

  • The data model broadly matches how the business actually works, even if the table names are historical.
  • New features are possible but slow, because logic is duplicated across controllers, views, and scripts.
  • Releases are risky because of thin test coverage, not because the design cannot accommodate the change.
  • The runtime and framework are still supported, or an upgrade path exists that someone has actually walked.
  • People who understand the domain rules are still employed and available to answer questions.

In this situation, the highest-value work is rarely rewriting code. It is characterization tests around the behavior you are afraid to touch, then extracting business rules out of the delivery layer so they can be tested in isolation, then breaking the largest coupling points. This is unglamorous and it compounds. A team that can deploy on a Tuesday afternoon without a war room has already solved most of what they thought a rewrite would solve.

When the architecture is the constraint

The case for replacement is strongest when a business capability is not merely expensive to build but structurally unavailable. This is a different claim from "the code is bad," and it should be defended with a specific requirement rather than a general feeling.

A common example is permissions. A system built on the assumption that a user has one role globally cannot represent a person who is a manager at one location and a technician at another. That is not a refactor; the assumption is baked into the schema, the session, every query, and every screen. Multi-tenancy, audit history, and currency handling behave the same way. They are either designed in or retrofitted at a cost close to replacement.

  • A required capability contradicts an assumption embedded in the data model, not just in the code.
  • The framework or runtime is past end of support, so security patching and hiring are both getting harder.
  • Integration is now a product requirement and the system was never structured to expose a stable interface.
  • The cost curve is inverted: each release makes the next one more expensive rather than cheaper.
  • Two or more of the above are true at once, which is the usual real-world case.

Any single symptom here can normally be absorbed. When three stack, incremental work stops paying back, because every improvement fights the same underlying assumption.

The middle path: rewrite behind a stable interface

The version of a rewrite that works in practice is bounded. You choose one capability, define the interface the rest of the system uses to reach it, build the replacement behind that interface, and cut over when it is demonstrably better. The rest of the system does not know anything changed. If the new implementation is wrong, you route back.

  1. 1

    Pick one capability with a clear boundary and real business pain. Do not start with the hardest or the most central.

  2. 2

    Write characterization tests describing what the current implementation actually does, including the behavior nobody documented.

  3. 3

    Define the interface the rest of the system will use, and put the legacy implementation behind it first. Nothing has been rewritten yet.

  4. 4

    Build the replacement behind the same interface, running against the same data where possible to avoid a one-way migration.

  5. 5

    Shift traffic progressively with a fallback path, and keep the old implementation deployable until the new one has survived a full business cycle.

  6. 6

    Delete the old implementation. A rewrite that leaves both versions running has doubled the maintenance burden, not reduced it.

The step teams skip is the last one. Two implementations of the same capability is the worst possible resting state, and it is where stalled modernization programs come to live.

A fifteen-year-old retail and service platform was rebuilt capability by capability alongside the system it replaced, with reversible cutovers and a low blast radius at each stage. There were no big-bang cutovers, and the businesses running on it stayed open throughout.
Rebuilding for the next decade, an anonymized B-Team engagement

Symptoms and the path they point to

This table maps the complaints we hear at the start of an assessment to the work that usually resolves them. It is a starting hypothesis, not a verdict, and the point is that most symptoms do not point at a full rewrite.

SymptomUsual underlying causePath that usually resolves it
Every release requires a war roomMissing test coverage and no safe rollbackRefactor: characterization tests, CI gates, deployment automation
Simple changes break unrelated featuresBusiness logic duplicated across the delivery layerRefactor: extract a domain layer and enforce boundaries in CI
Nobody will touch one particular moduleConcentrated complexity and lost knowledgeTargeted rewrite of that module behind a stable interface
A required feature is estimated in quarters, repeatedlyThe data model cannot represent the conceptRewrite that capability; refactor around it
Partners cannot integrate with usNo stable interface was ever designedRefactor first: expose an interface over existing behavior
The runtime is out of supportDeferred upgrades compounding over yearsUpgrade or replace the platform layer; this is not optional work
Onboarding a new engineer takes monthsUndocumented conventions and no architectural seamsRefactor: documentation, seams, and enforced structure
Common symptoms and the modernization path they typically indicate

What changes the answer

Test coverage

Coverage is the single biggest input. With a meaningful suite, refactoring is safe and incremental, and the case for a rewrite weakens considerably. With no tests, refactoring is guesswork and rewriting is a specification problem, because the only complete description of the business rules is the code you are trying to replace. If you have no tests, your first move is tests, regardless of which path you eventually choose.

Remaining domain knowledge

Rewrites consume domain knowledge at a rate people underestimate. Fifteen years of accumulated rules (the exceptions, the one customer with unusual terms, the workaround from a 2014 incident) has to be rediscovered and re-encoded. If the people who know why the system behaves the way it does have left, a rewrite is substantially riskier than the code quality suggests, and preserving existing behavior becomes the dominant cost.

Team size and delivery obligations

A small team cannot maintain a legacy system and build its replacement at full speed simultaneously. Something gives, and it is usually the legacy system, which is still the one earning revenue. Either fund both tracks honestly or narrow the rewrite until one team can carry it alongside its existing obligations. A rewrite scheduled around whatever capacity is left after support work is a rewrite that will not finish.

Whether the business can wait

If a contract renewal, an audit, or a customer commitment lands in four months, that date is an architectural constraint. It rules out any path with no deliverable before then, which in practice rules out a full rewrite and often argues for a bounded bridge while the longer work proceeds behind it.

Cost and risk, stated honestly

PathTime to first user-visible valueMain riskWhere it fails
Incremental refactoring2–6 weeksEffort disappears into maintenance and never reaches a visible outcomeWhen the underlying constraint is architectural and no amount of cleanup removes it
Capability rewrite behind an interface6–14 weeks per capabilityBoth implementations stay alive and maintenance cost doublesWhen the boundary is drawn badly and the interface leaks legacy assumptions
Full parallel rebuild, phased3–6 months for the first sliceScope grows to include every legacy behavior, including the unused onesWhen feature parity is the goal instead of the specific capabilities that matter
Big-bang replacement12–24 months, all at onceNo feedback until the cutover, and no way backAlmost always; we do not recommend this shape
Illustrative profile for a mid-sized operational system; ranges are our typical engagements

A two-week way to decide

You do not need a six-month study. You need enough evidence to make the call per capability and to know what would change your mind.

  1. 1

    List the capabilities of the system, then mark each one as heavily used, lightly used, or dead. Use logs, not opinions.

  2. 2

    For each heavily used capability, name the change the business wants and ask whether the current data model can represent it.

  3. 3

    Measure the cost curve: how long did comparable changes take two years ago versus now?

  4. 4

    Identify where domain knowledge lives, and whether it is in tests, documentation, or one person's memory.

  5. 5

    Choose the smallest capability that is both structurally blocked and commercially painful, and scope that one as a rewrite behind an interface.

  6. 6

    Put everything else on the refactoring track, and set an explicit review date to re-examine the classification with new evidence.

The output is a sequence rather than a verdict: a small number of replacements with clear boundaries, a continuous refactoring effort around them, and a list of things you have agreed to stop maintaining. That sequence is what makes modernization fundable, because each step produces something in production instead of a promise about a future cutover.

Common questions

Is it ever right to rewrite an entire system at once?
Rarely, and only when the system is small enough that one team can rebuild it inside a few months, or when it has so few users that a bad cutover is survivable. For anything business-critical, the risk profile is poor: you get no feedback until the end and no way back. If the whole system truly must be replaced, phase it by capability and run the old and new versions side by side.
How do we know whether the architecture is really the constraint?
Name a specific capability the business needs and trace what it would take to build. If the blocker is duplicated logic, missing tests, or tangled code, that is a refactoring problem. If the blocker is an assumption in the data model (one role per user, one currency, one tenant, no history), then the architecture is the constraint and cleanup will not remove it.
Can we refactor and rewrite at the same time?
Yes, and that is usually the correct shape. One or two capabilities get replaced behind stable interfaces while the rest of the codebase is improved in place. The risk is capacity: if the same engineers are doing both, the replacement work gets starved by support obligations. Separate the tracks explicitly or accept that the rewrite will move slowly.
What do we do when nobody understands the legacy behavior anymore?
Write characterization tests before you change anything. They describe what the system currently does rather than what it should do, which is exactly what you need when the specification has been lost. This work is valuable on either path, and it is the cheapest insurance available against a replacement that quietly drops a rule someone depends on.
Should we upgrade the framework first or start replacing capabilities?
If the runtime is out of support, upgrading is not really a choice. Security patching and hiring both depend on it. If it is still supported, sequence the upgrade behind the capability work so that business value ships first. Doing both at once is possible but doubles the number of moving parts during every cutover.
How long before a modernization program shows results?
Expect measurable delivery improvements from refactoring within a couple of months, and the first replaced capability in production within one to two quarters. If a plan produces nothing user-visible for longer than that, the increments are too large. Shorter increments cost the same in total and fail visibly instead of expensively.

Want this applied to your operation?

The first conversation is a no-cost fit discussion about the problem, its importance, and the people involved. We respond within one business day.

Start a conversation