Skip to main content

Stateful Decisioning in Fintech: Beyond Standard DMN

· 5 min read
Richard Bízik
Creator of QuantumDMN

In the high-stakes world of Fintech—whether it's real-time payment processing, loan origination, or fraud detection—decisions can rarely be made in isolation. A purely stateless "Yes/No" often isn't enough. You need context. You need history. You need Stateful Decisioning.

In this post, we'll demonstrate how QuantumDMN solves a classic Fintech problem that traditional stateless DMN engines struggle with: Velocity Checks.

The Problem: The Velocity Trap

Imagine you are building a Buy Now, Pay Later (BNPL) engine. You have a simple risk rule:

"Approve the loan if the user's risk score is low."

In a standard DMN engine (like Camunda or Drools), this is easy. You send the user's data, the engine calculates a score, and returns Approved.

But what if a fraudster requests 50 small loans in 5 minutes? Each individual loan might be for a small, unsuspicious amount like $50.

A stateless engine sees each request as a brand new event.

  • Request 1: $50, Risk Low -> Approved
  • Request 2: $50, Risk Low -> Approved
  • ...
  • Request 50: $50, Risk Low -> Approved

By the time you notice, the fraudster has walked away with $2,500. To stop this, you usually have to query your database before calling the decision engine, effectively "leaking" business logic into your application code.

The Solution: QuantumDMN KPIs

QuantumDMN treats Performance Indicators (KPIs) as first-class citizens. It remembers the history of decisions (the "Ledger") and allows you to build rules based on Time Windows.

Let's model a Velocity Check that automatically rejects a loan if the user has made more than 5 requests in the last 5 minutes.

The KPI Definition

We create a single KPI that tracks request counts using a time window:

<performanceIndicator id="kpi_velocity" name="VelocityCheck" window="5m">
<aggregation>
<aggregator name="requestCount" function="COUNT" field="counter"/>
</aggregation>
<context>
<contextEntry>
<variable name="counter" typeRef="number" />
<literalExpression><text>1</text></literalExpression>
</contextEntry>
</context>
</performanceIndicator>

First we define the KPI metrics that should be collected. In this case we just count the number of requests.

Velocity Check KPI

Then we define the aggregation function that should be applied to the collected metrics.

Velocity Check KPI aggregation

This tells the engine: "Every time this KPI is evaluated, record a value of 1. Count all values from the last 5 minutes."

The Decision

Now we use the KPI in our decision logic:

<decision id="dec_loan" name="LoanApproval">
<informationRequirement><requiredInput href="#input_risk_score"/></informationRequirement>
<informationRequirement><requiredPerformanceIndicator href="#kpi_velocity"/></informationRequirement>
<literalExpression><text>
if VelocityCheck.requestCount > 5 then "REJECT_VELOCITY_LIMIT"
else if RiskScore > 80 then "REJECT_RISK"
else "APPROVED"
</text></literalExpression>
</decision>

Now, when the fraudster attempts request #6 within 5 minutes, the engine automatically sees the history and rejects the request. No external database query needed.

Advanced: Per-Account Spending Limits

The example above tracks velocity globally. But what if you need to track spending per account over a 24-hour window? For example, enforcing daily spending limits or detecting rapid transaction patterns for the same account.

Dynamic Key Aggregation

QuantumDMN supports dynamic key aggregation using the context put function. This allows you to aggregate values grouped by a dynamic key (like AccountId).

Complete Working Example

Advanced Per-Account Spending

Here is the full DMN XML you can import and test:

Download decision.xml

The KPI Definition

The AccountSpending KPI tracks spending over a 24-hour window with multiple aggregators:

<performanceIndicator id="kpi_account_spending" name="AccountSpending" window="24h">
<aggregation>
<aggregator name="totals" function="SUM" field="context"/>
<aggregator name="counts" function="COUNT" field="context"/>
<aggregator name="maxTx" function="MAX" field="context"/>
</aggregation>
<context>
<contextEntry>
<variable name="context" typeRef="Any" />
<literalExpression><text>context put({}, AccountId, Amount)</text></literalExpression>
</contextEntry>
</context>
</performanceIndicator>

The Decision Table

The TransactionDecision uses a decision table with FIRST hit policy to evaluate multiple conditions:

Advanced Rules Configuration

The rules check:

  1. Daily limit exceeded: If total spending for the account exceeds the DailyLimit input
  2. Too many transactions: If the account has more than 10 transactions in 24 hours
  3. Default approval: If neither condition is met

How It Works

  1. context put({}, AccountId, Amount) creates a context like {"ACC-123": 500} using AccountId as the key
  2. Each evaluation stores this context in the ledger with a timestamp
  3. The aggregation merges all contexts by key:
    • totals (SUM): Total spending per account
    • counts (COUNT): Number of transactions per account
    • maxTx (MAX): Largest single transaction per account
  4. The decision table uses get value(AccountSpending.totals, AccountId) and get value(AccountSpending.counts, AccountId) to lookup the current account's aggregated values

Example Scenario

EvaluationAccountIdAmountDailyLimitAccountSpending.totalsDecision
1ACC-1235001000{ACC-123: 500}APPROVED
2ACC-1233001000{ACC-123: 800}APPROVED
3ACC-4562001000{ACC-123: 800, ACC-456: 200}APPROVED
4ACC-1234001000{ACC-123: 1200}REJECT_DAILY_LIMIT_EXCEEDED

Additionally, if an account makes more than 10 transactions within 24 hours, it will be rejected with REJECT_TOO_MANY_TRANSACTIONS.

This pattern enables sophisticated fraud detection scenarios like:

  • Per-account daily limits: Reject if account spending exceeds threshold
  • Per-account velocity: Track transaction counts per account
  • Multi-dimensional analysis: Combine with MIN, MAX, AVG aggregators

We can test the behaviour using evaluate button in the modeler:

Advanced Per-Account Evaluation

Conclusion

By moving state into the decision model, we've simplified our architecture.

  1. No external database queries needed for the velocity check.
  2. Logic is centralized in the DMN model, not scattered in code.
  3. Real-time protection against rapid-fire fraud attempts.
  4. Per-entity tracking with dynamic key aggregation.

This is the power of Quantitative Decisioning. It's not just about rules; it's about the broader context of your business operations.