Legacy Enterprise Modernization
Purpose
Change a system that is in production, poorly understood, and still earning revenue, without
a rewrite and without a stop-the-world release. The failure mode this exists to prevent is
the parallel rewrite: a new system built beside the old one, competing against a target that
keeps moving, funded until the first budget review, and eventually abandoned with both
systems in production.
The second failure is subtler and more common: a modernisation programme that has run for a
year, produced several new services, and decommissioned nothing — so the organisation now
operates two architectures and pays for both.
Principles
Avoid a parallel rewrite while the original changes unless a bounded scope,
compatibility strategy and funded cutover make drift controllable.
Keep steps bounded with an observable benefit or prerequisite they unlock.
Fund enabling work explicitly rather than assuming immediate revenue.
Track decommissioning alongside business and risk-reduction outcomes.
New code alone does not prove reduced coexistence cost.
Understand before changing; pin before understanding fully.
Characterisation tests capture behaviour you have not yet
explained, which is most of it.
The legacy model must not infect the new one.
That is the entire purpose of an anti-corruption layer.
Workflow
- Map what exists, from evidence: which tables are written by what, which endpoints are
actually called, where rules live (code, procedures, triggers, jobs, spreadsheets).
Reconcile production evidence, documentation and operator knowledge: each has blind spots.
- Pin behaviour with characterisation tests at the boundary you will preserve — usually
the HTTP API or the batch output — before touching anything.
- Pick the first slice by value and by risk, not by architecture: something that changes
often (so the pain is real), is reasonably self-contained, and whose failure is survivable.
- Establish data ownership before transferring writes. Shared writers constrain
independent semantic changes; compatible additive work and read extraction can proceed.
- Strangle: route the slice's traffic to new code, keep the old path available, and
remove it once the new one has proven itself.
- Decommission explicitly, as a scheduled deliverable with a date. Otherwise both
systems live forever.
Decision rules
The system still changes regularly and earns money
→ prefer incremental replacement. A rewrite carries moving-target,
parity and cutover risk; use it only with evidence those costs are bounded.
The system is frozen, small, and thoroughly understood
→ a rewrite may be genuinely cheaper. This is rare; verify
"frozen" against a representative change window and operational roadmap.
Several applications write the same tables
→ establish write authority and compatible migration rules first.
Read-only extraction or additive schema evolution may proceed, but
independent writes remain coupled until ownership is enforced.
A new component must read a legacy schema
→ anti-corruption layer. Translate at the boundary; do not let
the legacy shape become the new model.
Business rules live in stored procedures and triggers
→ inventory them before anything else. Rules invisible to the
application are what makes "we replaced that module" false.
There are no tests
→ characterisation tests at the outermost stable boundary, from
real production inputs where possible.
The programme has produced new services and removed nothing
→ investigate removal blockers and coexistence cost. Prioritize a
bounded retirement path; continue independent justified work.
A slice's old and new paths must both work for a long period
→ route by feature flag per case, with an explicit owner per
case, and a comparison policy if both run.
Rules
- Inspect the project's JDK/toolchain, framework/database versions and compatibility contracts
before applying snippets. They illustrate application-specific seams, not a complete program
or authorization to upgrade the stack.
- A rewrite is a bet that the new system will catch up before the old one moves. It
rarely does, and the failure is usually organisational rather than technical: the funding
outlasts neither the roadmap nor the sponsor.
- Characterisation tests capture behaviour, including bugs. That is intentional — users
and downstream systems depend on behaviour nobody specified, and the migration's job is not
to fix it silently. Record the ones that look wrong; decide about them separately.
- Prefer triangulated evidence over recollection alone. Access/audit logs cover only their retention,
sampling and instrumentation; documentation can encode rare regulatory and recovery paths absent
from recent traffic. Choose an observation window from business cycles, not “a month” by default.
- Rules hide outside the application. Stored procedures, triggers, scheduled jobs,
database defaults, ETL scripts and a spreadsheet someone runs monthly. A module "replaced"
without inventorying those has left its rules behind, and they will fire on the new data.
- An anti-corruption layer is not a mapper. Its job is to prevent the legacy model's
concepts from entering the new one, which means it may drop, merge, rename and reinterpret
— and it will be ugly, because it holds the mismatch that would otherwise be spread
through the new code (
enterprise-base-patterns).
- Write ownership constrains independent evolution. Several writers can coexist under additive,
backward-compatible migrations, but conflicting semantics and removal cannot be made independently.
Establish authority before transferring writes. This is usually the largest and least glamorous
part of the work, and skipping it is why extractions fail
(
distribution-boundaries).
- Prefer strangling at a boundary that already exists — an endpoint, a queue, a batch file —
because it is already a contract and something already speaks it.
- Decommissioning must be on the plan with a date. "We will remove the old path once
things are stable" is how a programme ends with two systems. Removal is the deliverable
that realises the benefit.
- Keep the legacy system running well while it lives. Deliberate neglect ("it is going away")
extends its life by making the migration riskier and the team's mornings worse.
- Prefer separating technology upgrades from domain restructuring so failures remain attributable.
Combine them only when one unlocks the other and the migration has independent behavioral,
compatibility and rollback evidence.
References
- Strangler and anti-corruption layer —
choosing the interception point, routing a slice with a flag, running old and new in
parallel with a comparison policy, the anti-corruption layer's shape and what belongs in
it, and decommissioning as a scheduled step with its verification. Read when planning or
executing an extraction from a legacy system.
- Understanding and migrating the data —
discovering a system from production evidence, characterisation tests without a
specification, inventorying rules in procedures and triggers, establishing table
ownership, and migrating a shared database incrementally with dual-write and
reconciliation. Read at the start of a modernisation, and before any change to a shared
schema.
1---2name: legacy-enterprise-modernization3description: Modernising an enterprise application that is in production and cannot stop: understanding a system nobody fully knows, pinning behaviour before changing it, strangling functionality out incrementally, and defending a new model with an anti-corruption layer. Use when a rewrite is proposed for a system that still earns money, when a shared database has several writers, when business rules live in stored procedures and triggers, when there are no tests and no specification, when a strangler migration has stalled with both systems running, or when a modernisation has run for a year with nothing decommissioned. Does not cover the specific pattern-to-pattern migrations (architecture-refactoring-paths), recognising the problems (enterprise-architecture-smells), whether a boundary should be remote (distribution-boundaries), or class-level seams and dependency breaking (java-legacy-code-testing).4---56# Legacy Enterprise Modernization78## Purpose910Change a system that is in production, poorly understood, and still earning revenue, without11a rewrite and without a stop-the-world release. The failure mode this exists to prevent is12the parallel rewrite: a new system built beside the old one, competing against a target that13keeps moving, funded until the first budget review, and eventually abandoned with both14systems in production.1516The second failure is subtler and more common: a modernisation programme that has run for a17year, produced several new services, and **decommissioned nothing** — so the organisation now18operates two architectures and pays for both.1920## Principles2122```text23Avoid a parallel rewrite while the original changes unless a bounded scope,24 compatibility strategy and funded cutover make drift controllable.2526Keep steps bounded with an observable benefit or prerequisite they unlock.27 Fund enabling work explicitly rather than assuming immediate revenue.2829Track decommissioning alongside business and risk-reduction outcomes.30 New code alone does not prove reduced coexistence cost.3132Understand before changing; pin before understanding fully.33 Characterisation tests capture behaviour you have not yet34 explained, which is most of it.3536The legacy model must not infect the new one.37 That is the entire purpose of an anti-corruption layer.38```3940## Workflow41421. **Map what exists**, from evidence: which tables are written by what, which endpoints are43 actually called, where rules live (code, procedures, triggers, jobs, spreadsheets).44 Reconcile production evidence, documentation and operator knowledge: each has blind spots.452. **Pin behaviour with characterisation tests** at the boundary you will preserve — usually46 the HTTP API or the batch output — before touching anything.473. **Pick the first slice by value and by risk**, not by architecture: something that changes48 often (so the pain is real), is reasonably self-contained, and whose failure is survivable.494. **Establish data ownership before transferring writes.** Shared writers constrain50 independent semantic changes; compatible additive work and read extraction can proceed.515. **Strangle**: route the slice's traffic to new code, keep the old path available, and52 remove it once the new one has proven itself.536. **Decommission explicitly**, as a scheduled deliverable with a date. Otherwise both54 systems live forever.5556## Decision rules5758```text59The system still changes regularly and earns money60 → prefer incremental replacement. A rewrite carries moving-target,61 parity and cutover risk; use it only with evidence those costs are bounded.6263The system is frozen, small, and thoroughly understood64 → a rewrite may be genuinely cheaper. This is rare; verify65 "frozen" against a representative change window and operational roadmap.6667Several applications write the same tables68 → establish write authority and compatible migration rules first.69 Read-only extraction or additive schema evolution may proceed, but70 independent writes remain coupled until ownership is enforced.7172A new component must read a legacy schema73 → anti-corruption layer. Translate at the boundary; do not let74 the legacy shape become the new model.7576Business rules live in stored procedures and triggers77 → inventory them before anything else. Rules invisible to the78 application are what makes "we replaced that module" false.7980There are no tests81 → characterisation tests at the outermost stable boundary, from82 real production inputs where possible.8384The programme has produced new services and removed nothing85 → investigate removal blockers and coexistence cost. Prioritize a86 bounded retirement path; continue independent justified work.8788A slice's old and new paths must both work for a long period89 → route by feature flag per case, with an explicit owner per90 case, and a comparison policy if both run.91```9293## Rules9495- Inspect the project's JDK/toolchain, framework/database versions and compatibility contracts96 before applying snippets. They illustrate application-specific seams, not a complete program97 or authorization to upgrade the stack.98- **A rewrite is a bet that the new system will catch up before the old one moves.** It99 rarely does, and the failure is usually organisational rather than technical: the funding100 outlasts neither the roadmap nor the sponsor.101- **Characterisation tests capture behaviour, including bugs.** That is intentional — users102 and downstream systems depend on behaviour nobody specified, and the migration's job is not103 to fix it silently. Record the ones that look wrong; decide about them separately.104- Prefer triangulated evidence over recollection alone. Access/audit logs cover only their retention,105 sampling and instrumentation; documentation can encode rare regulatory and recovery paths absent106 from recent traffic. Choose an observation window from business cycles, not “a month” by default.107- **Rules hide outside the application.** Stored procedures, triggers, scheduled jobs,108 database defaults, ETL scripts and a spreadsheet someone runs monthly. A module "replaced"109 without inventorying those has left its rules behind, and they will fire on the new data.110- **An anti-corruption layer is not a mapper.** Its job is to prevent the legacy model's111 concepts from entering the new one, which means it may drop, merge, rename and reinterpret112 — and it will be ugly, because it holds the mismatch that would otherwise be spread113 through the new code (`enterprise-base-patterns`).114- **Write ownership constrains independent evolution.** Several writers can coexist under additive,115 backward-compatible migrations, but conflicting semantics and removal cannot be made independently.116 Establish authority before transferring writes. This is usually the largest and least glamorous117 part of the work, and skipping it is why extractions fail118 (`distribution-boundaries`).119- Prefer strangling at a boundary that already exists — an endpoint, a queue, a batch file —120 because it is already a contract and something already speaks it.121- **Decommissioning must be on the plan with a date.** "We will remove the old path once122 things are stable" is how a programme ends with two systems. Removal is the deliverable123 that realises the benefit.124- Keep the legacy system running well while it lives. Deliberate neglect ("it is going away")125 extends its life by making the migration riskier and the team's mornings worse.126- Prefer separating technology upgrades from domain restructuring so failures remain attributable.127 Combine them only when one unlocks the other and the migration has independent behavioral,128 compatibility and rollback evidence.129130## References131132- [Strangler and anti-corruption layer](references/strangler-and-anticorruption.md) —133 choosing the interception point, routing a slice with a flag, running old and new in134 parallel with a comparison policy, the anti-corruption layer's shape and what belongs in135 it, and decommissioning as a scheduled step with its verification. Read when planning or136 executing an extraction from a legacy system.137- [Understanding and migrating the data](references/understanding-and-data-migration.md) —138 discovering a system from production evidence, characterisation tests without a139 specification, inventorying rules in procedures and triggers, establishing table140 ownership, and migrating a shared database incrementally with dual-write and141 reconciliation. Read at the start of a modernisation, and before any change to a shared142 schema.