Boost Business Agility with a Dynamic Rule Engine in Spring Boot

This article explains how separating business rules from code using a dynamic rule engine—implemented with QLExpress, Spring Boot, and a lightweight front‑end—enables rapid rule changes, reduces deployment risk, and empowers non‑technical staff to adjust pricing, risk, and marketing policies in real time.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
Boost Business Agility with a Dynamic Rule Engine in Spring Boot

Introduction

As a backend developer, you may have faced urgent requests to modify promotional rules, risk‑control strategies, or pricing discounts, which traditionally require code changes, rebuilding, redeploying, and service restarts, often taking hours and introducing release risk.

Business Pain Point: The "Hard‑to‑Change" Rule Problem

Traditional Approach Issues

In conventional systems, business rules are hard‑coded inside the business logic:

// Traditional hard‑coded method
public BigDecimal calculateVipDiscount(String userLevel, BigDecimal price) {
    if ("GOLD".equals(userLevel)) {
        return price.multiply(new BigDecimal("0.8")); // 8% discount
    } else if ("SILVER".equals(userLevel)) {
        return price.multiply(new BigDecimal("0.9")); // 9% discount
    }
    return price; // No discount
}

This leads to slow response, high risk, high cost, and low flexibility.

Real‑World Scenarios

E‑commerce: frequent promotional rule adjustments, A/B testing, competitor price changes.

Finance: real‑time risk model tweaks, interest rate policy updates, dynamic audit rules.

Content Platforms: evolving recommendation algorithms, content moderation rules, user‑level systems.

Solution: Dynamic Rule Engine

Core Idea

Separate business rules from business code . The code handles workflow and data processing, while rules are stored as scripts and executed at runtime.

Traditional: BusinessLogic = Workflow + Hard‑coded Rules
Dynamic: BusinessLogic = Workflow + Scripted Rules

Technology Stack

Rule Engine: QLExpress (Alibaba open‑source) Backend Framework: Spring Boot Front‑end: HTML5 + TailwindCSS

Core Implementation: Building the Engine Step‑by‑Step

1. Project Structure

springboot-dynamic-rule/
├── entity/
│   ├── RuleScript.java          # Rule entity
│   ├── OrderProcessResult.java  # Order result
│   └── RuleExecuteResponse.java# Execution response
├── service/
│   ├── DynamicRuleEngine.java   # Engine core
│   └── OrderService.java       # Business service
├── controller/
│   ├── RuleController.java      # Rule management API
│   └── OrderController.java    # Business API
└── resources/
    └── static/
        ├── index.html          # Rule UI
        └── business.html       # Demo page

2. Rule Engine Core

@Slf4j
@Service
public class DynamicRuleEngine {
    // In‑memory rule store (could be DB in real projects)
    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) {
        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("Execute rule: {}, result: {}", ruleName, result);
        return RuleExecuteResponse.success(result);
    }
}

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());
            BigDecimal reduction = amount.subtract(result);
            steps.add(new OrderProcessResult.ProcessStep("VIP Discount",
                "Apply discount for level " + order.getUserLevel(), amount, result, reduction, "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 simulation.

Rule Management Page

Rule list with status.

Online editing of rule scripts.

Real‑time testing after modifications.

One‑click enable/disable.

Business Demo Page

Order simulator: input order data and view processing results.

Step‑by‑step display of rule execution.

History of processed orders.

Rule management UI
Rule management UI
Business demo UI
Business demo UI

Front‑end Code Example

// Dynamic rule execution (JS)
async function executeRule(ruleName, params) {
    try {
        const response = await fetch(`/api/rules/execute/${ruleName}`, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(params)
        });
        const result = await response.json();
        if (result.success) {
            displayResult(`Execution succeeded! Result: ${result.result}`);
        } else {
            displayResult(`Execution failed: ${result.errorMessage}`);
        }
    } catch (error) {
        displayResult(`Request error: ${error.message}`);
    }
}

function displayProcessSteps(processSteps) {
    const stepsHtml = processSteps.map((step, index) => `
        <div class="process-step">
            <div class="step-number">${index + 1}</div>
            <div class="step-content">
                <h4>${step.stepName}</h4>
                <p>${step.description}</p>
            </div>
            <div class="step-result">
                <div class="amount-change">¥${step.beforeAmount} → ¥${step.afterAmount}</div>
                <div class="reduction">Saved ¥${step.reduction.toFixed(2)}</div>
            </div>
        </div>`).join('');
    document.getElementById('processingSteps').innerHTML = stepsHtml;
}

Practical Use Cases

Scenario 1: E‑commerce Dynamic Pricing

Requirement: calculate discounts based on user level and order amount.

// Scripted rule (editable at runtime)
if (userLevel == "GOLD" && totalAmount >= 100) {
    return totalAmount * 0.8;
} else if (userLevel == "SILVER" && totalAmount >= 50) {
    return totalAmount * 0.9;
} else {
    return totalAmount;
}

Result: response time drops from hours to minutes, release risk shifts from system‑wide to rule‑level, and business staff can modify rules directly.

Scenario 2: Real‑time Risk Control

// Risk scoring rule
score = baseScore;
if (userAge < 18) { score -= 20; }
if (creditLevel == "HIGH") { score += 30; }
if (monthlyIncome > 10000) { score += 15; }
return score >= 60 ? "PASS" : "REJECT";

Enables instant adjustment of audit strategies when market risk changes.

Scenario 3: Flexible Marketing Campaigns

// Double‑11 promotion rule
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;

Allows pre‑configuration, real‑time tweaks, and rapid rollback of promotional rules.

Conclusion

By extracting business rules from code, the system achieves:

Business agility : rule changes in minutes instead of hours.

System stability : fewer deployments and lower risk.

Team collaboration : business users can directly edit rules.

Cost reduction : less effort from developers, testers, and ops.

Source code: GitHub repository

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootQLExpressBusiness RulesDynamic Rule Engine
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

0 followers
Reader feedback

How this landed with the community

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.