Behaviour archaeology
We read the code, trace production traffic and interview whoever is left. LLM-assisted summarisation speeds comprehension, but nothing enters the migration plan until an executable test proves it.
Characterisation test suiteModernisation
For the system that still earns money and nobody wants to touch. A routing layer goes in front, capabilities move onto a modern stack one at a time, and old paths get switched off.
Legacy rarely means old syntax. It means behaviour nobody can describe any more: the rounding rule in the invoicing job, the nightly file three downstream teams silently depend on, the flag that exists because of an incident in 2014. The code is the only remaining specification and the people who wrote it have moved on. That undocumented behaviour, not the codebase, is the asset you are actually migrating — and it is the thing a rewrite quietly agrees to reproduce without ever writing it down.
Full rewrites fail on a requirement nobody can write down: do everything the old system does. Meanwhile the business keeps changing the old system, so the target moves for as long as the build runs. We work in waves instead. A routing layer goes in front and forwards everything unchanged when it lands; each wave then takes one capability, rebuilds it, proves parity against the old path under real traffic, flips the route and deletes what it replaced.
| What is true of this work | What it forces | What we do about it |
|---|---|---|
| The code is the only specification | Behaviour must be captured first | Characterisation tests before any rebuild |
| The old system keeps changing | The migration target moves | Short waves, one slice each |
| One database, many hidden writers | Slices cannot own data cleanly | Change data capture, single writer |
| Batch jobs nobody documented | Failures surface days after cutover | Batch scheduled as first-class waves |
| Hybrid state costs two architectures | Waves must actually retire something | Named kill list per wave |
We read the code, trace production traffic and interview whoever is left. LLM-assisted summarisation speeds comprehension, but nothing enters the migration plan until an executable test proves it.
Characterisation test suiteThe gateway that makes incremental migration possible. It fronts the legacy application, holds the old URL and payload contracts intact, and lets us move one route at a time unnoticed.
Live routing seamShared tables are what block a slice. It moves to its own schema, change data capture keeps the monolith in sync rather than dual writes, one writer per table enforced.
Change data capture syncBefore traffic switches, the new path runs on mirrored requests with writes suppressed while a reconciler compares outputs field by field. Mismatches are triaged as legacy or new bugs before cutover.
Reconciliation reportOvernight jobs, SFTP drops, fixed-width files and the report finance still opens on a Monday. Undocumented and load-bearing, so they are scheduled as first-class waves rather than found during cutover weekend.
Rebuilt scheduled jobsTurning the old thing off is a deliverable, not a hope. Each wave ends with servers deprovisioned, licences cancelled, scheduled jobs removed and dead code deleted from the repository.
Decommission recordWe inventory every entry point, batch job, integration and database object, then trace which actually run. Dead routes are marked for deletion instead of migration, and the first slice is wrapped in characterisation tests.
You getSlice map, kill list, characterisation tests
The routing layer goes in front of the legacy application, proved transparent under production load, then the first slice is rebuilt behind it. Fourteen days in, new code serves one real route in a demo.
You getLive routing seam plus one slice
Live traffic is mirrored to the new path with writes suppressed, and outputs are reconciled until the diff is empty or every difference is explained. Traffic then shifts in stages, the old path staying warm.
You getReconciliation report and cutover runbook
We delete the legacy path, drop the tables it owned, cancel the licence, remove the cron entry and record what the wave killed. The next slice reuses the seam and test harness already in place.
You getDecommission record per wave
Strangle it while the system is live and still changing
A rewrite freezes your only working specification while the business keeps editing it, so the target moves for the whole build. Working behind a seam keeps every wave small, reversible and shippable, and you can stop after any of them.
We’d choose otherwise whenthe app is small, one team owns the whole domain, and the runtime is unsupported
A modular monolith by default, services only where justified
Most legacy pain is coupling and untested code, not deployment topology. Splitting a system you do not yet understand into twenty network calls converts logic bugs into distributed ones, which are far harder to reconcile during a migration.
We’d choose otherwise whena slice needs its own scaling profile, release cadence or compliance boundary — usually a handful
AI reads the legacy code; it does not ship the migration
We use it where it is strong: summarising modules, recovering business rules, drafting characterisation tests. Line-by-line translation preserves the structure you were paying to escape, and a plausible translation of a rounding rule looks correct until month-end reconciliation fails.
We’d choose otherwise whenthe target is a like-for-like port of a small, fully tested module with no business rules
Keep the bugs through cutover, fix them straight after
Parity is the only cheap way to prove a migration is safe. Change behaviour and outputs at the same time and every mismatch becomes an argument instead of a defect, which is how cutovers stall in review.
We’d choose otherwise whenthe old behaviour is a compliance or security problem — then it ships as a tracked change
Fourteen days into the first wave you get a working demo: the routing seam live in front of your application and one real capability served by new code, with the old contract intact. That first slice is chosen to be genuinely used but not business-critical, so the mechanism is proved on real traffic before anything that matters moves.
Because the estimate would be fiction and you would still pay the difference. Nobody can price twenty years of undocumented behaviour from the outside. We price a fixed-scope assessment, then price each wave once its slice has been read and tested. You approve wave by wave and can stop after any one, with working software in production.
The route flips back, usually in seconds. Nothing is deleted at cutover — the legacy path stays deployed and warm behind the seam until the new one has run clean for an agreed period. Every wave ships with a rollback runbook naming the switch, the owner, and the data repair steps if writes have already landed.
You own all of it — source, infrastructure definitions, tests and documentation transfer to you in full. We build on standard platforms and avoid anything only we can operate. The seam itself is ordinary gateway configuration. Handover includes a walkthrough with your engineers plus a 30-day warranty on defects in what we shipped.
It stays inside your boundary. The shadow path is deployed in your own infrastructure and accounts, writes are suppressed so nothing reaches downstream systems or third parties, and the reconciler runs there rather than on our machines. For regulated fields we compare hashes instead of storing raw values, and diff records are kept only until the mismatch closes.
Thirty minutes with the engineers who would build it. You leave with a scope, a timeline and a fixed price — or an honest no, and the reason why.