Legacy modernization & strangler fig¶
At a glance
Priority: P0 · Complexity: ⅗ · Est. time: 3 h · Phase: 4 · Prereqs: Architecture styles, DDD strategic, Sagas & outbox You're done when: you can assess a legacy system (value, risk, coupling), choose a modernization strategy (retain, rehost, replatform, refactor, rebuild, replace, retire), design a strangler-fig migration with seams, ACL and data-migration steps, and write the business case and sequencing for it.
Why it matters¶
Most enterprise engineering is modernization, not greenfield: mainframes, 15-year-old Java monoliths, Oracle stored-procedure empires, Delphi thick clients. Staff/Principal architects are hired for this because the failures are expensive: "big-bang rewrites" have a well-documented record of running years over budget and dying before delivering (the second-system effect). The strangler fig — grow the new system around the old, route traffic incrementally, and retire the old piece by piece — is the dominant safe strategy. Fowler's more recent Patterns of Legacy Displacement (with Ian Cartwright, Rob Horn and James Lewis) adds a catalogue of concrete techniques.
AI changes the economics: LLMs accelerate code comprehension, documentation recovery, test generation and mechanical translation (COBOL → Java, Struts → Spring) — but they don't remove the need for characterisation tests, seams and incremental cutover. Treat AI as an accelerator of the analysis and translation steps, with equivalence verified by tests.
Core concepts¶
Why big-bang rewrites fail¶
- The old system keeps changing during the rewrite (moving target); the new one must chase it.
- Hidden behaviour: 15 years of edge cases live in the code, not in specs.
- No value until the end; funding and sponsors evaporate first.
- Second-system effect: over-ambitious redesign.
- Cutover risk concentrated in a single date.
Modernization strategies (the "R's")¶
| Strategy | What | When | Risk/Effort |
|---|---|---|---|
| Retain | Leave as is; wrap or ignore | Stable, low-change, low-risk | Low |
| Retire | Turn off | Unused or duplicate functionality (often 10–30% of a portfolio) | Low; value from discovery |
| Rehost (lift & shift) | Move to cloud/VMs unchanged | Data centre exit deadline | Low; little benefit beyond infra |
| Replatform | Small tweaks (managed DB, containers) | Cheap operational wins | Low–medium |
| Refactor / re-architect | Restructure code, decompose | Core system needing change velocity | Medium–high |
| Rebuild | Rewrite from requirements | Technology dead-end, small scope, well-understood | High |
| Replace | Buy SaaS/COTS | Generic subdomains (HR, finance, CRM) | Medium; integration and data migration |
Decide per subdomain, not per system: core subdomains → refactor/rebuild with DDD; supporting → replatform or replace; generic → replace with SaaS; dead → retire (DDD strategic).
Assessing the legacy system¶
| Question | How to answer |
|---|---|
| What does it do and for whom? | Capability map; interview users; traffic analysis; find dead features (logs, usage analytics) |
| Where is the change pressure? | git history hotspots; backlog by area; cost of delay |
| How risky is it? | Incident history, bus factor, unsupported tech, security findings |
| How coupled is it? | Dependency analysis, shared DB tables, integration inventory, batch job chains |
| Where are the seams? | Interfaces, message boundaries, UI routes, batch handoffs, DB views |
| What data does it own? | Data model, master vs derived data, quality, retention/regulatory needs |
| What's the business case? | Cost of run + cost of delay + risk vs cost of change |
Strangler fig pattern¶
Named by Fowler after the vines that grow around a host tree and eventually replace it. Steps:
- Introduce a facade/routing layer in front of the legacy system (API gateway, reverse proxy, UI shell, message router).
- Identify a thin vertical slice (a capability/route with clear boundaries and value).
- Build the new implementation of the slice; keep the legacy path live.
- Route traffic to the new implementation gradually (by user cohort, tenant, percentage, header, feature flag), compare results (shadowing/parallel run), roll back if needed.
- Retire the legacy code for that slice (delete it!). Repeat.
flowchart LR
U[Clients] --> F[Facade / Router<br/>gateway, feature flags]
F -- "old routes" --> L[(Legacy system)]
F -- "migrated routes" --> N1[New service: Quotes]
F -- "migrated routes" --> N2[New service: Tracking]
N1 -. ACL .-> L
N2 -. events / CDC .-> L
L -- "CDC / events" --> BUS[[Event backbone]]
BUS --> N1
BUS --> N2
style L fill:#eee,stroke:#999
Seam types where you can intercept:
| Seam | Technique | Example |
|---|---|---|
| HTTP/API | Reverse proxy or gateway routes by path/tenant | /api/quotes/* → new service, rest → legacy |
| UI | Micro-frontends, shell app, edge-side includes | New booking page embedded in legacy portal |
| Messaging | Message router/content-based routing; consume legacy events | Legacy publishes to MQ; new service subscribes |
| Database | Views, CDC, "branch by abstraction" at data layer | Debezium streams table changes to new service |
| Batch/file | Replace a batch step with a service; keep file handoffs | New rating engine replaces nightly job, writes same file |
| Code | Branch by abstraction (Paul Hammant): introduce an interface in the monolith, implement new and old behind it, switch by flag | Replace pricing module in-process |
Data migration: the hard part¶
Strangling code is easy compared with strangling data. Options:
| Approach | Description | Trade-offs |
|---|---|---|
| Shared DB (temporary) | New service reads/writes legacy tables | Fast start; couples to legacy schema; forbid long-term (intrusive coupling) |
| Read-through/ACL | New service calls legacy API for data it doesn't own yet | Latency, availability dependency; clean model via ACL |
| CDC replication (legacy → new) | Debezium streams legacy changes to the new store | Legacy remains source of truth during transition; eventual consistency |
| Dual write with reconciliation | Write to both (via outbox) and compare | Complex; requires reconciliation and a clear "source of truth" per phase |
| Cutover per entity/tenant | Move ownership per customer segment; migrate history | Clean but needs routing by entity; good for multi-tenant |
| Event interception + synchronisation | Capture legacy events/transactions, replay to new | Works with mainframes/MQ |
Patterns from Patterns of Legacy Displacement: Transitional Architecture (temporary scaffolding you will throw away — budget for it and plan its removal), Event Interception, Legacy Mimic (new system exposes the legacy's interface to avoid changing consumers), Divert the Flow, Revert to Source, Extract Product Feature, Critical Aggregator, Feature Parity (avoid recreating everything; resist parity for low-value features).
Verification: parallel run/shadow traffic — send production requests to both, compare responses (diff service), fix discrepancies before switching. Golden master tests from recorded production data provide equivalence checks.
Sequencing and business case¶
Choose slices by value × feasibility:
- High business value (change pressure, revenue, pain) and low coupling → first.
- Early slice should be representative but survivable: proves the pipeline (routing, deployment, observability, data sync) end to end.
- Avoid starting with the hub (customer master) — start at the edge and work inward, or extract data-owning core last.
- Fund incrementally: each slice must deliver measurable value (cycle time, cost, capability) to survive budget reviews.
- Track the kill list: legacy components decommissioned; a modernization without deletions is just addition (and cost).
Anti-corruption layer in practice¶
# new-service adapter: translate legacy CUSTOMER_MASTER row (fixed codes) into new domain model
class LegacyCustomerACL:
def __init__(self, legacy_client): self.legacy = legacy_client
def get(self, customer_id: str) -> Customer:
r = self.legacy.fetch_customer(cust_no=customer_id.zfill(10))
return Customer(
id=customer_id,
name=r["CUST_NM"].strip().title(),
payment_terms=PaymentTerms.from_legacy_code(r["PMT_TRM_CD"]),
status=CustomerStatus.ACTIVE if r["ACT_FLG"] == "Y" else CustomerStatus.INACTIVE,
)
AI-assisted modernization¶
| Task | LLM usefulness | Guardrails |
|---|---|---|
| Code comprehension, summaries, dependency maps | High | Verify against runtime traces/tests |
| Extracting business rules from COBOL/PL-SQL | Medium–high | Domain expert review; rules as executable tests |
| Generating characterisation tests | High | Run against the legacy to confirm expected outputs |
| Mechanical translation (language/framework) | Medium–high with tests | Equivalence via golden master/parallel runs |
| Designing target architecture | Low–medium (context is human) | Use as brainstorming partner; you decide |
| Data mapping proposals | Medium | Validate with samples and reconciliation reports |
Agents work best on well-bounded slices with strong tests: slice by seam, generate tests from legacy behaviour, generate the new implementation, verify equivalence. Track "AI-translated LOC verified by tests" not raw LOC.
Senior-level nuance¶
- Organisation matters more than code: align the new-slice team to the target bounded context (Inverse Conway) and give them ownership of both the new service and the legacy slice being retired.
- Prevent the legacy from growing: freeze new features in the legacy area, or new features go into the new system only ("stop digging").
- Transitional architecture is a cost you must plan to delete — track it explicitly.
- Don't over-modernize: retaining a stable, low-change component is a valid decision; "modern" isn't a business goal.
- Compliance & audit may require data lineage and reconciliation reports at each cutover.
- Measure: % traffic on new paths, lead time for changes in migrated areas, incident rate, run cost, legacy LOC/modules deleted.
Resources¶
| Resource | Type | Why this one | Level | Cost |
|---|---|---|---|---|
| StranglerFigApplication (Fowler) | article | Origin of the metaphor; short and clear | intermediate | free |
| Patterns of Legacy Displacement (Fowler et al.) |
article | Catalogue of practical patterns (Transitional Architecture, Event Interception, Legacy Mimic...) | advanced | free |
| Architecture Modernization (Nick Tune & Jean-Georges Perrin) |
book | DDD + sociotechnical view of modernization; how to choose targets and design teams | advanced | paid |
| How to break a Monolith into Microservices (Fowler/Newman) | article | Sequencing and slicing guidance from Sam Newman | intermediate | free |
| Monolith to Microservices (Sam Newman) | book | Patterns: strangler, branch by abstraction, parallel run, data decomposition | intermediate | paid |
| Azure Architecture Center — Strangler Fig | docs | Concise issues/considerations, when not to use | intermediate | free |
| AWS Prescriptive Guidance — Strangler fig | docs | Cloud implementation guidance with sequence diagrams | intermediate | free |
| microservices.io — Strangler application | docs | Forces and refactoring pattern context | intermediate | free |
| Debezium docs | docs | CDC toolkit for streaming legacy DB changes to new systems | advanced | free |
| Understand Legacy Code |
article | Practical techniques to gain safe footing in legacy code | intermediate | free |
Hands-on lab¶
Goal: perform a mini strangler migration. 2 h.
- Take (or create) a small legacy app: Flask/Spring monolith with
/quotes,/bookings,/customersendpoints and one SQLite/Postgres DB. - Put nginx/Traefik or a FastAPI gateway in front; route everything to legacy (baseline).
- Record 500 real-ish requests/responses (golden master) for
/quotes. - Build a new
quotesservice (clean domain, hexagonal), initially calling legacy pricing via an ACL. - Shadow traffic: gateway sends copies to the new service; a diff script reports mismatches; fix until < 0.5% diff (document legitimate differences).
- Canary: route 5% → 25% → 100% by header/cookie; add a rollback flag.
- Data: stream legacy quote-rule tables to the new service via CDC (Debezium) or a polling sync; verify with reconciliation report.
- Delete the legacy
/quotescode; record the deletion in a "kill list". - AI extension: use an LLM to summarise the legacy pricing code into candidate business rules; convert three rules into failing tests and verify against legacy behaviour.
Expected output: gateway config, diff report, canary rollout log, reconciliation report, and the deleted legacy code commit.
Questions¶
L1 — Recall¶
Q1. Describe the strangler fig pattern in five steps.
Answer
- Put a facade/router in front of the legacy system. 2. Pick a thin vertical slice. 3. Build the new implementation while legacy still serves. 4. Gradually route traffic to the new slice with the ability to roll back (canary, flags). 5. Decommission the legacy slice; repeat until the legacy can be turned off.
Q2. What is branch by abstraction?
Answer
A technique for replacing a component inside a codebase without long-lived branches: introduce an abstraction (interface) over the existing implementation, route callers through it, build the new implementation behind the same abstraction, switch via configuration/flag, then remove the old implementation and possibly the abstraction. Enables continuous integration during large changes.
Q3. Name the 'R' strategies for application portfolio modernization.
Answer
Retain, retire, rehost (lift and shift), replatform, refactor/re-architect, rebuild, replace (repurchase). Decide per subdomain/application considering business value, technical health and cost.
Q4. What is a transitional architecture and why must you plan to remove it?
Answer
Temporary components built to bridge old and new systems during migration (sync jobs, adapters, facades, dual writes). They have real cost, add failure modes and become permanent if not tracked; hence budget for them, define exit criteria and treat their removal as part of the migration plan.
L2 — Apply¶
Q5. Plan the migration of a monolith's Quoting capability to a new service. What's the sequence?
Answer
(1) Characterise: record production request/response pairs; identify data used (rate tables, customer contracts) and callers. (2) Establish routing: gateway/facade. (3) Create seams: within the monolith, introduce a QuoteService interface (branch by abstraction) if callers use in-process calls. (4) Build the new service with its own domain model, ACL to legacy for data not yet migrated. (5) Sync reference data via CDC/events. (6) Shadow and diff results; fix discrepancies. (7) Canary by customer segment; monitor business metrics (conversion, quote latency). (8) Migrate write ownership of rate tables; remove legacy code and dead tables. Track and delete transitional pieces.
Q6. Two systems (legacy and new) need the same customer data during a 12-month transition. Choose a data strategy.
Answer
Keep legacy as source of truth initially; stream changes via CDC to the new service's store (read model); the new service uses its local copy for reads and sends writes to legacy through an API/ACL until ownership flips for the customer domain. At flip: freeze writes briefly (or use cutover per tenant), do a final sync and reconciliation, make the new service the source of truth, and reverse the sync direction (new → legacy) for consumers still on legacy until they migrate. Avoid dual writes without clear ownership; add reconciliation reports at each phase.
Q7. How do you verify functional equivalence for a rewritten pricing engine?
Answer
Golden master from recorded production inputs (with privacy scrubbing) run through both; property-based tests on invariants; shadow production traffic with a diff service classifying differences (bug, legitimate change, tolerance e.g. rounding); domain-expert review of samples in discrepancy classes; gradual rollout with business KPI monitors. For high-risk domains, parallel-run through a full billing cycle.
L3 — Design & trade-offs¶
Q8. Rebuild vs refactor for a 20-year-old core booking system with 60% of features rarely used. Decide.
Answer
Neither wholesale. First retire: analyse usage to find the dead 30–60% (biggest ROI). For the live core, refactor/strangle by subdomain: extract the volatile, high-value capabilities (pricing, tracking) with DDD boundaries; leave stable low-change parts (Retain) behind an ACL. Rebuild only small, well-understood, high-pain modules or where tech is a dead end (unsupported runtime). Justification: rewrite risk, moving target, hidden rules. Plan an explicit feature parity policy — don't replicate low-value behaviour — and get business agreement to drop features.
Q9. Mainframe COBOL system processes nightly batches and has no APIs. Modernization options?
Answer
Options: (1) Wrap: expose CICS/transaction or MQ interfaces via an integration layer/API gateway (Legacy Mimic) to allow new consumers; (2) Event interception: capture the mainframe's outputs/DB2 changes (CDC) to feed new services; (3) Replace batch steps incrementally with services writing the same output files; (4) Automated code translation (COBOL→Java) as a rehost-like step to escape licences — high risk of "COBOL in Java"; (5) Rebuild by domain with strangler. Recommended combination: wrap + CDC to enable new capabilities immediately, then displace high-change batch steps first, leaving stable ledger-like batches last. Use LLMs to extract rules and generate tests, verified against production outputs.
Q10. Shared-database shortcut: the new service will read/write the legacy tables directly 'for now'. Accept?
Answer
Only as a time-boxed transitional step with explicit exit criteria: it creates intrusive coupling and locks the legacy schema; the new service inherits legacy constraints and any schema change breaks both. If accepted: read-only access via database views (a defined interface), no writes, a date and owner for removal, tests that detect drift, and a plan to move data ownership (CDC/ACL). Prefer an ACL/API or CDC-based read model if feasible — the "temporary" shared DB tends to be permanent. Record as an ADR with review trigger.
L4 — Staff-level ambiguity¶
Q11. The CEO has approved a 'two-year platform rewrite' with a big-bang cutover. You believe it will fail. How do you respond?
Answer
Engage constructively: identify what the CEO wants (speed, cost, capability) and show how an incremental approach achieves it sooner with less risk. Bring evidence: industry failure rates of big-bang rewrites, your own system's hidden-behaviour complexity (present an inventory of undocumented rules discovered through a 2-week analysis spike), and a concrete alternative: strangler roadmap with the first slice delivering value in 3 months, funding tied to milestones and a kill list. Offer risk-based governance: quarterly go/no-go gates, measurable outcomes (traffic migrated, legacy deleted, lead time improved). Propose a compromise if needed: rebuild only bounded, well-understood modules; keep a facade so incremental cutover remains possible. Escalate risks in writing (ADR) and secure sponsorship from delivery leadership.
Q12. After 18 months of strangling, 40% of traffic is on new services, but the legacy still needs a full team and costs are up. How do you get back on track?
Answer
Diagnose why deletions lag: migrated slices leave legacy code and data behind (no decommission discipline), transitional architecture accumulates, features keep being added to legacy, the data-owning core is untouched. Actions: introduce a kill list with owners and dates (each migration story includes deletion and infra shutdown), freeze feature work in legacy (new features only in new system), prioritise slices that unlock decommissioning of expensive components (licences, mainframe MIPS) rather than only user-visible ones, reassess sequencing towards the hub data domain, and report cost metrics (run cost and legacy team size) alongside traffic percentages. Reset expectations with leadership using a transparent burn-down of legacy capabilities.
Real-world use cases¶
- Banks displacing mainframe cores with CDC/event interception and per-product cutover.
- Retailers strangling monolithic e-commerce platforms by routing checkout, search and catalogue to new services.
- Logistics replacing nightly batch rating engines and EDI gateways with event-driven services while keeping partner interfaces (Legacy Mimic).
- Government modernising benefits systems slice by slice with parallel runs for compliance.
- AI-assisted rewrites of framework upgrades (e.g. Struts/JSF → Spring/React) with characterisation tests and agent-driven mechanical translation.
Pitfalls & anti-patterns¶
- Big-bang rewrite with a single cutover date.
- Replicating every legacy feature (feature-parity trap).
- Building the new system on the legacy database schema forever.
- No routing facade, so no way to roll back.
- Never deleting legacy code; transitional architecture becoming permanent.
- Starting with the hardest hub domain (customer master) or the most trivial one (proves nothing).
- Treating modernization as a technology project instead of an organisational one (teams, ownership, incentives).
Checklist¶
- I can explain strangler fig, branch by abstraction and the R strategies without notes
- I ran a shadow/canary migration of one slice with a diff report
- I can design data migration options (CDC, ACL, per-tenant cutover) and their trade-offs
- I can build a business case and sequencing for a modernization
- I answered all L3 questions out loud in < 3 min each