Software Engineering · Requirements and Design
Decision Tables and Rule Completeness in Policy Specifications
How a decision table exposes overlapping and missing rules in eligibility logic, why hit policies change behaviour, and what completeness costs as conditions multiply.
Eligibility logic is where requirements documents most often hide a hole. A paragraph states three conditions under which a discount applies, another paragraph states an exception, and a third states what happens when two of them hold at once. The prose is readable and the interactions are unstated. A decision table makes the interactions the content: rows are conditions, columns are rules, and every cell states whether a condition is required, forbidden, or irrelevant for that rule.
The form is old, predating structured programming, and it survives because the questions it forces are exactly the ones prose permits skipping. For each rule: which conditions matter, what value each must take, and what the outcome is. For each pair of rules: can they both apply, and if so which wins. For each combination of condition values: is there a rule, or is the combination unhandled.
Cells that do not say anything
A decision table cell has three states, not two. The condition must hold, the condition must not hold, or the condition is irrelevant to that rule, often written as a dash. The third state is what makes tables compact: a rule that does not depend on country of residence covers every country in one column rather than one column per country.
That compactness is also the source of a specific bug. A dash means “any value”, so a rule with a dash overlaps every rule that constrains the same condition. Two rules that both apply to a given input are a conflict, and whether the conflict is benign depends on the hit policy, which is the rule that decides what the table’s output is when several rules match.
The common hit policies are: first match in evaluation order, unique match required, any match accepted provided all matching rules agree on the output, and collect-all with an aggregation. Each is a different specification, and the choice changes behaviour on inputs that match more than one rule. A table with first-match semantics and a rule order that is not documented is a table whose behaviour cannot be determined by a reader, because the order is part of the specification and the columns do not carry it.
The DMN standard names these policies explicitly and gives the table a hit-policy attribute whose default is unique match, which is the useful part of the standardisation: it converts an implicit convention into a stated property. A table that relies on the default is stating a policy, whether or not the author intended to, and a table with first-match semantics whose rule order is not documented is incomplete in a way no amount of testing reveals, because the ambiguity only manifests on overlapping inputs.
Completeness is a coverage claim
A table is complete when every combination of input values matches at least one rule. That claim is checkable by counting: with $k$ binary conditions, there are $2^k$ combinations, and the rules must cover all of them. Dashes make the count harder to read but not harder to compute, since a rule with $d$ dashes covers $2^d$ combinations, so the coverage claim reduces to a sum over rules of $2^{d}$ against $2^k$.
Completeness is not the same as correctness, and the distinction matters when the table is reviewed. A complete table with a wrong outcome in one cell passes the coverage check and fails the requirement. Coverage answers “is there an answer for every input”, not “is the answer right”. Reviews tend to check the second and skip the first, which is why the hole usually appears in the combination nobody imagined: three conditions all false, or a condition whose value is unknown rather than false.
Unknown values deserve their own row. A boolean condition in a specification is usually three-valued in reality: the fact is true, the fact is false, or the fact is not known — the customer has not provided a document, the check has not run, the upstream system timed out. A table with two-valued conditions silently maps unknown to false, which is a policy decision about risk. For an eligibility rule, treating unknown as false denies benefits that might be owed; treating it as true grants benefits that might be fraudulent. The choice belongs in the table as an explicit column, not in the implementation as a default.
def matches(rule, facts):
# A cell value of None means "irrelevant"; False/True are constraints.
return all(cell is None or facts[name] == cell
for name, cell in rule.conditions.items())
def evaluate(table, facts):
hits = [r for r in table.rules if matches(r, facts)]
if not hits:
raise ValueError("no rule covers these facts") # completeness
if table.hit_policy == "unique" and len(hits) > 1:
raise ValueError("overlapping rules under unique policy")
return hits[0].outcome # first-match ordering, documented
The two error paths are the point of the code: an uncovered input and an ambiguous input are different defects, and collapsing them into a single default outcome is how tables acquire behaviour nobody specified.
Consistency, and the cost of proving it
A table is consistent when no two rules with different outcomes can both apply. Consistency is a satisfiability question: for each pair of rules, whether some assignment of input values satisfies both rules’ constraints while their outcomes disagree. With $k$ conditions and $n$ rules, the naive check is $O(n^2)$ pairs, each a conjunction of at most $2k$ literals.
For small tables this is trivial and worth doing exhaustively. For large ones it is not: the pair count grows quadratically and the tables in policy domains grow because the domain does. Three practical reductions apply.
The first is to order rules by specificity and make the ordering part of the specification, so that overlap is resolved by priority rather than forbidden. This is the first-match policy, and it converts a consistency proof into a review of ordering, which humans do better than satisfiability checking.
The second is to structure the table into a decision tree: test one condition, branch, then test the next. A tree with $k$ levels has $k$ decisions on any path rather than $2^k$ cells, and its completeness is checkable by inspecting each internal node for a missing branch. The trade is that the tree form makes it harder to see that two distant leaves apply the same outcome, which matters when a policy is stated as a set of exceptions.
The third is to accept incompleteness and specify a default. A table with an explicit fall-through outcome is complete by construction, and the fall-through is a documented policy rather than an accidental one. This is the only option that scales without either a proof or a restructure, and it should be chosen deliberately.
Where tables stop being the right notation
A decision table assumes the outcome depends on a fixed set of conditions evaluated independently. When conditions interact — when the value of one is only meaningful given another — the table has to encode the interaction as an extra condition, and the condition count grows with the number of valid combinations rather than with the number of facts.
The second limit is temporal. A table evaluates a snapshot; a policy that depends on history, such as “the discount applies only if it has not been applied in the previous twelve months”, needs state that the table does not carry. Encoding that state as a condition is possible and pushes the problem into the caller, which is why such rules usually belong in a state machine with an explicit history, or in a specification whose state includes the prior applications.
The third limit is the review itself. A table with twenty conditions and forty rules is not readable in a meeting, and a specification nobody can review is not more precise than prose — it is precise in a notation that the people who own the policy cannot check. The useful size is the size at which the domain expert can point at a column and say whether it is right. Beyond that, the table’s value is as a generated artefact from a smaller source, and the source is what gets reviewed. A condition whose wording carries an unresolved quantifier scope problem survives the translation into a table unchanged, and the table then encodes the ambiguity with more confidence than the prose did.
What the table reliably delivers is the elimination of a class of defect: the unhandled combination and the unstated overlap. Both are invisible in prose and both are cheap to find in a table, which is why the notation persists in domains where the rules are the product. The remaining defect class — the outcome that is wrong for every input it covers — is not addressed by any notation, and is the part that refinement can only detect against a specification that was right to begin with.
References
- Decision Model and Notation (DMN) — Object Management Group
- Code Complete, second edition — Steve McConnell, Microsoft Press, 2004
- Principles of Model Checking — Baier and Katoen, MIT Press, 2008