Using DTRules to build a tax engine
Article — a worked case study. TaxReturn is a legacy sample kept for reference; the project's flagship example is now SinusitisTherapy, which runs as an interactive interview. This article uses the tax problem to illustrate how DTRules models complex, regulated logic — it is not a certified tax tool.
Income tax is a useful stress test for a rules engine. It is large (hundreds of interacting rules), regulated (every number traces back to a published source), and constantly revised (new brackets and limits every year). This article walks through how the TaxReturn sample models that complexity with DTRules — the same patterns apply to any domain where the logic is too intricate for if/else and too dynamic to hardcode.
The shape of the problem
A federal return follows Form 1040: gross income, deductions, taxable income, tax liability, credits, and a final balance, with state tax layered on top. Each step is its own decision and each maps to a specific form line and code section. That structure maps cleanly onto a tree of decision tables.
Input Data
→ Compute_Tax_Return (orchestrator, FIRST by filing status)
→ Calculate_Gross_Income (Form 1040 lines 1–9)
→ Calculate_Deductions (line 12)
→ Calculate_Taxable_Income (line 15)
→ Calculate_Tax_Liability (lines 16–24)
→ Calculate_Credits (lines 19–21)
→ Calculate_Final_Balance (lines 33–37)
→ Dispatch_State_Tax (if the state has an income tax) Orchestration with a top-level table
The entry table does almost no arithmetic itself. It branches on filing status and
performs each sub-calculation in order. Keeping the orchestrator thin means each
sub-table stays focused and independently testable.
<decision_table>
<table_name>Compute_Tax_Return</table_name>
<attribute_fields><Type>FIRST</Type></attribute_fields>
<conditions>
<condition>job.filing_status = "MFJ"</condition>
<condition>job.filing_status = "Single"</condition>
<condition>job.filing_status = "HOH"</condition>
<condition>otherwise</condition>
</conditions>
<actions>
<action>Calculate_Gross_Income</action>
<action>Calculate_Deductions</action>
<action>Calculate_Taxable_Income</action>
<action>Calculate_Tax_Liability</action>
<action>Calculate_Credits</action>
<action>Calculate_Final_Balance</action>
<action>if job.has_state_income_tax then Dispatch_State_Tax</action>
</actions>
</decision_table> Progressive brackets as a FIRST table
Tax brackets are a natural fit for a FIRST table: the conditions are ordered
thresholds, and only the first matching column fires. Each action encodes the base tax for the
bracket plus the marginal rate above the threshold.
<decision_table>
<table_name>Apply_Tax_Brackets_MFJ</table_name>
<attribute_fields><Type>FIRST</Type></attribute_fields>
<conditions>
<condition>result.taxable_income <= 23850</condition>
<condition>result.taxable_income <= 96950</condition>
<condition>result.taxable_income <= 206700</condition>
<condition>otherwise</condition>
</conditions>
<actions>
<action>set result.tax = result.taxable_income * 0.10</action>
<action>set result.tax = 2385 + (result.taxable_income - 23850) * 0.12</action>
<action>set result.tax = 11157 + (result.taxable_income - 96950) * 0.22</action>
<action>set result.tax = 35302 + (result.taxable_income - 206700) * 0.24</action>
</actions>
</decision_table> Constants live in the EDD
Bracket thresholds, deduction amounts, and credit limits are data, not logic. Storing them as EDD fields with default values means updating for a new tax year is a data edit, not a rewrite of every table — and the rules read naturally against named constants.
Per-state files keep contributors out of each other's way
State tax is where the project scales out. Each state owns two files —
XX_edd.xml for its constants and XX_dt.xml for its tables — and
Dispatch_State_Tax routes to the right calculator by job.state. Because
states never share a file, contributors can add or revise a state without merge conflicts.
sampleprojects/TaxReturn/xml/
TaxReturn_edd.xml # shared data model
TaxReturn_dt.xml # federal tables
states/
CA_edd.xml CA_dt.xml
CO_edd.xml CO_dt.xml
... # one pair per state Auditability comes for free
Because the logic is the table, a regulated calculation is self-documenting. Every table
carries comments citing its form line and code section, every decision can append to an
audit_trail array, and Git history ties each rule change back to the publication that
prompted it. That traceability is the reason decision tables have been used in production benefits
systems for two decades.
The build pipeline (v1.16.0)
Rules are authored in EL in the *_dsl tags and compiled to postfix by
dtrules build — Excel is the system of record, and the same DSL is written back to
Excel in the same operation. The removed dtrules compile command and
build --from-xml flag are no longer part of the workflow.
# Excel → XML, compile DSL → postfix
dtrules build
# Check Excel ↔ XML consistency and self-contained references
dtrules verify Takeaways
- Model the domain as a tree of focused tables, orchestrated by a thin entry table.
- Use FIRST tables for ordered-threshold logic like brackets.
- Keep constants in the EDD so yearly updates are data edits.
- Split independent units (states, services) into separate files to avoid conflicts.
- Lean on the engine's built-in auditability for anything regulated.
See also
- SinusitisTherapy — the flagship sample, applying the same patterns to clinical logic.
- The interactive interview — run a decision table as a conversation.
- Decision tables concept guide.
- Browse the TaxReturn source on GitHub .