Building Decision Tables for Complex Business Rules
Most examples of decision tables use two or three conditions. Real business rules rarely do. Insurance eligibility, loan approval, tax calculation, shipping logic — these systems have five, six, seven conditions, each interacting with the others. The naive approach — enumerate every combination — produces tables with 128 or 256 columns that nobody can review, maintain, or derive useful tests from.
This post covers the techniques for making decision tables tractable at real-world scale: collapsing redundant rules, handling don't-care conditions properly, splitting tables by concern, and working through a complete shipping rules example.
The Scale Problem
Start with the math. For N binary conditions:
| Conditions | Max rules |
|---|---|
| 2 | 4 |
| 3 | 8 |
| 4 | 16 |
| 5 | 32 |
| 6 | 64 |
| 7 | 128 |
| 8 | 256 |
At 5 conditions, 32 rules is already borderline — it fits on one page but barely. At 7 conditions, 128 rules is completely unmanageable. The techniques below bring these numbers to something workable.
Technique 1: Identify Dominant Conditions
Not all conditions are equal. Some conditions, when they take a particular value, determine the outcome regardless of everything else.
Example: in a loan application, if the applicant has an active bankruptcy, the answer is always "decline" — full stop. No need to enumerate all combinations of credit score, income, and debt ratio when bankruptcy is Y.
Identify these "knockout" conditions first. For each one, add a single collapsed rule with that condition as Y and everything else as —. Then enumerate the remaining rules only for the case where the knockout condition is N.
This alone can cut your table size dramatically.
Technique 2: Collapse Redundant Rules
Two rules are candidates for collapsing when:
- They differ in exactly one condition
- All their action entries are identical
When both conditions hold, replace the two rules with one, marking the differing condition as — (don't care).
Example:
| R1 | R2 | |
|---|---|---|
| Credit score >= 700 | Y | Y |
| Income >= $50k | Y | N |
| Bankruptcy flag | N | N |
| Result: Approve | X | X |
R1 and R2 have identical actions and differ only in "Income >= $50k". Collapse to:
| R (collapsed) | |
|---|---|
| Credit score >= 700 | Y |
| Income >= $50k | — |
| Bankruptcy flag | N |
| Result: Approve | X |
Repeat this process iteratively. After one round of collapsing, you may find new candidates that weren't adjacent before.
Caution: Only collapse rules when both conditions hold simultaneously. Don't collapse rules just because they look similar — verify the actions match exactly. And be aware that collapsing can obscure boundary conditions, so use BVA alongside the table for numeric thresholds.
Technique 3: Split Tables by Decision Point
Large tables are often trying to do too much. If your system makes a sequence of decisions, split it into separate tables — one per decision point.
Example: a loan approval process has two stages:
- Pre-qualification (income, credit score, bankruptcy)
- Rate determination (for approved applicants: DTI ratio, loan term, property type)
Don't build one giant table covering all stages. Build two smaller tables and chain them: the action of table 1 ("qualified" / "declined") becomes a condition in table 2.
This keeps each table under 4-5 conditions and makes the overall logic far more readable. It also makes it easier to see which tests cover which stage of processing.
Technique 4: Use Extended Entry for Multi-Value Conditions
When a condition has more than two meaningful states, limited entry tables (Y/N) force you to split it into multiple binary conditions. Extended entry tables handle this directly.
Instead of:
- C1: Age >= 18 (Y/N)
- C2: Age >= 65 (Y/N)
Use:
- C1: Age tier (Under 18 / 18-64 / 65+)
This single extended-entry condition replaces two binary conditions, halving that dimension of the table. The tradeoff is slightly more complex table structure — you can no longer mechanically check completeness by counting 2^N rules.
Worked Example: E-Commerce Shipping Rules
A realistic shipping rules engine with these conditions:
- Member tier: Gold, Silver, None
- Order total: Under $50 / $50–$99 / $100+
- Shipping speed selected: Standard / Express / Overnight
- Product type: Regular / Oversized / Hazmat
The possible actions:
- Free shipping
- Standard rate ($5.99)
- Express rate ($12.99)
- Overnight rate ($24.99)
- Oversized surcharge (+$15)
- Hazmat surcharge (+$25)
- Express/Overnight not available (hazmat restriction)
Step 1: Identify Knockouts
Hazmat products can only ship Standard — Express and Overnight are not available regardless of member tier or order total. Add this as a collapse rule first:
| Knockout Rule | |
|---|---|
| Product type | Hazmat |
| Member tier | — |
| Order total | — |
| Speed selected | Express OR Overnight |
| Action | Show "Not available" error |
And:
| Hazmat Standard | |
|---|---|
| Product type | Hazmat |
| Member tier | — |
| Order total | — |
| Speed selected | Standard |
| Action | Standard rate + $25 hazmat surcharge |
Two rules cover the entire hazmat case. Remove hazmat from the remaining table.
Step 2: Handle Oversized Products
Similarly, oversized products always add a $15 surcharge on top of whatever shipping rate applies. Rather than duplicating every rule for oversized vs regular, represent this as a modifier:
Note in table: all rules apply +$15 oversized surcharge when product type = Oversized.
This is a table annotation, not a separate column. It keeps the core logic table clean.
Step 3: Core Shipping Rate Table (Regular Products)
Now with product type simplified away, three conditions remain: member tier (3 values), order total (3 values), speed (3 values) = 27 combinations maximum.
| Free | Standard ($5.99) | Express ($12.99) | Overnight ($24.99) | |
|---|---|---|---|---|
| Gold + Any total + Standard | X | |||
| Gold + Any total + Express | X | |||
| Gold + Any total + Overnight | X | |||
| Silver + $100+ + Standard | X | |||
| Silver + $50–99 + Standard | X | |||
| Silver + Under $50 + Standard | X | |||
| Silver + Any total + Express | X | |||
| Silver + Any total + Overnight | X | |||
| None + $100+ + Standard | X | |||
| None + $50–99 + Standard | X | |||
| None + Under $50 + Standard | X | |||
| None + Any total + Express | X | |||
| None + Any total + Overnight | X |
This table has 13 rules instead of 27. The collapses:
- Gold tier gets free standard shipping regardless of order total (3 rules → 1)
- Silver and None tiers: Express and Overnight rates are the same regardless of order total (6 rules → 2 each)
Step 4: Review for Gaps
Read through every rule. Ask: is there a realistic state I haven't covered?
For this table, we haven't specified what happens if someone selects Express with a $100+ order as a None-tier customer. Looking at the table: "None + Any total + Express" → $12.99. That's covered by the collapsed rule.
What about Gold + $100+ vs Gold + under $50 for Standard shipping? The collapsed "Gold + Any total + Standard" → free covers both.
Step 5: Derive Test Cases
From 13 rules, derive 13 test cases minimum. For collapsed rules (those with —), pick representative values:
- "Gold + Any total + Standard": test with total = $30 (under $50) — tests the most interesting case where a non-Gold user would pay
- "Gold + Any total + Standard": add a second test with total = $150 to cover boundary if needed
Add the hazmat and oversized tests from steps 1 and 2. Total: ~18 test cases for a complete shipping rules test suite.
Dealing with "Don't Care" Conditions in Tests
When a rule has a — condition, you have flexibility — but use it deliberately:
- Pick the most interesting value: the one that would break a naive implementation. If a competitor's logic would charge for shipping, pick the value where your system should give it free.
- Pick boundary values: if the condition is numeric and the don't-care is because it doesn't matter, test at the boundary to confirm it really doesn't matter.
- Don't always pick the same default value: rotating through different don't-care values across rules provides incidental coverage of more combinations.
Reviewing the Table for Quality
Before deriving test cases, review the completed table with these checks:
Completeness check: Are there any combinations not covered by any rule? For fully enumerated tables, count the rules (after expanding collapsed ones) and verify it totals to 2^N.
Contradiction check: Are there any two rules with identical condition entries that have different actions? That's a specification error.
Redundancy check: Are there two rules with identical conditions AND identical actions? One is redundant — merge or remove.
Feasibility check: Are there rules with condition combinations that can never occur? Remove them or add an "impossible" annotation rather than testing them.
When Tables Get Too Big: Know Your Limit
If after applying all these techniques you still have more than 20-25 rules, consider whether the table is trying to capture a single cohesive business rule or multiple separate rules bundled together. Almost always it's the latter. Split the table.
A decision table with 8 rules that you can fit on a whiteboard and review with a business analyst in 10 minutes is worth ten times more than a 64-rule table that nobody reads. Comprehensibility is a quality criterion for test design artifacts, not just for code.
The goal is a table that a developer, QA engineer, and product manager can all look at together and agree on — before a single test is written or a single line of code is committed.