Build a Dynamic Rule Engine with Spring Boot 3: Strategy & Registry Patterns

This article explains why traditional if‑else logic hampers complex business rule maintenance and demonstrates how to create a flexible Spring Boot 3 rule engine using strategy and registry patterns, complete with interfaces, implementations, a registry, an engine, and a REST controller for testing.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Build a Dynamic Rule Engine with Spring Boot 3: Strategy & Registry Patterns

1. Introduction

In complex business systems, rules change frequently and involve many conditional checks, making traditional if‑else implementations hard to maintain, test, and deploy. A rule engine decouples rule logic from core code, improving scalability and manageability.

if (user.getAge() > 60 && user.getLevel() > 3) {
    // Rule A
} else if (...) {
    // Rule B
}
// ...more than 100 lines of “if hell”

Using the Strategy pattern together with a Registry pattern allows dynamic insertion of rules without touching core logic.

2. Practical Implementation

2.1 Core Interfaces

Define a generic Rule interface and a marker Context interface.

public interface Rule<T extends Context, R> {
    /** Rule name */
    String getName();
    /** Evaluate rule */
    R evaluate(T context);
}
public interface Context {}

2.2 Context Example

A UserContext carries a User object.

public class UserContext implements Context {
    private final User user;
    public UserContext(User user) { this.user = user; }
    public User getUser() { return user; }
}

2.3 Rule Implementations

Two simple rules demonstrate age and level checks.

@Component
public class UserAgeRule implements Rule<UserContext, Boolean> {
    @Override public String getName() { return "user_age_rule"; }
    @Override public Boolean evaluate(UserContext ctx) {
        if (ctx == null) throw new IllegalArgumentException("Context must not be null");
        return ctx.getAge() > 18;
    }
}
@Component
public class UserLevelRule implements Rule<UserContext, Boolean> {
    @Override public String getName() { return "user_level_rule"; }
    @Override public Boolean evaluate(UserContext ctx) {
        if (ctx == null) throw new IllegalArgumentException("Context must not be null");
        return ctx.getUser().getLevel() >= 3;
    }
}

2.4 Registry

RuleRegistry

stores all rule beans and provides lookup by name.

@Component
public class RuleRegistry {
    private final Map<String, Rule<?, ?>> ruleStore = new HashMap<>();
    public RuleRegistry(List<Rule<?, ?>> rules) {
        rules.forEach(r -> ruleStore.put(r.getName(), r));
    }
    @SuppressWarnings("unchecked")
    private <T extends Context, R> Rule<T, R> getRuleInternal(String name) {
        Rule<?, ?> rule = ruleStore.get(name);
        if (rule == null) throw new IllegalArgumentException("Rule not found: " + name);
        return (Rule<T, R>) rule;
    }
    public <T extends Context, R> R execute(String name, T ctx) {
        return getRuleInternal(name).evaluate(ctx);
    }
}

2.5 Engine

RuleEngine

delegates execution to the registry and handles missing‑rule errors.

@Service
public class RuleEngine {
    private final RuleRegistry ruleRegistry;
    public RuleEngine(RuleRegistry ruleRegistry) { this.ruleRegistry = ruleRegistry; }
    public <T extends Context, R> R execute(String name, T ctx) {
        try { return ruleRegistry.execute(name, ctx); }
        catch (IllegalArgumentException e) {
            throw new RuntimeException("Rule '" + name + "' not found or invalid context", e);
        }
    }
}

2.6 Controller Test

A simple REST controller receives a rule name and a User payload, builds a UserContext, and returns the evaluation result.

@RestController
@RequestMapping("/rules")
public class RuleController {
    private final RuleEngine ruleEngine;
    public RuleController(RuleEngine ruleEngine) { this.ruleEngine = ruleEngine; }
    @PostMapping("/evaluate")
    public ResponseEntity<Boolean> evaluate(@RequestParam String rule,
                                            @RequestBody User user) {
        UserContext ctx = new UserContext(user);
        return ResponseEntity.ok(ruleEngine.execute(rule, ctx));
    }
}

3. Result

Running the controller with the two rules returns true or false according to the supplied user data, as shown in the screenshots below.

Test result 1
Test result 1
Test result 2
Test result 2
Rule engine architecture
Rule engine architecture
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.

BackendDesign PatternsjavaStrategy PatternSpring Boot
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

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.