Skip to content

Strangler Fig Pattern

The strangler fig pattern is the dominant migration strategy for legacy modernization. Named after strangler fig trees that grow around a host tree — eventually replacing it entirely while the host continues to function — the pattern lets you replace system components incrementally while both old and new systems run in parallel.

ModernizeSpec’s extraction-plan.json and migration-state.json are designed around this pattern: they track what has been extracted, what is in progress, and what remains.

┌─────────────────────────────────────────────────────┐
│ Router │
│ (feature flag, URL path, API version, etc.) │
└───────────────┬─────────────────┬───────────────────┘
│ │
┌───────▼───────┐ ┌──────▼───────┐
│ New System │ │ Legacy System │
│ (extracted) │ │ (remaining) │
│ │ │ │
│ Module A ✓ │ │ Module A ✗ │
│ Module B ✓ │ │ Module B ✗ │
│ │ │ Module C │
│ │ │ Module D │
└───────────────┘ └──────────────┘

Traffic for extracted modules routes to the new system. Everything else continues through the legacy system. Over time, more modules move to the new side until the legacy system handles nothing.

BenefitExplanation
Continuous deliveryEach extraction is independently deployable
ReversibleIf the new module fails, route traffic back to legacy
Incremental confidenceEach module builds on proven infrastructure
Business continuityThe system never goes down for migration
Team parallelismMultiple teams can extract different modules simultaneously

Every component in the legacy system progresses through a defined lifecycle. ModernizeSpec’s migration-state.json tracks this progression:

StateDescriptionEntry CriteriaExit Criteria
Not StartedLegacy code untouchedDefault stateAnalysis begins
In AnalysisUnderstanding behavior, mapping dependenciesTeam assignedParity tests written
ExtractingBuilding the new implementationParity tests existNew code compiles and passes unit tests
TestingRunning parity tests against new implementationNew code existsParity confidence > threshold
ShadowingNew system processes real traffic, results discardedParity tests passShadow results match legacy for N days
LiveNew system handles production trafficShadow validation passesMonitoring confirms stability
Legacy RemovedOld code deleted, migration completeNew system stable for N weeksOld code removed from codebase
Not Started ──▶ In Analysis ──▶ Extracting ──▶ Testing
Legacy Removed ◀── Live ◀── Shadowing ◀──────────┘

Every transition should be recorded with a timestamp, the actor (human or agent), and any notes. This creates an audit trail of the migration.

The anti-corruption layer (ACL) is the boundary between old and new systems. It translates data formats, protocols, and domain concepts so that legacy abstractions do not leak into the new system.

The ACL exists for one reason: the new system should be designed as if the legacy system does not exist. Domain models, naming conventions, and data structures in the new system should reflect the ideal design, not the legacy layout.

The ACL handles the translation between ideal and legacy at the boundary.

The ACL is temporary. As modules move from legacy to new:

  1. Phase 1: ACL translates between new module and legacy system
  2. Phase 2: ACL shrinks as more modules migrate (fewer translations needed)
  3. Phase 3: ACL removed entirely when legacy system is decommissioned

Design the ACL for easy removal — thin adapter layers, not deep abstractions.

During the transition period, both systems may need access to the same data. Three strategies handle this:

Write New, Sync to Legacy

New system is the primary writer. A sync mechanism propagates changes to the legacy database so legacy UI and reports continue to work.

Best when: New system launches first for a specific capability.

Write Legacy, Event to New

Legacy system continues as primary writer. Events (database triggers, CDC, application events) feed changes to the new system’s data store.

Best when: Legacy system cannot be modified to call new APIs.

Shadow Mode

Both systems process the same request. Legacy result is returned to the user. New system result is logged for comparison. No user impact.

Best when: Building confidence before cutover.

RiskMitigation
Data divergenceReconciliation jobs that detect and alert on mismatches
Ordering problemsSequence numbers or timestamps on all writes
Partial failuresOutbox pattern — persist intent, then sync asynchronously
Performance overheadShadow mode adds latency; budget for it or make it async

A seam (from Michael Feathers’ “Working Effectively with Legacy Code”) is a place where you can alter program behavior without editing the code at that point. Seams are the insertion points for the strangler fig — where you can intercept requests and route them.

Seam TypeWhere to Find ItExample
API endpointHTTP routes, RPC definitionsRoute /api/invoice to new service
Message queueEvent bus, pub/sub topicsSubscribe new consumer to invoice.created topic
Database viewViews that abstract table accessReplace view to read from new tables
Configuration switchFeature flags, environment variablesUSE_NEW_TAX_ENGINE=true
Interface/protocolDependency injection pointsSwap LegacyTaxCalculator for NewTaxCalculator
PreprocessingMiddleware, interceptors, filtersInsert translation layer before request reaches legacy handler
  1. Map all entry points — HTTP routes, CLI commands, scheduled jobs, event handlers
  2. Identify the narrowest point where a request crosses a module boundary
  3. Verify the seam is clean — no side effects from the interception itself
  4. Test the seam — route one request through and verify both paths work

Feature flags control which implementation handles each request. They enable gradual rollout and instant rollback.

PhaseFlag StateTraffic
Developmentoff0% to new system
Internal testinginternal-onlyEmployees only
Canarypercentage:55% of production traffic
Gradualpercentage:255075Increasing production traffic
Full rollouton100% to new system
CleanupFlag removedLegacy code removed

Flags are temporary. Every flag should have:

  • A creation date
  • An expected removal date
  • An owner responsible for cleanup
  • A maximum lifetime (typically 30-90 days after full rollout)

Stale flags accumulate as technical debt. Track them in migration-state.json alongside component states.

The order in which you extract modules matters. Dependencies between modules create constraints.

  1. Foundation first — shared utilities, core data models, configuration
  2. Leaf modules second — modules with no dependents (nothing else imports them)
  3. High-value modules early — maximize business value delivered
  4. High-coupling modules last — they require the most ACL work
Extract in this order:
Phase 1: Foundation
├── Currency (0 dependencies)
├── Mode of Payment (0 dependencies)
└── UOM (Unit of Measure, 0 dependencies)
Phase 2: Core Calculations
├── Tax Calculator (depends on: Tax Rule ✓ from Phase 1)
└── Pricing Rule Engine (depends on: Currency ✓ from Phase 1)
Phase 3: Transaction Engine
├── GL Entry Engine (depends on: Tax Calculator ✓, Currency ✓)
└── Payment Allocation (depends on: GL Entry ✓)
Phase 4: Business Documents
└── Sales Invoice (depends on: GL Entry ✓, Tax ✓, Payment ✓)

Each phase depends only on components extracted in prior phases. This maps directly to extraction-plan.json’s phases[] with dependencies[].

The reference implementation validated this sequence: Mode of Payment (19 tests, zero dependencies) → Tax Calculator (24 tests, depends on tax rules) → GL Entry Engine (32 tests, depends on both). Each extraction was independently testable and deployable.

The strangler fig pattern maps directly to two ModernizeSpec files:

Pattern ConceptSpec FileSpec Field
Extraction phasesextraction-plan.jsonphases[]
Phase dependenciesextraction-plan.jsonphases[].dependencies[]
Risk scoringextraction-plan.jsonphases[].risk
Component statesmigration-state.jsoncomponents[].state
State transitionsmigration-state.jsoncomponents[].history[]
Feature flag statusmigration-state.jsoncomponents[].featureFlag
Overall progressmigration-state.jsonsummary.percentComplete