Ground Truth

The Old System Knows Things Nobody Wrote Down

Mostafa DhouibMostafa Dhouib·9 min read
The short answer

In long-lived business platforms a large share of policy sits in stored procedures, triggers, constraints, defaults and view definitions rather than in application code. A developer reading the application sees a call and a result, and the policy inside is invisible. Enumerating it from the system catalogue produces an artefact the organisation has never had, and the gap between that artefact and the specification is the real finding.

The Old System Knows Things Nobody Wrote Down

The short answer. In a business platform that has run for a decade or more, a large share of the organisation's policy does not live in application code. It lives in stored procedures, triggers, check constraints, column defaults and view definitions. A developer reading the application sees a call and a result, and the policy inside that call is invisible to them, even though it is frequently the most business-critical logic in the company. You find it by enumerating the database's own catalogue rather than by interviewing people, because interviews surface the behaviour users notice, and a system's actual behaviour includes everything it does correctly enough that nobody ever noticed. The artefact is a rules register.

A replacement project for an old platform usually starts with discovery workshops. Users describe what they do, someone writes it down, and the result is called a functional specification.

That document is not wrong. It is incomplete in a specific and dangerous way, and the incompleteness has a structural cause rather than a diligence cause.

The policy is not where the developer is looking

Platforms of this age were built when putting logic in the database was normal practice, and then they accreted.

Stored procedures hold multi-step operations with branching that encodes real commercial policy, such as which discount applies to which customer class under which conditions. Triggers fire on write and maintain invariants, write audit rows, cascade changes to related records, or reject operations that break a rule nobody has stated out loud in years. Check constraints and column defaults encode assumptions about what a valid record is. View definitions carry the operative definition of a customer, an active account or a billable item, and those definitions have been edited repeatedly in response to real problems.

The application calls a procedure and gets a result. Reading the application tells you the call site and the shape of the return value. It tells you nothing about the policy, because the policy is on the other side of the boundary.

That is the whole mechanism. Everyone involved is reading the wrong artefact, and the artefact they are reading looks complete.

What the application shows
A call to a stored procedure
The arguments passed in
The shape of the return value
Nothing about the policy
frequently the most business critical logic in the company
What sits on the other side
Stored procedures with branching
which discount applies to which customer class under which conditions
Triggers firing on write
maintaining invariants, writing audit rows, rejecting operations that break a rule nobody has stated out loud in years
Check constraints and column defaults
the operative definition of a valid record
View definitions edited repeatedly
the working definition of a customer, an active account or a billable item
the procedure call
A user can describe behaviour they observe. They cannot describe behaviour that has never surprised them, and a constraint is load bearing precisely because it has been quietly successful.
FigureThe whole mechanism is that everybody is reading the wrong artefact, and the artefact they are reading looks complete. The application shows a call site and a return shape. The policy is on the other side of the boundary, where no amount of careful reading of the application will reach it.

Interviews produce the noticed behaviour

A user can describe behaviour they observe. They cannot describe behaviour that has never surprised them.

Consider a trigger that silently normalises a field on insert. For years, every record entering the system comes out consistent, so no user has ever had a reason to form a belief about it. Ask them how that field works and they will describe the field, not the normalisation, because from where they sit there is nothing to describe.

The same applies to rejections. A constraint that prevents an invalid combination has, over its life, mostly prevented things nobody tried to do. It is load-bearing precisely because it has been quietly successful.

So the specification describes the behaviour people notice, and the set of things a system does is much larger than the set of things people notice about it. That difference is not a gap in the workshop facilitation. It is the definition of a mature system.

How to enumerate it

The enumeration is mechanical, and it is done against a copy of the database with nothing live touched.

Start from the system catalogue, not from a diagram. Every relational engine exposes its own metadata. Extract the full list of routines with their source text, the full list of triggers with their table, timing and event, every constraint with its expression, every column default, and every view with its definition. This is a query, not an investigation, and it gives you the complete population before any judgement is applied.

Build the dependency graph. Which procedures call which other procedures. Which views are built on which views. Which triggers write to tables that have their own triggers. Depth here is the thing that makes manual tracing fail, and a nested view four levels deep with a filter at each level is a definition nobody can state from memory.

Classify each item as plumbing or policy. A trigger maintaining a modified timestamp is plumbing. A trigger that reassigns ownership of a record when a status changes is policy. This is the step that needs a human, and it is the only step that does, which is why the extraction has to be automatic.

Record the last modified date wherever the engine keeps it. A procedure edited recently is live policy. One untouched for many years may be dead, and dead policy that gets faithfully reimplemented is its own kind of waste.

Check execution reality against the catalogue. A procedure existing is not evidence that it runs. Where the engine keeps execution statistics, pull them. Where it does not, log calls on the copy under a replay of real traffic. An object that has not executed in a year is a different risk from one that executes thousands of times a day.

