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.
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;
}
endDrools 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.
Yang Money Pot Technology Team
Enhancing service efficiency with technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.