Skip to content

Commit 1e959a1

Browse files
committed
feat: add rule engine design pattern
Add an educational rule-engine module demonstrating the Rule Engine pattern. Each business rule is an independent object exposing evaluate (decision) and execute (action); a RuleEngine evaluates a collection of rules against an immutable loan-application context in insertion order, runs the passing rules, and reports every passed and failed rule via an immutable RuleEngineResult. Includes three concrete rules, an App demo, unit tests, README, and class diagram.
1 parent b55fc2b commit 1e959a1

16 files changed

Lines changed: 1113 additions & 0 deletions

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@
208208
<module>resource-acquisition-is-initialization</module>
209209
<module>retry</module>
210210
<module>role-object</module>
211+
<module>rule-engine</module>
211212
<module>saga</module>
212213
<module>separated-interface</module>
213214
<module>serialized-entity</module>

rule-engine/README.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
---
2+
title: "Rule Engine Pattern in Java: Replacing Tangled Conditionals with Composable Rules"
3+
shortTitle: Rule Engine
4+
description: "Learn the Rule Engine design pattern in Java. Encapsulate each business rule as an independent object and let an engine evaluate and execute them against a shared context, replacing large nested if/else blocks with small, testable, composable rules."
5+
category: Behavioral
6+
language: en
7+
tag:
8+
- Business
9+
- Decoupling
10+
- Domain
11+
- Encapsulation
12+
- Extensibility
13+
---
14+
15+
## Intent of Rule Engine Design Pattern
16+
17+
Encapsulate each business rule as an independent, self-contained object and let an engine evaluate and execute a collection of those rules against a shared context, so that decision logic can grow and change without rewriting a large nested conditional block.
18+
19+
## Detailed Explanation of Rule Engine Pattern with Real-World Examples
20+
21+
Real-world example
22+
23+
> A bank decides whether to approve a loan by checking several independent criteria: the applicant must be old enough, earn enough, and have a good enough credit score. Instead of hard-coding one enormous condition, the bank keeps each criterion as a separate policy on a checklist. A clerk walks the whole checklist for every application, ticks off the criteria that pass, and writes down the ones that fail. Adding a new criterion means adding a line to the checklist, not rewriting the whole approval procedure.
24+
25+
In plain words
26+
27+
> The Rule Engine pattern turns each branch of a giant `if/else` into its own object and hands a collection of them to an engine that runs them all against the same input, collecting which passed and which failed.
28+
29+
## Programmatic Example of Rule Engine Pattern in Java
30+
31+
Each rule separates the **decision** (`evaluate`) from the **action** (`execute`). `evaluate` reports whether a rule's condition holds; `execute` performs the side effect associated with a satisfied rule.
32+
33+
```java
34+
public interface Rule<T> {
35+
36+
String name();
37+
38+
boolean evaluate(T context);
39+
40+
void execute(T context);
41+
}
42+
```
43+
44+
The context is an immutable value object that carries the data the rules inspect.
45+
46+
```java
47+
public record LoanApplication(
48+
int age, double monthlyIncome, double loanAmount, int creditScore) {}
49+
```
50+
51+
Concrete rules encapsulate one criterion each. New criteria are added by writing new classes — the engine never changes.
52+
53+
```java
54+
public class MinimumAgeRule implements Rule<LoanApplication> {
55+
56+
private final int minimumAge;
57+
58+
public MinimumAgeRule(int minimumAge) {
59+
this.minimumAge = minimumAge;
60+
}
61+
62+
@Override
63+
public String name() {
64+
return "MinimumAgeRule";
65+
}
66+
67+
@Override
68+
public boolean evaluate(LoanApplication context) {
69+
return context.age() >= minimumAge;
70+
}
71+
72+
@Override
73+
public void execute(LoanApplication context) {
74+
LOGGER.info("Applicant age {} meets the minimum age of {}.", context.age(), minimumAge);
75+
}
76+
}
77+
```
78+
79+
The engine holds a defensively copied, immutable list of rules. It evaluates every rule in insertion order, executes the ones that pass, and reports the combined outcome. It never stops at the first failure, so a single run reports *all* the reasons an application was rejected.
80+
81+
```java
82+
public class RuleEngine<T> {
83+
84+
private final List<Rule<T>> rules;
85+
86+
public RuleEngine(List<Rule<T>> rules) {
87+
this.rules = List.copyOf(rules);
88+
}
89+
90+
public RuleEngineResult run(T context) {
91+
Objects.requireNonNull(context, "context must not be null");
92+
List<String> passed = new ArrayList<>();
93+
List<String> failed = new ArrayList<>();
94+
for (Rule<T> rule : rules) {
95+
if (rule.evaluate(context)) {
96+
rule.execute(context);
97+
passed.add(rule.name());
98+
} else {
99+
failed.add(rule.name());
100+
}
101+
}
102+
return new RuleEngineResult(failed.isEmpty(), passed, failed);
103+
}
104+
}
105+
```
106+
107+
The result is an immutable value object describing the run.
108+
109+
```java
110+
public record RuleEngineResult(
111+
boolean approved, List<String> passedRules, List<String> failedRules) {
112+
113+
public RuleEngineResult {
114+
passedRules = List.copyOf(passedRules);
115+
failedRules = List.copyOf(failedRules);
116+
}
117+
}
118+
```
119+
120+
Putting it together, the `App` builds an engine and runs two applications through it.
121+
122+
```java
123+
var engine =
124+
new RuleEngine<>(
125+
List.of(
126+
new MinimumAgeRule(18),
127+
new MinimumIncomeRule(2000.0),
128+
new CreditScoreRule(650)));
129+
130+
engine.run(new LoanApplication(30, 3500.0, 15000.0, 720)); // approved
131+
engine.run(new LoanApplication(17, 1500.0, 15000.0, 720)); // rejected: age and income
132+
```
133+
134+
Program output:
135+
136+
```
137+
Applicant age 30 meets the minimum age of 18.
138+
Applicant income 3500.0 meets the minimum income of 2000.0.
139+
Applicant credit score 720 meets the minimum score of 650.
140+
Loan approved. Passed rules: [MinimumAgeRule, MinimumIncomeRule, CreditScoreRule]
141+
Applicant credit score 720 meets the minimum score of 650.
142+
Loan rejected. Failed rules: [MinimumAgeRule, MinimumIncomeRule]
143+
```
144+
145+
### Behavioral decisions
146+
147+
The example commits to the following semantics, each covered by tests:
148+
149+
* `evaluate` returning `true` means the rule **passes** (its condition is satisfied); a passing rule then has its `execute` action run.
150+
* The engine evaluates **every** rule; it does not stop after the first failure, so all rejection reasons are reported.
151+
* Outcomes are reported as an immutable `RuleEngineResult` carrying the overall `approved` flag plus the names of the passed and failed rules.
152+
* Rules execute in **insertion order**.
153+
* A `null` context passed to `run` throws `NullPointerException`; a `null` rule collection or a `null` rule element is rejected on construction.
154+
* An engine with **no rules** approves vacuously (nothing failed).
155+
156+
## Class diagram
157+
158+
See [rule-engine.urm.puml](./etc/rule-engine.urm.puml) for the PlantUML class diagram.
159+
160+
## When to Use the Rule Engine Pattern in Java
161+
162+
* Decision logic is a growing set of independent conditions that would otherwise become a large, hard-to-read nested `if/else`.
163+
* Rules must be added, removed, or reordered without touching the code that coordinates them.
164+
* You need to report *all* failing conditions, not just the first one.
165+
* Business rules deserve to be unit tested in isolation.
166+
167+
## Real-World Applications of Rule Engine Pattern in Java
168+
169+
* Loan, insurance, and credit approval workflows.
170+
* Validation frameworks such as Bean Validation, where each constraint is an independent rule.
171+
* Fraud detection and risk scoring, where many independent checks contribute to one decision.
172+
* Pricing, discount, and promotion eligibility engines.
173+
174+
## Benefits and Trade-offs of Rule Engine Pattern
175+
176+
Benefits
177+
178+
* Encapsulation: each rule owns one criterion.
179+
* Extensibility: new rules are new classes; the engine is closed for modification.
180+
* Testability: rules and the engine can be tested independently.
181+
* Transparency: the result lists every passed and failed rule.
182+
183+
Trade-offs
184+
185+
* Debugging a decision means tracing several small objects instead of reading one block.
186+
* Rule ordering and conflicts must be managed deliberately when rules are not independent.
187+
* Over-abstracting trivial logic into a rule engine adds indirection that a simple `if` would not.
188+
189+
## Related Java Design Patterns
190+
191+
* [Specification](../specification): combines boolean criteria that an object must satisfy; a Rule Engine orchestrates and executes such criteria.
192+
* [Chain of Responsibility](../chain-of-responsibility): passes a request along handlers; a Rule Engine instead runs every rule against one context.
193+
* [Strategy](../strategy): each rule is effectively a pluggable strategy for one decision.
194+
* [Command](../command): a rule's `execute` action resembles an encapsulated command.
195+
196+
## References and Credits
197+
198+
* [Patterns of Enterprise Application Architecture](https://www.amazon.com/gp/product/0321127420) (Martin Fowler)
199+
* [Should I use a Rules Engine? (Martin Fowler)](https://martinfowler.com/bliki/RulesEngine.html)
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
@startuml
2+
package com.iluwatar.ruleengine {
3+
interface Rule<T> {
4+
+ name() : String {abstract}
5+
+ evaluate(context : T) : boolean {abstract}
6+
+ execute(context : T) : void {abstract}
7+
}
8+
class RuleEngine<T> {
9+
- rules : List<Rule<T>>
10+
+ RuleEngine(rules : List<Rule<T>>)
11+
+ run(context : T) : RuleEngineResult
12+
+ rules() : List<Rule<T>>
13+
}
14+
class RuleEngineResult {
15+
+ RuleEngineResult(approved : boolean, passedRules : List<String>, failedRules : List<String>)
16+
+ approved() : boolean
17+
+ passedRules() : List<String>
18+
+ failedRules() : List<String>
19+
}
20+
class LoanApplication {
21+
+ LoanApplication(age : int, monthlyIncome : double, loanAmount : double, creditScore : int)
22+
+ age() : int
23+
+ monthlyIncome() : double
24+
+ loanAmount() : double
25+
+ creditScore() : int
26+
}
27+
class MinimumAgeRule {
28+
- minimumAge : int
29+
+ MinimumAgeRule(minimumAge : int)
30+
+ name() : String
31+
+ evaluate(context : LoanApplication) : boolean
32+
+ execute(context : LoanApplication) : void
33+
}
34+
class MinimumIncomeRule {
35+
- minimumIncome : double
36+
+ MinimumIncomeRule(minimumIncome : double)
37+
+ name() : String
38+
+ evaluate(context : LoanApplication) : boolean
39+
+ execute(context : LoanApplication) : void
40+
}
41+
class CreditScoreRule {
42+
- minimumScore : int
43+
+ CreditScoreRule(minimumScore : int)
44+
+ name() : String
45+
+ evaluate(context : LoanApplication) : boolean
46+
+ execute(context : LoanApplication) : void
47+
}
48+
class App {
49+
+ App()
50+
+ main(args : String[]) : void
51+
}
52+
}
53+
RuleEngine --> "*" Rule
54+
RuleEngine ..> RuleEngineResult
55+
MinimumAgeRule ..|> Rule
56+
MinimumIncomeRule ..|> Rule
57+
CreditScoreRule ..|> Rule
58+
MinimumAgeRule ..> LoanApplication
59+
MinimumIncomeRule ..> LoanApplication
60+
CreditScoreRule ..> LoanApplication
61+
@enduml

rule-engine/pom.xml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!--
3+
4+
This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
5+
6+
The MIT License
7+
Copyright © 2014-2022 Ilkka Seppälä
8+
9+
Permission is hereby granted, free of charge, to any person obtaining a copy
10+
of this software and associated documentation files (the "Software"), to deal
11+
in the Software without restriction, including without limitation the rights
12+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13+
copies of the Software, and to permit persons to whom the Software is
14+
furnished to do so, subject to the following conditions:
15+
16+
The above copyright notice and this permission notice shall be included in
17+
all copies or substantial portions of the Software.
18+
19+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25+
THE SOFTWARE.
26+
27+
-->
28+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
29+
<parent>
30+
<artifactId>java-design-patterns</artifactId>
31+
<groupId>com.iluwatar</groupId>
32+
<version>1.26.0-SNAPSHOT</version>
33+
</parent>
34+
<modelVersion>4.0.0</modelVersion>
35+
<artifactId>rule-engine</artifactId>
36+
<dependencies>
37+
<dependency>
38+
<groupId>org.slf4j</groupId>
39+
<artifactId>slf4j-api</artifactId>
40+
</dependency>
41+
<dependency>
42+
<groupId>ch.qos.logback</groupId>
43+
<artifactId>logback-classic</artifactId>
44+
</dependency>
45+
<dependency>
46+
<groupId>org.junit.jupiter</groupId>
47+
<artifactId>junit-jupiter-engine</artifactId>
48+
<scope>test</scope>
49+
</dependency>
50+
</dependencies>
51+
<build>
52+
<plugins>
53+
<plugin>
54+
<groupId>org.apache.maven.plugins</groupId>
55+
<artifactId>maven-assembly-plugin</artifactId>
56+
<executions>
57+
<execution>
58+
<configuration>
59+
<archive>
60+
<manifest>
61+
<mainClass>com.iluwatar.ruleengine.App</mainClass>
62+
</manifest>
63+
</archive>
64+
</configuration>
65+
</execution>
66+
</executions>
67+
</plugin>
68+
</plugins>
69+
</build>
70+
</project>

0 commit comments

Comments
 (0)