Custom software
Read the legacy system before you replace it
The undocumented behaviour in an old system is not an accident. It is usually twenty years of edge cases somebody handled correctly and nobody wrote down.

Every legacy replacement starts the same way. Someone shows you the system, apologises for it, and describes what it is supposed to do. Then you read the code and find eleven branches nobody mentioned.
The instinct is to treat those branches as cruft. Occasionally they are. Far more often each one is a real situation that happened once, hurt, and got handled. The branch is the scar tissue.
Characterisation before transformation
Before we change anything we write tests that pin down what the current system actually does, including the parts everyone agrees are wrong. Not what it should do. What it does.
This feels perverse for about a week. Then the first migration runs, three of those tests fail, and two of the three turn out to be behaviour a statutory requirement depends on. The third really was a bug, and now you can remove it deliberately, in its own change, with a note saying why.
Where the rules actually live
- The code, which is authoritative but silent about intent.
- The database constraints, often stricter and older than the code.
- The reports, which encode what the organisation believes is true.
- The people who work around the system daily, who know which fields lie.
- The tickets, which are where the edge cases are described in plain language.
Reports are the most underrated of these. A report that has been running for fifteen years is a specification that has been continuously validated by people who would complain if it were wrong.
Then replace it in place
We prefer to move a boundary at a time behind the existing interface, with both paths running and compared, rather than build a parallel system and attempt a cutover. Cutovers concentrate all the risk into one evening, which is precisely when you have the least ability to think.