Back to Scenarios & Design Questions
Q37Question 37 of 67

How would you replace a legacy service without a big-bang migration?

Key Concepts
  • A big-bang rewrite concentrates all risk into one switch-over moment with no gradual learning and often no safe way back.
  • The strangler-fig pattern places a routing facade in front of the legacy service and moves one slice at a time.
  • Shadow traffic sends real requests to both services, returns the legacy response, and compares the new response without customer impact.
  • Parallel validation flags any behavioral divergence before the new service is ever authoritative.
  • Cut over by percentage (1%, 10%, 50%, 100%) with a compatibility layer so callers do not change in lockstep.
  • Every increment must include a designed, fast, rehearsed return path to legacy (feature flag or routing rule).
  • The legacy must remain functional with current data during migration so flipping back never loses data.

Definition

Put a router in front of the legacy service and move one capability or slice at a time to the new one -- validating in parallel and keeping an instant path back -- until the legacy is strangled and retired.
  • Strangler-Fig Pattern – a facade routes traffic; initially all goes to legacy, then progressively more goes to the new service until legacy handles nothing and can be retired.
  • Shadow Traffic – the facade sends real requests to both, returns the legacy response, and discards the new one after comparing.
  • Parallel Validation – compares responses from both services and flags divergence before the new service is authoritative.
  • Compatibility Layer – translates between old and new contracts so callers do not change in lockstep.
  • Percentage Ramp – cut over by percentage (1%, 10%, 50%, 100%) watching metrics at each step.
  • Return Path – a designed, fast, rehearsed way back to legacy for every increment (feature flag, routing rule).

National Bank of India wants to replace its decades-old transaction system but cannot switch everything overnight. So they put a receptionist (router) in front. At first every request goes to the old system. Then they quietly send just balance enquiries to the new system while everything else stays on the old one, double-checking answers. If it works, they move deposits, then transfers, one capability at a time, keeping the old system ready to take over instantly if the new one gives a wrong answer.

Understanding the Concepts

Why is a big-bang rewrite dangerous?

All risk is concentrated into a single moment with no gradual learning and often no safe way back.

JAVA
package com.bank.migration;

public class BigBangRisk {
    public static void main(String[] args) {
        System.out.println("Big-bang rewrite:");
        System.out.println("  Build new system for 6 months");
        System.out.println("  Switch everything on one Saturday night");
        System.out.println("  If something is wrong → no safe way back");
        System.out.println("  All risk in one moment, no gradual learning");
        System.out.println();
        System.out.println("Strangler-fig:");
        System.out.println("  Move one slice at a time");
        System.out.println("  Validate each slice in parallel");
        System.out.println("  Instant rollback for each increment");
        System.out.println("  Risk spread across many small, reversible steps");
    }
}
DIAGRAM
Big-bang rewrite:
  Build new system for 6 months
  Switch everything on one Saturday night
  If something is wrong → no safe way back
  All risk in one moment, no gradual learning

Strangler-fig:
  Move one slice at a time
  Validate each slice in parallel
  Instant rollback for each increment
  Risk spread across many small, reversible steps

How does the strangler-fig facade work?

A routing facade sits in front. Initially everything goes to legacy. You progressively route slices to the new service.

JAVA
package com.bank.migration;

public class StranglerFacade {

    private final boolean balanceEnabled = true;  // feature flag
    private final boolean transferEnabled = false; // not migrated yet

    String route(String capability, String request) {
        if (capability.equals("balance") && balanceEnabled) {
            return "NEW service: " + request;
        }
        return "LEGACY service: " + request;
    }

    public static void main(String[] args) {
        StranglerFacade facade = new StranglerFacade();
        System.out.println(facade.route("balance", "SAV2001"));
        System.out.println(facade.route("transfer", "SAV2001 → SAV3002"));
    }
}
DIAGRAM
NEW service: SAV2001
LEGACY service: SAV2001 → SAV3002

How does shadow traffic with parallel validation work?

Send real requests to both, return the legacy response, and compare the new response. Any divergence is logged for investigation.

JAVA
package com.bank.migration;

public class ShadowTraffic {

    static String legacyBalance(String accountId) {
        return "75000.00";
    }

    static String newBalance(String accountId) {
        return "75000.00"; // should match; log if different
    }

    public static void main(String[] args) {
        String accountId = "SAV2001";
        String legacyResp = legacyBalance(accountId);
        String newResp = newBalance(accountId);

        if (!legacyResp.equals(newResp)) {
            System.out.println("DIVERGENCE on " + accountId
                    + ": legacy=" + legacyResp + ", new=" + newResp);
        } else {
            System.out.println("Match on " + accountId + ": " + legacyResp);
        }
        System.out.println("Caller receives LEGACY response (safe). New response discarded.");
    }
}
TEXT
Match on SAV2001: 75000.00
Caller receives LEGACY response (safe). New response discarded.

How do you cut over by percentage?

