How to preserve business behavior during modernization
How to find the behavior a legacy system's users depend on, decide what must survive a rewrite or migration, and prove afterwards that it did.
Modernization projects rarely fail because the new system cannot be built. They fail because the new system does something slightly different from the old one, and nobody knew the difference mattered until a customer, a regulator or a downstream job noticed. The behavior that breaks is almost never in the requirements document. It is in the code, the data and the habits of the people who use the system.
Preserving behavior is therefore an evidence problem before it is an engineering problem. You cannot preserve what you have not established.
Separate specified behavior from actual behavior
Every long-lived system has three overlapping sets of behavior.
- Specified. What the documentation, tickets and original design say it does.
- Actual. What it does today, including everything added by patches, hotfixes and defaults.
- Relied upon. The subset of actual behavior that some user, integration or report depends on, whether anyone intended it or not.
Modernization plans are usually written against the first set. The risk lives in the gap between the first and the third. A legacy order service that silently truncates notes at 255 characters is not in the spec. If a warehouse label printer has been formatted around 255 characters for eight years, it is relied upon.
The job is to make the third set visible before anything is replaced.
Build the behavior inventory from evidence
Work through the system’s surfaces and record what each one does, with an evidence state for every item.
Interfaces. For each endpoint, message type or file format: inputs accepted, including undocumented fields; outputs produced; validation rules; error codes; ordering and pagination; idempotency behavior; timeouts. Record whether each was observed in a running system, inferred from code, or verified by a test.
Data. For each table or store: who writes, who reads, what invariants hold in the actual data (not the schema), which columns are populated, which nullable columns are never null in practice. A column the schema allows to be null but which twelve years of rows never leave empty is an invariant someone depends on.
Time-based behavior. Batch jobs, scheduled reports, end-of-period processing, retention sweeps. These are the easiest to miss in a migration because they do not run during a typical test window.
Side effects. Emails sent, webhooks fired, ledger entries written, audit rows inserted, files dropped on a share. Order matters: a system that charges the card before sending the confirmation behaves differently from one that does the reverse when the second step fails.
Defaults and fallbacks. What happens when a field is missing, a currency is unknown, a dependency is down. Fallback behavior is where legacy systems hide the most decisions.
An inventory entry needs the behavior, the evidence and whether anyone relies on it:
behavior: coupon ignored unless currency is USD
state: verified
test: conformance/orders/coupon-currency.spec.ts
relied_upon: yes
by: EU storefront reconciliation report assumes zero discounts
decision: preserve
Decide what must survive
Not every behavior should be preserved. Some are bugs. Some are workarounds for constraints that no longer exist. The decision is a business one, and it needs the inventory to be made deliberately rather than by accident.
For each relied-upon behavior, choose one of four outcomes and record who chose it.
- Preserve exactly. The new system must match. Write the conformance test now, against the old system, so it is ready to run against the new one.
- Preserve with a documented change. The behavior changes, and every known dependent is told and migrated. The 255-character truncation becomes a validation error, and the label printer integration is updated first.
- Retire. Nobody relies on it. Confirm that with evidence, not assumption. A report that nobody has opened in two years, according to access logs, is evidence. “I don’t think anyone uses that” is not.
- Open. You do not know whether anyone relies on it. Treat it as preserve until you do, and add finding out to the plan.
The fourth category is the honest one, and it is often the largest at the start. The size of that category is a reasonable measure of how ready the organization is to begin replacing anything.
Make preservation testable
A behavior you intend to preserve needs a test that runs against both systems and produces the same answer. Three forms are useful.
- Conformance tests. Scripted requests with expected responses, run against old and new. They cover interfaces well and are the first thing to build.
- Record and replay. Capture real traffic from the old system, replay it against the new, and diff the responses and side effects. This finds the behaviors you did not think to write tests for. It needs care with personal data and with side effects that should not fire twice.
- Data invariants. Checks that run against the stores of both systems after the same inputs: row counts, sums, the never-null columns, referential integrity that the schema does not enforce.
A test only proves what it exercises. A conformance suite that passes on 40 endpoints says nothing about the 41st. Keep the inventory and the test suite in step so the coverage gap is visible rather than assumed away.
Migrate incrementally, verifying at each step
The strangler pattern is well known. What makes it work is less often said: each slice moved to the new system needs its own verified behavior inventory, its own tests running against both sides, and a period of running both before the old path is removed.
- Route a slice of traffic to the new implementation while the old one still handles the rest.
- Compare outputs and side effects continuously, not just at cutover.
- Keep the old path available until the comparison has run through every time-based behavior at least once. If there is a month-end job, that means at least one month-end.
- Only then retire the old path, and keep the tests. They are now the specification of the new system.
What to carry forward
At the end, the behavior inventory and the tests are the documentation the old system never had. Keep them with the new system. When the next modernization comes, and it will, the team starting it will have the thing this one lacked: an accurate account of what the system does and who depends on it.
Where Foundry fits
Foundry produces the behavior inventory and the change-impact assessment that this approach depends on. It analyzes the existing system, verifies findings with repeatable tests, labels what is observed, inferred, verified or still open, and delivers the conformance tests so they can run against the replacement. The Advance use case describes how teams use it ahead of a migration.