Let's talk

Services

Enterprise Software Modernisation Services

Replacing or re-platforming legacy systems in stages, while the business carries on running on them.

Technology and operations leaders carrying a system nobody wants to touch and nobody can switch off

Enterprise Software Modernisation — a system we built, in use
The Sazinga Comply audit log. The screen is a real capture of an application we built and run; the surroundings are illustrative.

The reason legacy systems survive is rarely sentiment. They survive because nobody can state with confidence what they do, and a rewrite that cannot be proven equivalent is a business risk rather than an upgrade.

Modernisation work is therefore mostly evidence work. Establish what the system actually does, encode that as tests, then move capabilities across one at a time behind a routing layer, with the old path still available until the new one has proven itself in production.

Legacy software modernisation is the replacement of a running system’s capabilities one at a time, under a routing layer, without a date on which everything changes at once. The absence of that date is the point. It is what makes each step reversible and what stops the programme depending on a single weekend going well.

Why most modernisations stall

They stall before any code is written, at the moment someone has to say what the system does.

A big-bang rewrite has to reproduce years of accumulated behaviour before it can go live even once. Some of that behaviour is deliberate, some is an accident that the business has since built processes around, and nothing in the codebase distinguishes the two. The programme therefore cannot be estimated, cannot be de-risked incrementally, and discovers what it missed on cutover weekend with the business watching.

The second cause is that the people who could answer the questions are busy running the company on the system being replaced. A modernisation that requires large blocks of their time competes with their day job and loses. The work has to be structured so their input arrives in short, specific bursts — confirm this behaviour, look at this report, tell me whether this branch has ever fired — rather than as a discovery phase they are asked to attend.

What a legacy system actually looks like from the inside

Not a monolith with clean seams. A set of near-duplicated answers to the same question, written at different times by different people, most of them slightly wrong.

The pattern recurs so reliably it is worth naming. On one system, the question “who is this job assigned to?” was answered independently in nine places. Eight of those implementations were wrong: eighty-one records got an arbitrary answer and eighteen named a person who was not assigned at all. Elsewhere, three separate code paths decremented the same stock column — one took a row lock, one wrote a ledger entry, and one did neither.

It is usually visible in the file sizes before it is visible anywhere else. In one estate we inherited, a single profile controller ran to 4,857 lines and its sibling in a second service to 3,525, and the matching rules the business actually competes on had been copied between the two and left to drift — 1,609 lines on one side, 1,288 on the other, no test in either. The database told a different story again: sixty tables in one hand-maintained SQL file, no migration framework, and five of those tables represented in the application’s model layer. In another, the dependency manifest declared drivers for two different databases at once, which is what a migration that stopped halfway looks like from the outside.

The other recurring shape is a fact that stopped being true and nothing noticed. A driver dashboard counted work by a row’s updated timestamp; the timestamp was written on insert and had never moved on any of two hundred and twenty-one rows, so “completed this month” silently meant “assigned this month”. A status column said active and nothing had looked at it since the day it was written.

Provenance is usually the first casualty, and it goes early. Manifests routinely still carry the starter template’s name, author and homepage years into a system’s life, so the file someone reaches for first to find out what they have inherited describes what the project was cloned from rather than what it became. Version history is often no better — an application in daily use can have one commit and a single tracked file, with the entire working tree unversioned.

A legacy system’s real specification is its data, not its code. The code says what should happen; the rows say what did.

How comprehension is done, and where AI actually helps

Behaviour is recovered from four sources and each is treated as a hypothesis until the others agree: the source, the database, production logs, and the people who use the system daily.

AI is genuinely valuable here and this is the place it earns the most. Faced with a two-thousand line stored procedure written by someone who left years ago, a model will produce a readable summary of what it appears to do, trace the call graph, identify inputs and outputs and flag branches that look unreachable. That replaces a week of reading with a day of checking, and it makes work tractable that otherwise never starts.

It is a starting hypothesis and nothing more. Every claim is verified against the running system before anyone relies on it, because the failure mode of these tools is a fluent, plausible, confidently wrong description — and legacy code is precisely where a reader has no independent intuition to catch it. What comes out of comprehension is not a document. It is characterisation tests: assertions that encode what the system does today, including the behaviour nobody intended, so the replacement has something concrete to be judged against.

When not to modernise

Three honest alternatives should be considered before a modernisation is commissioned.

Buy the package. If the system’s capabilities are now commodity — accounting, payroll, CRM, ticketing — the reason it was built in-house has usually expired. Re-implementing commodity software as custom software is the most expensive way to stand still.

Delete it. Some of what is being modernised is not used. Before rebuilding one workflow-heavy system around how the work actually ran, four hundred and eighty files were deleted: an approval step no record could ever reach, a module nobody opened. Establishing what is dead is faster than porting it, and the deletion is reversible in a way a rewrite is not.

Leave it alone. A stable system that nobody needs to change is not a problem, it is an asset with low running costs. The case for modernisation has to rest on a change you are actually blocked from making, a supported-platform deadline, or a risk you can name. “It is old” is not a requirement, and a programme built on it runs out of sponsorship in month five.

Why data migration is its own project