Ramp gradually: 1%, 10%, 50%, 100%, watching metrics at each step.

JAVA
package com.bank.migration;

import java.util.concurrent.ThreadLocalRandom;

public class PercentageRamp {

    static String route(String request, int newServicePercent) {
        int roll = ThreadLocalRandom.current().nextInt(100);
        if (roll < newServicePercent) {
            return "NEW service: " + request;
        }
        return "LEGACY service: " + request;
    }

    public static void main(String[] args) {
        int percent = 10; // 10% to new service
        for (int i = 0; i < 5; i++) {
            System.out.println(route("balance SAV2001", percent));
        }
        System.out.println("At " + percent + "% ramp, watch error rate and latency");
    }
}
TEXT
LEGACY service: balance SAV2001
LEGACY service: balance SAV2001
NEW service: balance SAV2001
LEGACY service: balance SAV2001
LEGACY service: balance SAV2001
At 10% ramp, watch error rate and latency

Why must every increment include a return path?

Incremental migration is only safe if each step is reversible. A feature flag or routing rule must be flippable instantly.

JAVA
package com.bank.migration;

public class ReturnPath {
    public static void main(String[] args) {
        System.out.println("Before cutover: test the rollback");
        System.out.println("  1. Route balance to NEW (feature flag ON)");
        System.out.println("  2. Observe for 1 hour");
        System.out.println("  3. Bug found! Flip feature flag OFF");
        System.out.println("  4. Balance routes back to LEGACY instantly");
        System.out.println("  5. No data lost because both read from shared store");
        System.out.println();
        System.out.println("If there is no designed return path, it is a big-bang in disguise.");
    }
}
TEXT
Before cutover: test the rollback
  1. Route balance to NEW (feature flag ON)
  2. Observe for 1 hour
  3. Bug found! Flip feature flag OFF
  4. Balance routes back to LEGACY instantly
  5. No data lost because both read from shared store

If there is no designed return path, it is a big-bang in disguise.

How do you keep legacy data current during migration?

During migration, writes often must go to a shared source of truth or be dual-written, so flipping back never loses data.

JAVA
package com.bank.migration;

public class SharedSourceOfTruth {
    public static void main(String[] args) {
        System.out.println("Option A: Shared database (both old and new read/write same DB)");
        System.out.println("  Rollback: flip route, data is always current");
        System.out.println();
        System.out.println("Option B: Dual-write (write to both old and new stores)");
        System.out.println("  Rollback: stop writing to new, old store is current");
        System.out.println();
        System.out.println("The guarantee: flipping back NEVER loses data");
    }
}
TEXT
Option A: Shared database (both old and new read/write same DB)
  Rollback: flip route, data is always current

Option B: Dual-write (write to both old and new stores)
  Rollback: stop writing to new, old store is current

The guarantee: flipping back NEVER loses data

What are common mistakes?

Discovering mid-incident that there is no safe way back, which is exactly the failure the strangler-fig exists to prevent.

JAVA
package com.bank.migration;

// WRONG: migrate to new service, decommission legacy database
// Bug found → no way back, data only in new store

// CORRECT: keep legacy fully operational until migration is COMPLETE
// Test rollback BEFORE cutover, not during an incident

What are common interview follow-up questions?

  • How do you handle writes during the migration window?
  • What if the legacy service's API is undocumented?
  • How do you decide the order of capabilities to migrate?
  • When is it safe to decommission the legacy service?

Visual Overview

DIAGRAM
  Strangler-Fig Migration
  ───────────────────────

  Phase 1: Shadow
  Client ──► Facade ──► Legacy (response returned)
                   └──► New (response compared, discarded)

  Phase 2: Ramp by percentage
  Client ──► Facade ──► Legacy (90%)
                   └──► New (10%, observed)

  Phase 3: Full cutover
  Client ──► Facade ──► New (100%)
                        Legacy (standby, ready for rollback)

  Phase 4: Retire
  Client ──► Facade ──► New (100%)
                        Legacy decommissioned

  At EVERY phase: flip feature flag → instant rollback to Legacy

Key Takeaway

Never replace a legacy service with a big-bang rewrite. The strangler-fig pattern spreads the risk across many small, reversible steps. A routing facade sits in front of legacy, and you move one slice at a time: shadow traffic first to compare, then percentage ramp to build confidence, then full cutover.

Before trusting the new service, de-risk with shadow traffic and parallel validation. Send real requests to both, return the legacy response, and flag any divergence. Only when divergence is zero do you cut over, usually ramping by percentage with a compatibility layer so callers do not change in lockstep.

Every increment must include a designed, fast, rehearsed return path. A feature flag or routing rule must be flippable instantly, and the legacy must remain functional with current data so flipping back never loses data. Test the rollback before the cutover, not during an incident.

Remember one sentence: a legacy replacement succeeds when every increment is small, validated in parallel, and individually reversible, so the organization can always walk back the moment the new path misbehaves.

---

Share
Use ← → arrow keys to navigate