Decoupling Complex Business Logic with a Custom Rule Engine
This article explains how a lightweight, Groovy‑based rule engine can decouple complex ticketing business logic from core systems, outlines its architecture, implementation details, performance optimizations, and provides code examples and benchmark results.
Complex business logic in ticketing systems leads to high maintenance cost; a rule engine is proposed to decouple logic.
The rule engine works in three steps: accept data, interpret rules, make decisions, allowing separation of rules from the core system.
Typical use cases include risk control, real‑time recommendation, lightweight offline calculations, and any scenario requiring decoupled complex logic.
Existing open‑source engines (Drools) and commercial solutions (ILOG JRules) were evaluated; due to steep learning curves and poor fit, a lightweight custom engine was built.
Key implementation goals: define rule language semantics (Groovy), dynamic rule condition editing, script editing, cluster‑wide publishing, and automated execution.
Challenges include rule priority, conflict avoidance, and designing a comprehensive rule‑management model.
Architecture: rules are stored with metadata (name, code, description, content, priority, status) and executed by loading Groovy scripts via GroovyClassLoader, caching them in ConcurrentHashMap, and invoking them with a Binding.
Performance optimizations: cache Script objects, use separate GroovyClassLoader per script, avoid frequent class generation that causes GC pressure and OOM.
Example code for rule initialization, caching, and execution is shown below.
public GroovyRuleScript(@NonNull RuleScriptItemDTO ruleScriptItem) { this.createTime = System.currentTimeMillis(); GroovyClassLoader classLoader = new GroovyClassLoader(); Class
clazz = classLoader.parseClass(ruleScriptItem.getScript()); this.scriptClass = clazz; this.scriptId = ruleScriptItem.getScriptId(); this.name = ruleScriptItem.getName(); classLoader = null; } ConcurrentHashMap
ruleScriptMaps = new ConcurrentHashMap<>(16); ConcurrentHashMap
shuntScriptMaps = new ConcurrentHashMap<>(16); ConcurrentHashMap
postScriptMaps = new ConcurrentHashMap<>(16); for (RuleScriptItemDTO scriptItem : scriptResponse.getData()) { try { if (Constants.RULE_TYPE.equals(scriptItem.getType())) { ruleScriptMaps.put(scriptItem.getScriptId(), new GroovyRuleScript(scriptItem)); } else if (Constants.SHUNT_TYPE.equals(scriptItem.getType())) { shuntScriptMaps.put(scriptItem.getScriptId(), new GroovyRuleScript(scriptItem)); } else if (Constants.POST_TYPE.equals(scriptItem.getType())) { postScriptMaps.put(scriptItem.getScriptId(), new GroovyRuleScript(scriptItem)); } } catch (Exception e) { log.error("[RuleStrategyRepository] init script error:{}", e.getMessage(), e); } } ruleScriptPool.reload(ruleScriptMaps); shuntScriptPool.reload(shuntScriptMaps); postScriptPool.reload(postScriptMaps); Object execute(Binding binding) throws ExecuteScriptException;Benchmarks show peak TPS of 29,900, with 99.9% of rule executions completing within 10 ms, 99.99% within 50 ms, and 99.999% within 500 ms.
The article concludes with UML class and sequence diagrams illustrating the engine.
Tongcheng Travel Technology Center
Pursue excellence, start again with Tongcheng! More technical insights to help you along your journey and make development enjoyable.
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.