Query the system catalogue
every routine with its source, every trigger with its table and event, every constraint expression, every default, every view definition
Build the dependency graph
depth is what makes manual tracing fail, and a nested view four levels deep is a definition nobody can state from memory
Classify each item as plumbing or policy
a modified timestamp is plumbing, reassigning ownership on a status change is policy, and this is the only step that needs a person
Record the last modified date
recently edited is live policy, untouched for years may be dead, and dead policy faithfully reimplemented is its own waste
Check execution reality
an object existing is not evidence that it runs, so pull execution statistics or log calls under a replay of real traffic
The output is a register with one row per rule, and its most valuable column is the one listing rules with no owner in the new design. A replacement scoped from the workshop specification is priced against the behaviour people notice. One scoped from the register is priced against the behaviour that exists.
FigureThe extraction is a query rather than an investigation, which is what gives you the complete population before any judgement is applied. Exactly one step needs a human, and that is the reason the rest has to be automatic.

The artefact this produces

The output is not a report. It is a register, maintained after the engagement, with one row per rule.

Each row carries the object it lives in, the table or entity it affects, a plain English statement of what it enforces, whether it is policy or plumbing, whether anything in the new design accounts for it, and who owns the decision if nothing does.

Three things come out of that register immediately.

The list of rules with no owner in the new design. This is the real finding and it is always longer than anyone expected. Each entry is a decision someone has to make, which is better than a surprise someone has to survive.

The list of rules nobody can explain. An object whose behaviour is clear but whose purpose is not. These are the ones that get deleted in a rewrite and then re-added as an urgent fix nine months later.

A written description of the organisation's own rules. Most companies in this position have never had one. The register outlives the migration and is useful on its own, even if the replacement never proceeds.

Why this is the cheapest work in the project

The enumeration runs against a copy, delivers no features, and carries no deployment risk. That is what makes it worth doing thoroughly rather than quickly.

It also reframes the commercial conversation. A replacement scoped from the workshop specification is priced against the behaviour people notice. A replacement scoped from the register is priced against the behaviour that exists. The second number is larger and it is the real one, and finding that out before signing a fixed scope is the point.

The honest limit

Enumeration finds what is reachable from the catalogue, which is not everything.

Policy can also sit in application code, in scheduled jobs outside the database, in integration middleware, in reporting layers that redefine entities a third time, and in spreadsheets that someone runs on the last day of the month. The register covers the database, and the database is usually where the largest concentration sits, but it is not the whole surface.

More importantly, reading a rule is not the same as knowing when it fires. A branch inside a procedure that only triggers on a condition absent from your copied data will be catalogued correctly and still surprise you, because you will have recorded its existence without ever seeing its effect. The register tells you a rule is there. It does not tell you how often it matters, and that limit is why the old system has to stay available during the replacement rather than being switched off once the catalogue is complete.

FAQ

Where does business logic actually live in an old business platform? A large share of it sits in the database rather than in application code, specifically in stored procedures, triggers, check constraints, column defaults and view definitions. The application calls a procedure and receives a result, so a developer reading the application sees the call site and nothing of the policy inside it, which is often the most business-critical logic in the organisation.

Why do discovery workshops miss these rules? Because users can only describe behaviour they have noticed. A trigger that has silently normalised a field correctly for years gives nobody a reason to form a belief about it, and a constraint that blocks an invalid combination has mostly blocked things nobody attempted. The set of things a mature system does is much larger than the set of things anyone has observed about it.

How do you enumerate database-resident policy? Query the engine's own system catalogue against a copy. Extract every routine with its source, every trigger with its table and event, every constraint expression, every column default and every view definition. Then build the dependency graph, classify each item as plumbing or policy, and pull execution statistics to separate live rules from dead ones.

What do you do with a rule nobody can explain? Record it in the register as unexplained and leave it in place. Rules with clear behaviour and unclear purpose are the ones most often deleted during a rewrite and then re-added later as an urgent production fix. An unexplained rule that has run for a decade has more evidence behind it than an assumption made in a workshop.

What is the deliverable of this work? A rules register with one row per rule, naming the object, the entity it affects, what it enforces in plain English, whether it is policy or plumbing, and whether anything in the new design accounts for it. Its most valuable column is the one showing which rules currently have no owner, and the register remains useful even if the replacement is cancelled.

Does a complete catalogue mean the migration is safe? No. A catalogue records that a rule exists, not how often it fires or under what real conditions. A branch that only activates on data absent from the copy is catalogued accurately and still behaves as a surprise later. The catalogue reduces how much is discovered in production, and keeping the old system running during replacement is what makes the remainder survivable.

Carrying a program like this one?

Tell us the system, the stakes, and the date that matters. You get a straight technical reply from the person who would lead the work, within 24 hours.

Bring us the program