Rewrites fail for a consistent reason: the replacement handles the documented behaviour and not the accumulated exceptions the business quietly depends on.
A legacy system is one that still runs the business and can no longer be changed safely. The instinct is to replace it entirely. That instinct is often right about the destination and consistently wrong about the route.
The undocumented behaviour is the system
Over years, a system accumulates exceptions: the customer type billed differently, the report finance depends on that nobody requested, the validation rule relaxed for one region. None of this is in the specification. All of it is load-bearing, and the replacement discovers it one production incident at a time.
Big-bang cutover concentrates all the risk
A single switchover means every unknown surfaces simultaneously, under time pressure, with rollback either impossible or itself risky. Replacing capability by capability spreads that risk across many small, reversible steps, each verified while the old path still works.
A sequence that tends to work
- Instrument the existing system to observe what it genuinely does under real usage
- Put a boundary in front of it so callers depend on an interface, not internals
- Move one capability behind that boundary, running old and new in parallel
- Compare outputs on live traffic until the new path is demonstrably equivalent
- Cut over that capability alone, with the old path available for rollback
- Repeat, retiring legacy code only once nothing calls it
This is slower than a rewrite on paper and faster in practice, because it does not require a period during which the business runs on something unproven. The system stays working the entire time, which is the only requirement that was never negotiable.