Skip to content

ADR 0008 Decision JSON DSL

Tiana_ edited this page May 30, 2026 · 1 revision

ADR-0008: JSON DSL for decision engine rules

Status: Accepted Date: 2026-04-25 Decider: Maintainer

Context

FinCore's Decision Engine evaluates rules deterministically over input contexts to produce decisions (APPROVE/REJECT/REVIEW). Use cases:

  • Pre-payment risk screening
  • AML transaction monitoring
  • KYC routing
  • Pre-credit approvals (no ML)
  • Feature flagging / segmenting

Choices for rule expression:

  1. JSON DSL - declarative, structured, machine-parsable, version-controllable
  2. Embedded scripting (JavaScript, Groovy, Python) - flexible but unsafe and slow
  3. Custom DSL (like Drools' .drl, Formance's numscript) - bespoke syntax, learning curve
  4. DMN (Decision Model and Notation) - OMG standard, XML-heavy
  5. Drools KIE - full BRMS, heavyweight

For modern fintech that wants:

  • Operators (compliance, risk) editing rules without code deploys
  • Rules in version control / GitOps (Compliance-as-Code)
  • Audit-friendly evaluation logs
  • LLM-friendly synthesis ("translate this English to a rule")

JSON wins. It's the closest format to "data" - readable, parseable by any tool, valid in every language.

Decision

Use a typed JSON DSL with the following grammar:

{
  "id": "rule-uuid",
  "ruleSetId": "payment-screening",
  "priority": 100,
  "terminate": false,
  "definition": {
    "conditions": {
      "all": [
        { "field": "amount.amount", "op": ">", "value": 10000 },
        { "field": "destination.country", "op": "in", "value": ["NG", "PK", "RU"] },
        {
          "any": [
            { "field": "user.ageDays", "op": "<", "value": 90 },
            { "field": "user.kycStatus", "op": "!=", "value": "APPROVED" }
          ]
        }
      ]
    },
    "action": {
      "decision": "REVIEW",
      "reason": "high_amount_high_risk_country_or_unverified"
    }
  }
}

Operators supported:

  • Comparison: =, !=, <, <=, >, >=
  • Set: in, not_in, contains
  • Pattern: matches (regex), starts_with, ends_with
  • Existence: is_null, is_not_null
  • Logical: all, any, none (nested)

Field paths use dotted JSONPath-lite (amount.amount, user.kycStatus). Resolved against input context object at evaluation time.

Rules are validated server-side at insert time:

  • Schema-level: structural (every condition has field, op, value; logical wrappers have valid children)
  • Type-level: op matches value type (> requires numeric, in requires array)
  • Reference-level: field paths exist in the declared RuleSet's context schema

Consequences

Positive

  • Operator-editable: a non-developer compliance officer can write rules with a JSON editor
  • Version-controllable: rules in git, PR reviews, GitOps deployment (Compliance-as-Code Killer Feature)
  • LLM-friendly: GPT/Claude can synthesize rules from natural language easily
  • Audit-friendly: decision_logs stores the matched rule's full JSON for replay
  • Type-safe: strict server-side validation rejects malformed rules at write time
  • Cross-language: any language can serialize/deserialize JSON; rules can be embedded in YAML, environment configs, etc.
  • Testable: rule + context = deterministic decision, trivially unit-testable

Negative

  • Verbose: deeply nested rules get noisy. Mitigated by tooling (rule visualizer in operator UI, Phase E v0.4)
  • Limited expressiveness: complex math (e.g., velocity over time windows) needs context preparation outside the engine
  • No turing completeness: can't loop, can't recurse. By design - keeps evaluation fast and predictable.

Neutral

  • DMN proponents will critique. DMN is great for some industries; for fintech it's overkill (and the XML hurts)
  • Drools veterans may want full BRMS. Their use case is not v0.1's target.

Alternatives considered

Embedded JavaScript (Nashorn / GraalVM)

  • Rejected: security risk (sandboxing JS is hard)
  • Rejected: non-deterministic timing (engine pauses for GC)
  • Rejected: harder to log "what evaluated to what"

Drools (.drl files)

  • Rejected: heavyweight, requires KIE Workbench / Guvnor for editing
  • Rejected: alien syntax for Kotlin/Java natives
  • Rejected: pulls in lots of dependencies
  • Genuinely strong for complex BRMS use cases - out of v0.1 scope

DMN 1.5 (XML-based standard)

  • Considered: standardized, tooling exists (Camunda DMN Modeler)
  • Rejected: XML, too heavy for our use cases
  • May add DMN compatibility layer if customer demand emerges (Y2)

YAML DSL

  • Considered: more human-readable than JSON
  • Rejected: less ecosystem support; JSON wins for API serialization
  • Note: rules can be stored as YAML in git (Compliance-as-Code) and converted to JSON server-side. We allow both ingest formats.

CEL (Common Expression Language, Google's)

  • Considered: typed, fast, sandboxed
  • Rejected: less familiar to fintech developers
  • Rejected: requires CEL parser dependency in every language

Custom Kotlin DSL

  • Considered: most expressive
  • Rejected: requires recompilation to add rules; fails operator-edit goal
  • Rejected: not language-agnostic (can't use TypeScript, Python adopters)

Validation

  • Schema validation: 100% of valid rules accepted, 100% of malformed rules rejected with field-level error
  • Performance: 100-rule evaluation p99 < 10ms, 1000-rule evaluation p99 < 50ms
  • Determinism: same input + same active rules → same output, byte-for-byte
  • LLM synthesis: feed natural language, parse output, verify it passes server-side validation in 95%+ of generated cases

Related

Clone this wiki locally