in

Legacy Software Modernization: Rewrite or Strangler Fig?

'Let's rewrite the system from scratch' is the most expensive and most regretted decision in enterprise software. We compare five modernization approaches on risk, time and cost, then walk through the strangler fig pattern that makes a no-downtime transition possible, together with the hardest part: the data-layer strategy.

Strangler fig diagram showing capabilities moved step by step to new modules through a gateway in front of the old system

A fifteen-year-old system, an application server that's no longer supported, a single developer who knows the source code and a deployment process where something breaks on every release. At this point the sentence that comes to the table is always the same: "Let's rewrite it from scratch." It is the most expensive and most regretted decision in enterprise software; two years later there are two systems, a half-finished transition and hidden business rules still running in the old one.

This article starts with the right diagnosis — what is the system's real problem? — then compares five modernization approaches on risk, time and cost; explains why a rewrite is usually wrong, walks through the strangler fig pattern that makes a no-downtime transition possible, covers the hardest part, the data-layer strategy, and lists the mistakes we see in the field.

Diagnosis first: what are you trying to solve?

Symptom

The real problem is usually

Right first step

A small change takes weeks

No tests, manual deployment, spaghetti code

Test and deployment automation; code unchanged

Platform support has ended (JDK 8, old app server)

Security and compliance risk

Rehost/replatform: same code, new infrastructure

Only one person knows it

Knowledge risk, no documentation

Characterization tests and architecture records

New features can't be added, the business waits

The architecture can't carry change

Strangler fig: new features in new modules

High license and operations cost

Dependency on commercial infrastructure

Replatform or package replacement

"Legacy" is not an age but a condition: a system that is risky and expensive to change. A method chosen without a diagnosis gives the right answer to the wrong problem.

Five approaches

Approach

What's done

Risk

Duration

When

Rehost / replatform

Same code; containers, current JDK, managed database

Low

Weeks–months

When platform risk is the main problem; the first step of every path

Refactor in place

Tests added, modules separated, code cleaned

Low–medium

Continuous

When the codebase is salvageable

Strangler fig

Capabilities moved one by one to the new system; the old one shrinks and retires

Medium, low per step

6–24 months, value every month

The default path for large, live, critical systems

Rewrite (big bang)

New system from scratch; one-day cutover

High

12–36 months, value at the end

Small system, dead platform, clear specification

Package / SaaS replacement

An off-the-shelf product instead of the custom system

Medium

6–18 months

Standard process, no differentiation (see custom software vs off-the-shelf)

Why a rewrite is usually wrong

  • The feature-parity trap: The old system holds thousands of business rules accumulated over fifteen years, most of them unwritten. The new system that "does the same" turns out incomplete on go-live day.

  • Value comes at the end: For two years the business gets nothing; meanwhile every feature added to the old system must be added to the new one — two targets move at once.

  • The second-system effect: The ambition to "do everything right this time" makes the new system more complex than the first.

  • The one-day cutover: Hard to reverse, the highest-risk moment; data migration, training and integrations on the same night.

