Application Modernization

Legacy Application Modernization: Rewrite or Improve Incrementally?

A practical framework for choosing between a full rewrite, incremental modernization with the strangler pattern, and targeted refactoring of legacy software.

Conceptual overview, not a screenshot

Every successful application eventually becomes a legacy application. The code that won the first customers is still running, now surrounded by years of additions, workarounds and dependencies that nobody would choose today. At some point the team starts asking whether it would be faster to throw it away and start again.

Sometimes it would be. More often, a full rewrite trades a known set of problems for a larger, less visible set. This article sets out when a rewrite is justified, why rewrites carry hidden risk, and how incremental modernization and targeted refactoring offer safer paths for most systems.

Why Legacy Applications Become Difficult to Maintain

Age alone does not make software legacy. Michael Feathers’ book Working Effectively with Legacy Code popularized a practical definition: code without tests. Without tests, every change is a gamble, so changes slow down and the code calcifies. Common symptoms include:

  • Unsupported foundations. A framework, language runtime or database version that no longer receives security updates.
  • Tangled dependencies. A change in one area breaks something apparently unrelated.
  • Lost knowledge. The people who understood why the code works the way it does have moved on.
  • Manual, risky deployments that happen rarely because each one is stressful.
  • Scaling limits, such as long-running work inside web requests or a single database doing everything.
  • Integration friction. Connecting to modern services takes more effort than building the feature itself.

The business symptom is the same in every case: simple changes take weeks, and nobody can say with confidence what a change will break.

When a Full Rewrite Makes Sense

A rewrite is the right call in a narrower set of situations than teams often expect:

  • The platform is a dead end. The technology is unsupported and has no realistic upgrade path, or the product must move to a platform the old code cannot run on.
  • The system is small. Understanding and testing the existing code would cost more than rebuilding it.
  • The product itself is changing fundamentally. If most of the old behavior is no longer wanted, there is less to preserve.
  • The current system cannot be changed safely at all, for example because it cannot be built or deployed from source.

Even then, the rewrite needs a migration plan: how data moves, how long both systems run side by side, and how users are switched over.

Why Rewrites Often Carry Hidden Risks

The existing code contains years of decisions that are written down nowhere else: bug fixes for unusual customers, edge cases in calculations, integration quirks. In his essay Things You Should Never Do, Part I (opens in a new tab), Joel Spolsky used Netscape’s rewrite as an example of how throwing away working code also throws away that accumulated knowledge.

Other risks compound it:

  • The target keeps moving. The business still needs changes in the old system during the rewrite, so the team maintains two systems or freezes one.
  • Value arrives late. Months of work deliver nothing to users until the switch-over.
  • The cutover is a single, large risk. Every feature, integration and data record changes at once.
  • Data migration is underestimated. Real data contains inconsistencies the new design never anticipated.

Benefits of Incremental Modernization

Incremental modernization replaces or improves the system in small, reversible steps while it keeps running. Each step delivers something useful, contains its own risk and can be undone if it goes wrong. The team learns how the system really behaves as it goes, and the business never has to stop for a cutover.

The costs are real too. For a period there are two implementations of some behavior, a routing layer between them and possibly data kept in sync. Without discipline, the old system is never fully retired. The overall effort can also be larger than a rewrite would have been if everything had gone to plan. The point is that the incremental path rarely goes catastrophically wrong.

The Strangler Pattern Explained

Martin Fowler named this approach the Strangler Fig (opens in a new tab) after vines that grow around a host tree until they replace it. New components grow around the old system and take over its behavior piece by piece.

In practice it looks like this:

  1. Put a routing layer in front of the system. A reverse proxy, API gateway or facade in the code that initially passes everything through unchanged.
  2. Choose a seam. A capability with clear inputs and outputs, such as reporting, notifications or one group of API endpoints.
  3. Build the new implementation of that capability alongside the old one.
  4. Shift traffic gradually, perhaps to internal users first, then a percentage of requests, while comparing results.
  5. Switch fully, then delete the old code for that capability. Deleting it is the step most often skipped.
Replacing a legacy system one capability at a time
  1. Users and integrations
  2. Routing layerProxy · gateway · facade
  3. Routed per capability
    Legacy applicationUnchanged features
    New serviceMigrated capability
  4. Data moved per capability
    Legacy databaseStorage & queues
    New data storeStorage & queues
  5. Compare, monitor, then retire the old path
  • Data flow
  • Processing
  • Storage & queues
  • Outcome
A conceptual view of the strangler pattern. Each capability moves behind the routing layer only after its behavior has been compared with the legacy path.

When the seam is inside the codebase rather than at the network edge, the related technique of branch by abstraction (opens in a new tab) applies: put an abstraction in front of the old implementation, build the new one behind the same abstraction, and switch over while the system keeps being released.

API Extraction and Modularization

