Legacy Modernisation Without the Big-Bang Rewrite
Full rewrites fail for predictable reasons
The old system encodes fifteen years of business rules, most of them undocumented and some of them the only reason a particular customer stays. A rewrite has to rediscover all of them while the original keeps changing, and it delivers no value until it is finished — which means it is the first thing cut when priorities shift. We have been brought in to rescue enough of these to be blunt about it: unless the existing system is genuinely unmaintainable, incremental replacement is both cheaper and much more likely to finish.
The strangler pattern, applied practically
Put a routing layer in front of the legacy application. Move one capability at a time behind that router into a new service, with the old code left in place until the new path is proven. Each move is independently valuable and independently reversible. The first candidate should be something with clear boundaries and low coupling — notifications, document generation, a reporting view, a public API. Avoid starting with the core transactional heart of the system, however tempting, because that is where the undocumented rules live.
The database is the hard part
Splitting application code is straightforward compared to splitting a database that every module reads directly. The intermediate step that works: keep the shared database initially, but force all new code to access it through a defined service interface rather than direct queries. Once a capability's data access is funnelled through one place, moving that data to its own store becomes a contained project. Trying to split code and data simultaneously is how eighteen-month modernisations become three-year ones.
Not everything should become a microservice
Microservices buy independent deployment and scaling at the cost of network calls, distributed transactions, and an operational burden that needs a platform team to support. For a company with fifteen engineers, a well-structured modular monolith — clear internal boundaries, one deployable — delivers most of the maintainability benefit with a fraction of the operational cost. Split out services where there is a real reason: different scaling profile, different release cadence, different team ownership, different compliance boundary.
Characterisation tests before you touch anything
You cannot safely refactor code you cannot test, and legacy systems rarely have meaningful test coverage. Write tests that capture what the system currently does — including the bugs — for the paths you are about to change. These are not tests of correctness; they are a tripwire that tells you when behaviour changed unintentionally. Recording real production inputs and outputs is often the fastest way to build them, subject to proper handling of any personal data in that traffic.
Keep the lights on while you do it
Modernisation competes with the feature roadmap, and the roadmap usually wins in a straight fight. The way to protect it is to attach modernisation work to features: when a change is needed in an old module, that module gets migrated as part of the work. Progress is slower per quarter but it is continuous and it is politically survivable, which matters more. Track the percentage of traffic served by new components as a visible metric so progress is legible to people outside engineering.
Have a project that needs this kind of thinking?
Let's talk through what you're building — free consultation, no commitment.
Get in Touch