Partway through a monolith-to-services rewrite, the new service started producing record states that its own code could not produce.
I read the new code three times looking for the bug. It was not there.
The tables had other writers. A scheduled job that patched rows nightly. An old admin screen that wrote status fields directly, bypassing every rule the application layer enforced. A runbook with manual SQL in it, which someone ran when support escalated.
None of that lived in the module I was replacing. All of it wrote to the data the module owned.
The expensive part was trust rather than code: once states show up that the new service cannot produce, you can no longer tell a migration defect from a side-channel write, and you reconcile by hand until you can.
So now I inventory writers instead of endpoints, before I touch a line of a rewrite. Every cron. Every admin action. Every integration holding database credentials. Every manual procedure that exists because the product never shipped the button for it.
If a table has writers outside the scope being rewritten, that table is not ready to move. Either the side channels get absorbed into the new service first, or the rewrite inherits them as undocumented behavior and finds them in production.
Legacy systems are hard, I think, for a reason that has little to do with the age of the code: the real interface is wider than the code suggests.
I write up more of these migration lessons on my blog: https://polycratia.com/c/648f7152
When you last moved a module out of a monolith, what turned out to be writing to its data that nobody had documented?