Extraction goes better when boundaries are made explicit inside the existing application first. Group code by business capability, route calls through defined interfaces and stop reaching across modules into each other’s data. A modular monolith with clean boundaries is often a perfectly good destination on its own.

When a capability does need to become a separate service, perhaps because it scales differently or another team owns it, define its API contract first, write contract tests that both sides run, and version the API from the start. Not everything needs to become a microservice. Each separate service adds deployment, monitoring and network failure modes that a module inside one application does not have.

Database Migration Considerations

The database is usually the hardest coupling to break, because many parts of the application read and write the same tables. Change it in small, compatible steps using parallel change (opens in a new tab), also known as expand and contract:

  1. Expand. Add the new tables or columns alongside the old ones.
  2. Migrate. Write to both, backfill historical data, and move reads to the new structure.
  3. Contract. Remove the old structure once nothing uses it.

Write backfill scripts so they can be stopped and resumed safely, and verify the results with record counts and checksums rather than by eye. Expect data-quality surprises such as orphaned records, unexpected nulls and values outside documented ranges, and decide in advance how each will be handled. Every step should have a rollback plan.

Testing Before Replacing Components

Before changing legacy behavior, capture what it does today:

  • Characterization tests record the current outputs for known inputs, including outputs that look wrong. The goal is to notice changes, not to judge correctness yet.
  • Golden-master comparisons work well for reports, calculations and exports: generate outputs from the old and new code for the same inputs and compare them.
  • Parallel runs send the same production requests to both implementations, serve the old result, and log any difference until the new path matches.
  • Contract tests protect the boundaries of extracted APIs.

Production monitoring completes the picture. Error rates, latency and business metrics for the migrated capability should match the old path before traffic moves fully.

Security and Infrastructure Improvements

Some of the most valuable modernization work changes no features at all. Updating unsupported runtimes and dependencies, moving secrets out of the code, enforcing least-privilege access, testing that backups can be restored, adding automated build and deployment, and introducing logging and monitoring all reduce risk immediately. These steps also make every later change safer, so they are often the right place to start.

A Practical Modernization Roadmap

  1. Assess. Map the architecture, dependencies, deployment process, test coverage and the areas causing the most business pain.
  2. Stabilize. Address security exposure, backups, monitoring and deployment reliability.
  3. Build a safety net of characterization tests around the first area you plan to change.
  4. Introduce the seam, whether that is a routing layer, a module boundary or an abstraction.
  5. Migrate the capability with the best ratio of business value to risk first.
  6. Measure, retire and repeat, deleting old code as soon as each capability has moved.
  7. Revisit the rewrite question with real data about how the system behaves.

Our application modernization service is organized around the same idea, from an engineering health review to a focused rescue sprint or a longer modernization program.

How to Estimate Project Risk

No honest estimate of a modernization project can be made without examining the system. What can be compared up front is how each approach behaves under uncertainty.

Risk profile of three modernization approaches
Risk factorFull rewriteIncremental modernizationTargeted refactoring
Timeline uncertaintyHigh: undocumented behavior is discovered lateMedium: each step is estimated separately, though the whole may take longerLow: the scope is narrow
Business disruptionHigh at cutoverLow to medium: capabilities switch one at a time and can be reversedLow
Regression riskHigh: everything changes at onceMedium: contained to one capability and reduced by parallel runsLow to medium: guarded by characterization tests
Migration complexityHigh: all data moves togetherMedium: data moves per capability, with temporary synchronizationUsually little or no data migration
Operational continuityOld system frozen or maintained in parallelOld and new run side by side throughoutUnchanged
Best suited toDead-end platforms and small systemsLarge, valuable systems that must keep runningSound architectures with specific problem areas

To place your own system, ask:

  • How well is the current behavior understood and documented?
  • How much of it is covered by automated tests?
  • How large and how clean is the data?
  • How many external systems integrate with it?
  • Is there a hard deadline, such as an end-of-support date or a contract renewal?
  • Can each change be rolled back, and how quickly?

The more of these answers are “not well” or “we don’t know”, the stronger the case for small, reversible steps.

Targeted change is often enough. The enterprise reporting case study describes a change focused on one workload: large PDF and Excel generation moved off the request path into a queue-based background architecture. If you are weighing these options for your own system, we can help you assess it before you commit.

Put this into practice

Working on a similar problem?

Tell us about the workflow, system or constraint you are dealing with. We will help you frame the problem before proposing software.

Privacy preferences

Choose how this website measures activity. You can use assessments, inquiries and booking links with optional tracking off.

Security, assessment progress and remembering these preferences remain available.

Our Supabase usage analytics and, when configured, Google Analytics measure public pages, campaigns and confirmed assessment leads. Form text and contact information are excluded.

Allow advertising storage and campaign attribution when advertising measurement tags are configured. This does not enable personalized ads or remarketing.

Read our Privacy Policy