Decision Tables Explained: From Spreadsheet to Production
Every codebase has that file. The one with 47 nested if/else blocks that encodes pricing logic, risk scoring, or approval routing. Everyone's afraid to touch it. Nobody fully understands it. And the business team that owns the logic can't even read it.
Decision tables fix this.
What Is a Decision Table?
A decision table maps inputs to outputs through a set of rules. Each row is a rule. Each column is either a condition (input) or an outcome (output).
Here's a loan eligibility table:
| Credit Score | Income | Loan Amount | Decision | Note |
|---|---|---|---|---|
| < 500 | — | — | reject | Credit too low |
| 500–650 | < 30,000 | — | reject | Insufficient income |
| 500–650 | 30,000–60k | ≤ 50,000 | manual_review | Borderline — needs review |
| 500–650 | > 60,000 | ≤ 100,000 | approve | Income compensates |
| 651–750 | — | ≤ 200,000 | approve | Standard approval |
| > 750 | — | ≤ 500,000 | approve | Premium tier |
| > 750 | — | > 500,000 | manual_review | Large loan — compliance req |
A compliance officer can read this. A developer can test this. A regulator can audit this.
The Code It Replaces
Here's what the same logic looks like embedded in application code:
func evaluateLoan(app Application) Decision {
if app.CreditScore < 500 {
return Decision{
Result: "reject",
Reason: "Credit score below minimum threshold",
}
}
if app.CreditScore <= 650 {
if app.Income < 30000 {
return Decision{Result: "reject", Reason: "Insufficient income"}
}
if app.Income <= 60000 && app.LoanAmount <= 50000 {
return Decision{Result: "manual_review", Reason: "Borderline case"}
}
if app.Income > 60000 && app.LoanAmount <= 100000 {
return Decision{Result: "approve", Reason: "Income compensates"}
}
}
if app.CreditScore <= 750 && app.LoanAmount <= 200000 {
return Decision{Result: "approve", Reason: "Standard approval"}
}
if app.CreditScore > 750 {
if app.LoanAmount <= 500000 {
return Decision{Result: "approve", Reason: "Premium tier"}
}
return Decision{Result: "manual_review", Reason: "Large loan"}
}
return Decision{Result: "reject", Reason: "No matching rule"}
}
Now imagine this function has 200 rules instead of 7. Imagine it was written by three different engineers over four years. Imagine the business wants to change a threshold from 650 to 680.
DMN: The Standard
DMN (Decision Model and Notation) is an industry standard for decision tables. TIATON implements DMN with extensions for versioning, testing, and observability.
A DMN table in TIATON is defined as structured data:
domain: lending
table: loan_eligibility
hit_policy: FIRST
inputs:
- name: credit_score
type: integer
- name: annual_income
type: integer
- name: loan_amount
type: integer
outputs:
- name: decision
type: string
- name: note
type: string
rules:
- when: { credit_score: "< 500" }
then: { decision: "reject", note: "Credit too low" }
- when: { credit_score: "[500..650]", annual_income: "< 30000" }
then: { decision: "reject", note: "Insufficient income" }
# ... remaining rules
Testing Before Deploy
Every table can have test cases:
{
"testCases": [
{
"name": "Low credit instant reject",
"input": { "credit_score": 420, "annual_income": 80000, "loan_amount": 10000 },
"expected": { "decision": "reject", "note": "Credit too low" }
},
{
"name": "Premium tier large loan needs review",
"input": { "credit_score": 790, "annual_income": 150000, "loan_amount": 600000 },
"expected": { "decision": "manual_review" }
}
]
}
Tests run before the version is published. If a test fails, the deploy is blocked. No more "let's hope the rule change works."
What Changes
| Before | After |
|---|---|
| Rules buried in code | Rules in readable tables |
| Change = code + review + deploy | Change = edit table + run tests |
| Testing = manual QA | Testing = automated test cases |
| Audit = "grep the logs" | Audit = click the session, see the trace |
| Rollback = revert commit + redeploy | Rollback = select previous version |
| Business reads nothing | Business reads the table directly |
The goal isn't to remove engineers from the process. It's to let engineers focus on systems — and let the business own their logic.
Next Steps
In the next article, we'll walk through building a complete decision domain with multiple tables, dependencies between them, and a testing strategy that catches rule conflicts before they reach production.