Backend Development 30 min read

Evolution of a Risk Decision Engine: From Rule Sets to Drools to a Self‑Developed Engine

This article describes the progressive evolution of a consumer‑finance risk decision engine—from an initial rule‑set implementation, through a Drools‑based configuration, to a fully self‑developed micro‑service engine—detailing architectural changes, component designs, execution flow, operational challenges, and solutions such as empty‑run testing.

Yang Money Pot Technology Team
Yang Money Pot Technology Team
Yang Money Pot Technology Team
Evolution of a Risk Decision Engine: From Rule Sets to Drools to a Self‑Developed Engine

In consumer finance, risk decision making directly influences the loan process; a reasonable credit limit improves order conversion, while accurate overdue risk rating reduces collection pressure and user delinquency. The decision engine serves as the computation core, receiving user features, applying model analysis and rule logic, and outputting credit decisions.

We first illustrate a simple decision flow that only returns a credit result based on a whitelist check, student suspicion, and model scores, using user ID, age, address, occupation, ID and facial data as inputs.

if (用户处于白名单)
    return 授信通过
if (用户疑似学生)
    return 授信拒绝
if (用户模型分A处于[0, 0.2] && 用户模型分B处于[0, 0.5))
    return 授信通过
else 
    return 授信拒绝

If credit is approved, an SMS is sent and the ordering entry is opened; if rejected, a notification is sent and the entry is closed. Decision latency affects user retention, so fast and accurate decisions are essential.

First Generation: Rule‑Set Mode

Each if condition is analyzed as a Boolean rule. Complex conditions like “user suspected student” can be expressed either as a single rule combining multiple sub‑conditions with OR, or as a rule set where each sub‑condition is a separate rule. Model score checks can be modeled as Boolean rules or as rules that output a Double value, allowing threshold adjustments without changing the rule itself.

All rules are eventually wrapped as rule sets to unify configuration, resulting in a rule‑set based engine that stores configuration in JSON and executes actions based on rule outcomes.

{
  "states": [
    {
      "name": "WHITE_LIST",
      "actions": [
        {
          "conditions": [{"ruleSetId": 1, "operator": "EQ", "value": true}],
          "type": "ACCEPT",
          "data": {"credits": 1000}
        },
        {"conditions": [], "type": "CONTINUE", "next": "STUDENT_REJECT"}
      ]
    },
    {
      "name": "STUDENT_REJECT",
      "actions": [
        {
          "conditions": [{"ruleSetId": 2, "operator": "EQ", "value": true}],
          "type": "REJECT",
          "data": {"credits": 0}
        },
        {"conditions": [], "type": "CONTINUE", "next": "MODEL_SCORE"}
      ]
    },
    {
      "name": "MODEL_SCORE",
      "actions": [
        {
          "conditions": [
            {"ruleSetId": 3, "operator": "GE", "value": 0},
            {"ruleSetId": 3, "operator": "LE", "value": 0.2},
            {"ruleSetId": 4, "operator": "GE", "value": 0},
            {"ruleSetId": 4, "operator": "LT", "value": 0.5}
          ],
          "type": "ACCEPT",
          "data": {"credits": 3000}
        },
        {"conditions": [], "type": "REJECT", "data": {"credits": 0}}
      ]
    }
  ]
}

While this approach works for early stages, it suffers from configuration explosion, long release cycles, and difficulty in visualizing rule semantics.

Second Generation: Drools Configuration

To address JSON verbosity, Drools scripts are introduced, allowing richer expressions, arithmetic operations, and better readability. An example Drools rule extracts model scores from a Data object and decides credit limits based on score ranges.

import com.yqg.risk.core.drools.Data;
import java.lang.Math;

rule "ModelRule"
when
    d: Data();
then
    double modelScoreA = d.scoreRuleSet.get(3);
    double modelScoreB = d.scoreRuleSet.get(4);
    if (modelScoreA >= 0 && modelScoreA < 0.2 && modelScoreB >= 0 && modelScoreB < 0.5) {
        d.decision = "ACCEPT";
        d.credit = 10000;
    } else if (modelScoreA >= 0.2 && modelScoreA < 0.5 && modelScoreB >= 0 && modelScoreB < 0.5) {
        d.decision = "ACCEPT";
        d.credit = 8000;
    } else if (modelScoreA >= 0.5 && modelScoreA < 0.75 && modelScoreB >= 0 && modelScoreB < 0.5) {
        d.decision = "ACCEPT";
        d.credit = 6000;
    } else if (modelScoreA >= 0 && modelScoreA < 0.2 && modelScoreB >= 0.5 && modelScoreB <= 1) {
        d.decision = "ACCEPT";
        d.credit = 5500;
    } else if (modelScoreA >= 0.2 && modelScoreA < 0.5 && modelScoreB >= 0.5 && modelScoreB <= 1) {
        d.decision = "ACCEPT";
        d.credit = 3000;
    } else {
        d.decision = "REJECT";
        d.credit = 0;
    }
end

Drools improves readability and flexibility but still requires strategy staff to write code, which raises the entry barrier.

Third Generation: Self‑Developed Risk Engine

With rapid business growth, a hybrid rule‑set/Drools solution becomes insufficient. The new engine is micro‑service based, supports Boolean, Double, and additional output types, offers UI‑driven configuration, and unifies rule, rule‑set, decision table, scorecard, model, function, and decision‑flow components.

Variable Definition

Variables are either global (modifiable) or feature (read‑only after initialization). Types include integer, decimal, string, and boolean.

Component Classification

Seven component types are provided: rule, rule‑set, decision table, scorecard, model, function, and decision‑flow. Rules are simple expressions (IValue op IValue). Rule‑sets group rules with execution strategies. Decision tables support one‑dimensional and two‑dimensional mappings. Scorecards perform binning and weighted scoring. Models call external model services. Functions allow custom Groovy scripts with a _main entry point.

int _main(int products) {
    for (int p : [8388608, 268435456]) {
        if ((products & p) != 0) {
            products = products - p;
        }
    }
    if (products == 0) {
        return 8;
    }
    return products;
}

Decision‑Flow Execution

Decision‑flow nodes include start, component, branch, assignment, and end nodes. Nodes can be marked as interrupt or dry‑run, influencing subsequent execution. The engine interacts with a feature engine to fetch real‑time features and with model services for scores.

Trace states (INIT, RUN_WAITING, RUNNING, SUCCEED, FAILED, BLOCKED, EXCEPTION, CANCELED) track execution progress. The engine batches trace retrieval, executes components, and updates trace status.

System Issues and Solutions

Redundant component execution caused unnecessary resource consumption; the team introduced automatic flow simplification to prune unused components. Configuration anomalies (missing inputs, null features) led to execution failures; extensive pre‑testing and an “empty‑run” (dry‑run) mechanism were added. Empty‑run creates a shadow version of a decision flow, runs a limited number of traces before the version goes live, ensuring stability without affecting production.

Conclusion

The three‑generation evolution reflects business scaling: simple rule‑sets for early stages, Drools for intermediate complexity, and a self‑developed micro‑service engine for mature, high‑throughput risk decisioning. Continuous improvement aims to make the decision engine smarter, more efficient, and tightly aligned with business needs.

rule enginesoftware architectureMicroservicesdroolsdecision enginedecision flowrisk engine
Yang Money Pot Technology Team
Written by

Yang Money Pot Technology Team

Enhancing service efficiency with technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.