A rewrite replaces your code, but what it really replaces is your specification, and in most legacy systems the running code is the only specification that exists.
So "the code is bad" is not a reason to rewrite. It is a reason to read. Five questions decide whether a rewrite is survivable.
-
What writes to this database that isn't this codebase? Cron jobs, admin SQL, another service, a person with direct access. Own the code but not every writer, and you inherit silent corruption on day one.
-
Which current behaviors are contracts, and which are accidents? Rounding direction, result ordering, a timezone default, an error code a client branches on. Callers depend on the accidents. You find that out at cutover.
-
Can both systems run against the same data at the same time? If not, what you have is not a rewrite: it is a cutover with no rollback.
-
How will you compare the two while both run? In payment work that is just reconciliation: same inputs, diff the outputs, flag divergence instead of trusting it. Without that, "it works" means only that nobody has complained yet.
-
What is actually blocking you? Usually one module (the payment path, the parser layer, the permissions model) and not the monolith around it. Strangle that one and keep the rest boring.
Forced migrations are the exception worth naming. A Python 2 to 3 move buys no features, so the only correct scope is mechanical and behavior-preserving, with no redesign smuggled in. Mixing the two is how a two-month migration becomes a year.
Rewrites rarely fail on the new architecture. They fail in the gap between what the old system did and what anyone ever wrote down.
Of those five, I would start with whichever one is hardest to answer in the system you're looking at right now.