Boost Business Agility with a Spring Boot Dynamic Rule Engine
This article explains how backend developers can replace hard‑coded business logic with a dynamic rule engine built on Spring Boot and QLExpress, enabling rapid rule changes, reducing deployment risk, and empowering non‑technical staff to manage complex e‑commerce, finance, and marketing scenarios.
Introduction
As a backend developer you often need to change business rules quickly: operations, product managers, and executives request rule adjustments that traditionally require code changes, rebuilding, redeployment, and service restarts, causing delays and risk.
Business Pain Points
Traditional Approach
In traditional systems business rules are hard‑coded in the code base.
// Traditional hard‑coded method
public BigDecimal calculateVipDiscount(String userLevel, BigDecimal price) {
if ("GOLD".equals(userLevel)) {
return price.multiply(new BigDecimal("0.8")); // 20% off
} else if ("SILVER".equals(userLevel)) {
return price.multiply(new BigDecimal("0.9")); // 10% off
}
return price; // no discount
}Slow response : rule changes require a full development cycle.
High risk : each deployment may affect the entire system.
High cost : coordination among development, testing, and operations.
Inflexible : cannot react quickly to market changes.
Real‑world Scenarios
E‑commerce : frequent promotion adjustments, A/B testing, competitor price changes.
Finance : real‑time risk model tuning, interest‑rate policy updates, audit rule optimization.
Content platforms : recommendation algorithm tweaks, content moderation rule updates, user‑level adjustments.
Solution: Dynamic Rule Engine
Core Idea
The engine separates business rules from business code, storing rules as scripts that are executed at runtime.
Business code : handles workflow and data processing.
Business rules : stored as independent scripts, executed dynamically.
Traditional: business logic = workflow + hard‑coded rules
Dynamic: business logic = workflow + scripted rulesTechnology Stack
Rule engine: QLExpress (Alibaba open‑source)
Backend framework: Spring Boot
Frontend: HTML5 + TailwindCSS
Core Implementation
1. Project Structure
springboot-dynamic-rule/
├── entity/
│ ├── RuleScript.java // rule entity
│ ├── OrderProcessResult.java // order result
│ └── RuleExecuteResponse.java // rule execution response
├── service/
│ ├── DynamicRuleEngine.java // rule engine core
│ └── OrderService.java // business service
├── controller/
│ ├── RuleController.java // rule management API
│ └── OrderController.java // business API
└── resources/
└── static/
├── index.html // rule management page
└── business.html // business demo page2. Rule Engine Core
@Slf4j
@Service
public class DynamicRuleEngine {
private final Map<String, RuleScript> ruleCache = new ConcurrentHashMap<>();
private final ExpressRunner expressRunner = new ExpressRunner();
@PostConstruct
public void init() { initDefaultRules(); }
private void initDefaultRules() {
// VIP discount rule
addRule(new RuleScript("vip_discount",
"if (userLevel == \"GOLD\") { return price * 0.8; } " +
"else if (userLevel == \"SILVER\") { return price * 0.9; } " +
"else { return price; }",
"VIP discount rule"));
// Full‑reduction rule
addRule(new RuleScript("full_reduction",
"if (totalAmount >= 200) { return totalAmount - 50; } " +
"else if (totalAmount >= 100) { return totalAmount - 20; } " +
"else { return totalAmount; }",
"Full‑reduction rule"));
}
public RuleExecuteResponse executeRule(String ruleName, Map<String, Object> params) {
try {
RuleScript rule = ruleCache.get(ruleName);
if (rule == null || !rule.isEnabled()) {
return RuleExecuteResponse.error("Rule not found or disabled: " + ruleName);
}
DefaultContext<String, Object> context = new DefaultContext<>();
if (params != null) { params.forEach(context::put); }
Object result = expressRunner.execute(rule.getScript(), context, null, true, false);
log.info("Executed rule: {}, result: {}", ruleName, result);
return RuleExecuteResponse.success(result);
} catch (Exception e) {
log.error("Rule execution failed: {}", ruleName, e);
return RuleExecuteResponse.error("Execution failed: " + e.getMessage());
}
}
}3. Business Service Integration
@Service
@RequiredArgsConstructor
public class OrderService {
private final DynamicRuleEngine ruleEngine;
public OrderProcessResult processOrder(Order order) {
List<OrderProcessResult.ProcessStep> steps = new ArrayList<>();
BigDecimal currentAmount = order.getOriginalAmount();
// Apply VIP discount
BigDecimal discounted = applyVipDiscount(order, currentAmount, steps);
// Apply full‑reduction
BigDecimal finalAmount = applyFullReduction(order, discounted, steps);
// Calculate points (omitted)
return new OrderProcessResult(/* build result */);
}
private BigDecimal applyVipDiscount(Order order, BigDecimal amount, List<OrderProcessResult.ProcessStep> steps) {
Map<String, Object> params = new HashMap<>();
params.put("userLevel", order.getUserLevel());
params.put("price", amount);
RuleExecuteResponse resp = ruleEngine.executeRule("vip_discount", params);
if (resp.isSuccess()) {
BigDecimal result = new BigDecimal(resp.getResult().toString());
steps.add(new OrderProcessResult.ProcessStep("VIP discount",
"Applied discount for level " + order.getUserLevel(), amount, result, amount.subtract(result), "vip_discount"));
return result;
}
return amount;
}
}4. REST API
@RestController
@RequestMapping("/api/rules")
@RequiredArgsConstructor
public class RuleController {
private final DynamicRuleEngine dynamicRuleEngine;
@GetMapping
public ResponseEntity<List<RuleScript>> getAllRules() {
return ResponseEntity.ok(dynamicRuleEngine.getAllRules());
}
@PostMapping
public ResponseEntity<String> addRule(@RequestBody RuleScript ruleScript) {
try {
dynamicRuleEngine.addRule(ruleScript);
return ResponseEntity.ok("Rule added successfully");
} catch (Exception e) {
return ResponseEntity.badRequest().body("Add failed: " + e.getMessage());
}
}
@PutMapping("/{ruleName}")
public ResponseEntity<String> updateRule(@PathVariable String ruleName, @RequestBody RuleScript ruleScript) {
try {
dynamicRuleEngine.updateRule(ruleName, ruleScript);
return ResponseEntity.ok("Rule updated successfully");
} catch (Exception e) {
return ResponseEntity.badRequest().body("Update failed: " + e.getMessage());
}
}
@PostMapping("/execute/{ruleName}")
public ResponseEntity<RuleExecuteResponse> executeRule(@PathVariable String ruleName, @RequestBody Map<String, Object> params) {
RuleExecuteResponse resp = dynamicRuleEngine.executeRule(ruleName, params);
return ResponseEntity.ok(resp);
}
}Operation UI
The demo provides a simple UI for rule management and business scenario simulation.
Rule Management Page
Rule list: displays all rules and their status.
Online editing: modify rule scripts directly in the browser.
Real‑time testing: test rule effects immediately after changes.
Status control: enable or disable rules with a single click.
Business Demo Page
Order simulator: input order data and view processing results.
Detailed steps: clearly show the execution process of each rule.
History: record past processing outcomes.
Application Scenarios
Scenario 1: E‑commerce Dynamic Pricing
Requirement: calculate discount based on user level and order amount.
// Hard‑coded version
if ("GOLD".equals(userLevel) && amount >= 100) { return amount * 0.8; } // Scripted version
if (userLevel == "GOLD" && totalAmount >= 100) { return totalAmount * 0.8; }
else if (userLevel == "SILVER" && totalAmount >= 50) { return totalAmount * 0.9; }
else { return totalAmount; }Speed: rule changes from hours to minutes.
Risk: from system‑wide deployments to isolated rule updates.
Barrier: business users can modify rules without developers.
Scenario 2: Real‑time Risk Control
Requirement: adjust audit strategy according to live risk metrics.
// Risk scoring script
score = baseScore;
if (userAge < 18) { score -= 20; }
if (creditLevel == "HIGH") { score += 30; }
if (monthlyIncome > 10000) { score += 15; }
return score >= 60 ? "PASS" : "REJECT";Fast response to market risk changes.
Fine‑grained control for different scenarios.
Supports A/B testing of multiple strategies.
Scenario 3: Marketing Campaign Configuration
Requirement: flexible rules for complex promotions.
// Double‑11 promotion script
if (activityType == "DOUBLE11") {
if (totalAmount >= 1000) { return totalAmount - 200; }
else if (totalAmount >= 500) { return totalAmount - 80; }
else if (totalAmount >= 200) { return totalAmount - 30; }
}
return totalAmount;Pre‑warm campaigns with scheduled activation.
Real‑time optimization based on performance data.
Instant rollback when issues are detected.
Conclusion
Business agility: rule changes from hours to minutes.
System stability: fewer deployments and lower risk.
Team collaboration: business users can edit rules directly.
Cost reduction: less effort for development, testing, and operations.
Source code: https://github.com/yuboon/java-examples/tree/master/springboot-dynamic-rule
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.
