The instinct when facing an old, hard-to-maintain codebase is often to propose starting over: a clean rewrite, modern stack, none of the accumulated compromises. It's an understandable instinct, and it's usually the wrong call for a system that's actively running a business, because a full rewrite means running two systems in parallel for months or years while the new one slowly catches up to everything the old one already does, quietly and often undocumented.
The strangler fig pattern is the more reliable alternative. New functionality gets built in the new system, and traffic gets routed to it incrementally, route by route or feature by feature, while the old system keeps running everything that hasn't been migrated yet. The old system shrinks over time instead of getting replaced all at once, and at no point is the business betting everything on a single cutover date.
The hardest part of this approach isn't writing the new code, it's understanding what the old system actually does, including the parts nobody remembers deciding on purpose. A surprising amount of legacy behavior exists because of a specific edge case from years ago, not because of a documented requirement, and migrating without preserving that behavior recreates a bug the original system quietly fixed a long time ago.
A practical first step is instrumenting the old system before touching it: logging which routes and features actually get used, and how often. Legacy systems accumulate dead code the same way they accumulate everything else, and migrating unused functionality is wasted effort that a full rewrite would have wasted too, just less visibly.
A full rewrite is occasionally the right call, usually when the old system's core architecture is fundamentally incompatible with what the product needs to become, not just outdated in its tooling. That's a real but less common case. For most legacy modernization work, incremental migration gets to a maintainable system faster, with far less risk to the business running on it in the meantime.

