Legacy Side-Effect Inventory
also called Trigger Inventory, Write Consequence Map
The enumerated list of triggers, stored procedures, scheduled jobs and integrations that fire as a consequence of a legacy write, which determines whether a capability can be moved incrementally at all.
A team two years into a strangler migration has moved three read capabilities and is ready to move its first write. The legacy system is an order manager on a database where roughly 40 triggers and packages hold business rules that exist nowhere else - not in a document, not in the application, not in anybody's head.
They move the write. The new service inserts the order row correctly. Stock is not decremented, because that was a row trigger. No audit row appears, because that was a package call. The warehouse never hears about the order, because the message was enqueued by another trigger. Nothing errors, and the mismatch is found three weeks later by someone reconciling stock.
In a system of this age the legacy write is not storage; it is a set of consequences, and the migration's unit of work is the consequence set rather than the table.
Why it matters
A read capability can be moved wrongly and the worst outcome is a wrong answer you can compare against the legacy answer whenever you like. A write cannot be checked after the fact, because the comparison requires knowing what should have happened, which is exactly the missing knowledge.
That changes how capabilities are sequenced. The usual advice - lowest volume first, to limit blast radius - optimises damage per hour and says nothing about detectability. A quarterly write with twelve undocumented triggers has the lowest volume in the system and is the worst possible first move. The inventory makes knowability the criterion, with volume as the tiebreaker.
Implementation patterns
- Build it from two sources and treat the gap as the risk register. The data dictionary gives the declared graph; in Oracle that is
ALL_TRIGGERS,ALL_DEPENDENCIESandALL_SOURCE. It misses dynamic SQL assembled inside packages. - Observe what actually fires, with 7 days or more of DML auditing or a logical-replication tap on the tables the capability writes. Observation misses the month-end path that did not run in the window.
- Include the non-trigger consequences - scheduled jobs keyed off a column a trigger sets, views refreshed on commit, extracts reading a timestamp, integrations polling for new rows.
- Sequence the cutover so every step before the last is reversible: shadow the write with no persistence, dual-persist with legacy authoritative, flip authority, disable the legacy triggers, delete the legacy path.
- Reconcile daily on the business event and alert on the first unmatched pair, not on a rate, because a capability cut over in week 14 produces a handful of mismatches a day that no percentage threshold notices.
Industry example
The pattern is characteristic of core-system replacements in insurance, banking and retail order management, where the database layer accumulated business logic through the 1990s and 2000s because that was where it performed. Public write-ups converge on one finding: the capabilities that could not be moved incrementally were the ones whose database-side consequences could not be enumerated, and the programmes that finished spent their first phase making the legacy side legible.
Failure scenarios
- Consequences stop silently when the new service writes directly, so stock, audit trails and downstream integrations drift with no error anywhere.
- Consequences run twice when the legacy path is kept warm as a safety net and both paths fire the triggers.
- Static analysis only, so dynamic SQL inside packages is missed and the inventory is confidently wrong.
- Observation only, over a window excluding month end, so the quarterly consequence appears three months after cutover.
- The legacy triggers disabled before the new service reproduces them, the one step routing cannot revert.
Trade-offs
The inventory costs 2 to 3 days per capability of investigation that produces no demo. The alternative is a cutover whose failure mode is silent, delayed and discovered by the business.
| Choose | Gains | Pays |
|---|---|---|
| Inventory before each write migration | The cutover is reversible and verifiable | Days per capability with nothing visible to show |
| Lowest-volume-first with no inventory | Visible progress sooner | A silent defect class found weeks later by reconciliation |
When not to use it
If the legacy business logic lives in application code rather than the database, the call graph is already the inventory and a separate document duplicates what a reader can see; sequence by volume instead. The same applies where a tested trigger inventory already exists, in which case ordering becomes a product decision.
If no capability has an enumerable consequence set, the inventory is not the next step. Making the legacy side legible is, by moving trigger logic into callable procedures or application code before any write moves.
Interview question
Q: You are about to move the first write off a 20-year-old database-centric system. The business wants the capability it cares most about. How do you choose, what do you build first, and when can you no longer roll back?
What a strong answer covers: that the criterion is enumerability of consequences rather than volume or business priority; both sources and why the gap between them is the finding; the five-step reversible sequence; that disabling the legacy triggers is the point of no return; and daily reconciliation alerting on the first mismatch.
Quick check
Quiz: Why is "move the lowest-volume write first" the wrong primary rule for a database-centric legacy system? Answer: It limits damage per hour and does nothing for detectability - a quarterly write with twelve undocumented triggers is both the lowest volume and the least knowable.
Flashcard: What are the two sources of a legacy side-effect inventory, and what does each miss? - The data dictionary gives the declared graph and misses dynamic SQL inside packages; a week or more of DML auditing shows what actually fires and misses the month-end path.