When a rewrite is right: the system is small (a few months), the platform is truly dead (no source code, can't be run) or the business process is changing anyway and the old behavior won't be preserved.

Strangler fig: step by step

The pattern is named after the fig species that grows around a host tree and eventually envelops it: the new system grows around the old one, takes over capabilities one by one, and the old system eventually empties out. It is the only path that is no-downtime, reversible and value-producing at every step.

Step 0: The old system                    client ──► [old system] ──► [old DB]

Step 1: A gateway in front                client ──► [gateway] ──► [old system] ──► [old DB]

Step 2: The first capability moves        client ──► [gateway] ─┬─► [old system] ──► [old DB]
                                                                 └─► [new: pricing] ──► (old DB / its own DB)

Step N: The old system empties            client ──► [gateway] ──► [new modules] ──► [new DB]
  1. Put a gateway in front. All traffic passes through an API gateway or reverse proxy; the routing rule is by path, tenant or feature flag. This step doesn't change the old system and is reversible on its own.

  2. Write characterization tests. Record the old system's real inputs and outputs (golden records): "for these 500 orders, pricing gives these results". The new module must pass the same tests. Where there is no specification, the behavior itself is the specification.

  3. Pick the seam. The first capability to move: high business value, low coupling, clear boundary (pricing, notifications, reporting, a new channel). Don't start with the hardest module; the first cycle is for the team to learn the method.

  4. Build the new module and route to it. First in shadow mode (both systems run, results are compared, only the old answer is returned), then gradual traffic (5% → 50% → 100%), rollback with a single flag.

  5. Retire the old. The moved capability's code is deleted from the old system; code that isn't deleted isn't a "backup", it's double maintenance.

  6. Repeat. Each cycle 4–8 weeks; at the end of every cycle, a new piece running in production.

# Spring Cloud Gateway: pricing to the new module, everything else to the old system
spring:
  cloud:
    gateway:
      routes:
        - id: pricing-new
          uri: http://pricing-service
          predicates:
            - Path=/api/price/**
            - Header=X-Canary, true          # internal users first; everyone once the flag is removed
        - id: legacy
          uri: http://legacy-app
          predicates:
            - Path=/**
// Characterization test: the old system's recorded results are the new module's contract
@ParameterizedTest
@MethodSource("goldenRecords")                                   // 500 real orders + the prices the old system produced
void newPricingMatchesTheOldSystem(OrderSample sample) {
    PriceResult result = pricing.calculate(sample.input());
    assertThat(result).usingRecursiveComparison().isEqualTo(sample.legacyResult());
}

The data layer: the hardest part

Moving code is easy; separating data is hard, and most strangler projects get stuck here. A three-stage path:

  1. Shared database (transitional): The new module reads and writes the old tables. A fast start, but the schema stays tied to the old system. Rule: the new module reaches the old schema through its own anti-corruption layer; it doesn't bind its entities directly to the old tables.

  2. Ownership transfer: The new module becomes the only writer of the moved capability's tables; the old system only reads (or goes through a view). The "one owner per table" principle.

  3. Separate database and synchronization: The new module moves to its own schema; data the old system still needs is projected back through change data capture (CDC, for example Debezium) or events. Dual writes (writing to two databases at once) create inconsistency; avoid them.

Data migration happens per capability and rehearsed, not as a big bang; every moved capability takes its own data with it.

Governance: how to measure, when to stop

  • Metrics: Percentage of traffic handled by the new system, change lead time (idea to production), number of production incidents, lines of code left in the old system. A dashboard, not a slide deck.

  • Time-box and exit criteria: A date and a definition of "done" for every cycle; a program that misses its target two cycles in a row is replanned, not quietly extended.

  • A change policy for the old system: New features only in new modules; the old system gets bug fixes and regulatory changes only. Without this rule the target keeps moving.

  • Business involvement: The business owner is part of acceptance testing at every seam; hidden rules surface most in those sessions.

Common mistakes in the field

  1. Modernizing everything. A module that doesn't change and works (the annual payroll calculation) can stay as it is; what needs modernizing are the parts the business wants changed.

  2. Moving without tests. Without characterization tests, "same behavior" can't be verified; bugs are learned from customers in production.

  3. Starting without a gateway. If clients connect directly to the old system, there is no routing; every transition requires a client deployment.

  4. Leaving data for last. "Let's move the code, we'll look at the data later" — later never comes, and the shared database becomes permanent.

  5. Not deleting old code. Both systems keep being maintained; the strangler's purpose is for the old one to shrink.

Frequently asked questions

Is strangler fig compatible with a modular monolith?

Yes, and it is often the best target: new capabilities are collected as well-bounded modules in one new application instead of separate services; modernization without distributed-system complexity. The boundaries can be turned into services later.

What if the old system's source code is gone?

The gateway and characterization tests are still possible: behavior is recorded from the outside (API, screens, database outputs). Without code, refactoring in place is off the table; strangler fig or package replacement remain.

How long does it take?

12–24 months for a mid-size enterprise system; but the first production value arrives in the first 6–8 weeks and the program can be stopped at any point — whatever has been moved stays in production. A rewrite has neither property.

Conclusion

Modernization is a program, not a project: the right diagnosis, a replatform that closes platform risk first, then strangler fig cycles backed by a gateway and characterization tests, data ownership transferred per capability and old code actually deleted. The rewrite is the exception; the no-downtime, reversible transition is the rule.

For a modernization diagnosis of your existing system and a strangler fig roadmap, see our software design and architecture and maintenance and support services. The discovery call is free.