A legacy system has no clean boundaries to extract — one shared database, entangled modules, no tests. What is the first move, and in what order should capability be recovered?
Show the full answer Hide the answer
The situation
Strangler, extraction and incremental migration all assume the existence of a seam — a boundary at which the system can be cut. A system with a single shared database written by every module, business logic in stored procedures and triggers, and no test coverage has no such boundary, and every proposed extraction turns out to require the whole system.
Creating the seam is therefore the first project, and it is not a step within the migration — it precedes it.
The order of recovery
1. Establish what the system currently does — not what it should do.
Characterisation tests: capture real inputs and outputs from production, and assert that the system produces those outputs. They document current behaviour including its bugs, which is exactly right, because the migration's obligation is to preserve behaviour that something depends on — and you cannot yet know which bugs are depended upon.
Without this, no change is safe and every refactoring is a gamble.
2. Find the real boundaries, from data rather than from opinion.
- Which tables are written by which code paths. A table written by one module is a candidate boundary; one written by nine is the problem.
- Change coupling from version control — what actually changes together, regardless of the module structure.
- Runtime call and query analysis, which reveals the actual dependency graph rather than the intended one.
3. Create seams inside the existing system, before extracting anything:
- Introduce an interface at the intended boundary, with all callers routed through it — still in-process, still the same database.
- Move logic out of the database — stored procedures and triggers into application code — because logic in the database is the hardest thing to extract and is usually the reason the seam does not exist.
- Split the write paths so exactly one module writes each table, which is the essential precondition for ever separating the data.
- Add an anti-corruption layer at the boundary, translating between the legacy model and a cleaner one, so the new component is not born carrying the old model's compromises.
4. Only then extract, following the strangler sequence, with the facade already in place from step 3.
The temptation to resist
Rewriting from scratch, which is enormously attractive here and fails for a well-documented reason: the legacy system's incomprehensible complexity is largely accumulated correctness — every strange conditional is a defect someone fixed, a regulatory requirement, or a customer's edge case.
A rewrite discards that and rediscovers it over several years, in production, while the business waits and the old system continues to change — so the rewrite must chase a moving target, which is why the two-system period extends indefinitely.
A rewrite is defensible when the domain has genuinely changed, such that the old behaviour is not the requirement any more. It is rarely defensible on the grounds that the code is bad, because the code is not the asset — the accumulated behaviour is.
Setting expectations honestly
This work is slow and produces no visible feature for a long time, and it must be funded and communicated as such. The credible framing is the one that makes it fundable: "we currently cannot change this system safely; here is the measured cost of that, and here is the sequence that restores the ability to change it" — with the first milestone being characterisation coverage, which is verifiable and unglamorous.