The application can be rebuilt incrementally. The data moves once, and it is the part that produces irreversible damage.

A migration that runs without errors proves nothing at all. What proves it is reconciliation: every entity count compared against the source, and the cause of every single difference named. On one import, 2,698 rows arrived where 2,800 were expected, and the hundred-odd missing records were the whole finding — they had a structural cause worth knowing about, and a clean run would have concealed it.

The step that actually moves the data is deliberately not automated. On one cutover the script that archived the old bookings was kept out of the numbered migration set on purpose, with the reason written at the top of the file: it runs once, by hand, on the production database, on cutover day, after a fresh backup. A one-way operation that a deployment pipeline could trigger by accident should not be reachable by one.

Sometimes the right decision is that there is no data migration. On one rebuild that was recorded as a locked decision at the start — greenfield, with seeded reference data, and the old system kept readable for a defined period rather than emptied into the new one. It is a legitimate answer and it is much cheaper than the alternative, but only if it is decided deliberately and written down, rather than arrived at in month six because the migration proved harder than the rewrite.

Two failure modes are worth designing against in advance. Destructive tooling should establish which system it is connected to by asking that system — the database’s own name, a marker row — because address-based checks are worthless where tunnels are used, and a tunnel makes production answer on localhost. And a reset that empties every table is a different operation from undoing what the script did: TRUNCATE ... CASCADE does not delete related rows, it empties related tables, which on one occasion took every system role and permission grant with it.

What cutover and the way back look like

A routing layer sits in front of the legacy system and sends selected requests to the new services. The old system keeps serving everything else. The share it handles shrinks until it can be switched off, and until that point every capability has two implementations that can be compared against each other on live traffic. The rebuilt schema usually runs under its own database name alongside the original rather than replacing it in place, so “go back” means pointing at the other one rather than restoring anything.

Capabilities are ordered by risk rather than by ambition. The first one across is chosen because it is well understood, low volume and easy to reconcile — the point of the first migration is to prove the routing, the dual-running and the rollback, not to deliver value. Reads move before writes. Anything financial moves late, after the reconciliation tooling has been exercised on something cheaper.

The switch itself is arranged so that no step has a window in which nothing is serving. On our own platform migration the new build was staged at a separate path and swapped in before the proxy was reloaded, rather than written over the live directory in place; the routing map was generated from the old system’s own inventory joined to six months of traffic data rather than compiled by hand; and every destination in that map was asserted to resolve in the new build before the switch, not after it. The application was verified by address before the name was repointed, so the first public request was not also the first request.

The way back is rehearsed, not documented. A rollback that has only been written down is a plan; a rollback someone has executed against a copy of production is a control, and a backup is proven by restoring it into a scratch database and counting rows, not by checking that a file exists. The same standard applies to every claim made about the new environment — a security control you have not watched refuse a request is an intention rather than a control, and a deploy that warns and carries on will report success while the old code keeps serving.

Verification continues after the switch, because the defects that matter here are quiet. On that same migration, every entry in the redirect map had been generated with a trailing slash while the outside world held plenty of those addresses without one, so a set of requests received a 404 where a 301 was intended. Nothing failed. Nothing alerted. It was found by going back and asking the consumers what they were actually getting.

What you are left with

A system in your repository, on your infrastructure, with migrations, characterisation tests, deployment scripts and a runbook — and, importantly, a record of the behaviours that were found and deliberately not carried across, with the reason for each.

That record is the artefact people undervalue and later need most. Years of undocumented behaviour is exactly what created the situation being fixed, and a modernisation that produces a new system with the same evidence problem has moved the risk rather than removed it.

How it runs

What the engagement includes

  1. 01 Comprehension first, using AI-assisted reading of undocumented code alongside people who know the business
  2. 02 Strangler-fig routing so new services take traffic one capability at a time
  3. 03 Incremental cutover with dual-running, reconciliation and a rehearsed way back
  4. 04 Data migration treated as its own project, with correctness checks that run on every rehearsal

Tell us how the work runs today.

This page is written for technology and operations leaders carrying a system nobody wants to touch and nobody can switch off. If that is you, describe what happens today — what is done by hand, what breaks, and what it costs when it does — and we will say honestly whether Enterprise Software Modernisation is the right shape for it, or whether something smaller would do.

A person reads every enquiry and replies within one working day.

Enterprise Software Modernisation — frequently asked questions

Why not rewrite the system in one go?

Because a big-bang rewrite has to reproduce years of undocumented behaviour before it can go live even once, and it usually discovers the missing behaviour on cutover weekend. Taking capabilities across one at a time means every step is reversible.

How does the strangler-fig approach actually work?

A routing layer sits in front of the legacy system and sends selected requests to new services instead. The old system keeps serving everything else, and the share of traffic it handles shrinks until it can be switched off.

What does AI genuinely help with on legacy code?

Reading it. Summarising what a large stored procedure or a class with no tests appears to do, mapping call graphs, drafting characterisation tests and flagging dead paths. Every output is verified against the running system before anyone relies on it.

What if the original developers are gone and there is no documentation?

That is the normal case. Behaviour is recovered from the code, the database, production logs and the people who use the system daily, then written down as characterisation tests so the new implementation has something to be judged against.