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 ScoreIncomeLoan AmountDecisionNote
< 500rejectCredit too low
500–650< 30,000rejectInsufficient income
500–65030,000–60k≤ 50,000manual_reviewBorderline — needs review
500–650> 60,000≤ 100,000approveIncome compensates
651–750≤ 200,000approveStandard approval
> 750≤ 500,000approvePremium tier
> 750> 500,000manual_reviewLarge 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

BeforeAfter
Rules buried in codeRules in readable tables
Change = code + review + deployChange = edit table + run tests
Testing = manual QATesting = automated test cases
Audit = "grep the logs"Audit = click the session, see the trace
Rollback = revert commit + redeployRollback = select previous version
Business reads nothingBusiness 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.