Let's get started with a Microservice Architecture with Spring Cloud:
Implementing a Simple Rule Engine in Java
Last updated: October 29, 2025
1. Overview
In many applications, business decisions depend on a set of rules that evaluate data to produce an outcome or reach a conclusion. A rule engine enables the definition and execution of business rules dynamically, while decoupling them from application code, making it easier to maintain, scale, and manage complex decision-making logic within applications.
Rule engines provide separation of concerns, flexibility, and reusability while executing business rules defined in a structured format. While there are mature libraries such as Drools or Easy Rules, and others that can handle complex rule management, there are cases where a simpler approach is enough. This allows us to keep the dependencies minimal while still fulfilling the specific business requirements.
In this tutorial, we’ll build a simple rule engine using two approaches. First, with Spring Expression Language (SpEL) for dynamic rules, and then a POJO-based approach, which provides more type safety.
2. Setup
To use the Spring Expression Language, we’ll need the spring-expression dependency:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>7.0.0-M7</version>
</dependency>
We’ll now define the model classes to use with the rule engine implementations.
Firstly, let’s define the Customer class:
public class Customer {
private String name;
private int loyaltyPoints;
private boolean firstOrder;
// standard getters and setters
}
Next, we’ll define the Order class:
public class Order {
private double amount;
private Customer customer;
// standard getters and setters
}
3. Using SpEL (Spring Expression Language)
Spring Expression Language (SpEL) is an expression language that supports querying and updating the object graph at runtime.
It supports multiple operators, is easy to use, and integrates well with Spring.
3.1. Defining the Rule
We’ll define the rule using SpEL, which can be created in configuration files or directly in the code itself, and it takes an expression and its description:
public class SpelRule {
private final String expression;
private final String description;
public SpelRule(String expression, String description) {
this.expression = expression;
this.description = description;
}
public boolean evaluate(Order order) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(order);
context.setVariable("order", order);
return parser.parseExpression(expression)
.getValue(context, Boolean.class);
}
// standard getters and setters
}
The evaluate method takes in a parameter of type Order and sets it in the context. Then, the system evaluates the context against the defined expression to determine whether the input values satisfy the rule.
3.2. Tests
We’ll try the following two simple rules with the SpEL engine:
- Loyalty Discount: If loyalty points are greater than 500, then the customer is eligible for a discount
- First Order High Value Discount: If it’s a first-time customer and the order amount is greater than 500, then offer a special discount
Let’s define and validate these rules below:
@Test
void whenLoyalCustomer_thenEligibleForDiscount() {
Customer customer = new Customer("Bob", 730, false);
Order order = new Order(200.0, customer);
SpelRule rule = new SpelRule(
"#order.customer.loyaltyPoints > 500",
"Loyalty discount rule"
);
assertTrue(rule.evaluate(order));
}
@Test
void whenFirstOrderHighAmount_thenEligibleForSpecialDiscount() {
Customer customer = new Customer("Bob", 0, true);
Order order = new Order(800.0, customer);
SpelRule approvalRule = new SpelRule(
"#order.customer.firstOrder and #order.amount > 500",
"First-time customer with high order gets special discount"
);
assertTrue(approvalRule.evaluate(order));
}
As we can see, two new business rules have been added and verified on the provided input data.
4. POJO-based Rule Engine
Now, let’s explore a Java-based rule engine that provides better type safety as compared to the SpEL approach.
4.1. Defining the Rule
We’ll begin by defining a Rule interface, which will provide the contract for all the business rules. The contract is similar to what we had for the SpEL engine; the engine evaluates an expression against the Order passed to the rule. The evaluate method provides much more type safety as it uses the input object attributes to define and enforce the rule:
public interface IRule {
boolean evaluate(Order order);
String description();
}
Next, we’ll define the rules based on the business requirements.
First, we start with the “Loyalty Discount” rule:
public class LoyaltyDiscountRule implements IRule{
@Override
public boolean evaluate(Order order) {
return order.getCustomer().getLoyaltyPoints() > 500;
}
@Override
public String description() {
return "Loyalty Discount Rule: Customer has more than 500 points";
}
}
Next, we’ll define the “First Order High Value Discount” Rule:
public class FirstOrderHighValueSpecialDiscountRule implements IRule {
@Override
public boolean evaluate(Order order) {
return order.getCustomer()
.isFirstOrder() && order.getAmount() > 500;
}
@Override
public String description() {
return "First Order Special Discount Rule: First Time customer with high value order";
}
}
Now that we have some rules in place, let’s define the rule engine, which will process these rules for the input data:
public class RuleEngine {
private final List<IRule> rules;
public RuleEngine(List<IRule> rules) {
this.rules = rules;
}
public List<String> evaluate(Order order) {
return rules.stream()
.filter(rule -> rule.evaluate(order))
.map(IRule::description)
.collect(Collectors.toList());
}
}
The rule engine has its own evaluate method, which takes the input parameter. Initially, it evaluates all the rules and finally returns the ones that are satisfied.
4.2. Tests
We have the rules defined as per the business requirements and a rule engine in place to process them. Let’s run some tests to validate the same:
@Test
void whenTwoRulesTriggered_thenBothDescriptionsReturned() {
Customer customer = new Customer("Max", 550, true);
Order order = new Order(600.0, customer);
RuleEngine engine = new RuleEngine(List.of(new LoyaltyDiscountRule(), new FirstOrderHighValueSpecialDiscountRule()));
List<String> results = engine.evaluate(order);
assertEquals(2, results.size());
assertTrue(results.contains("Loyalty Discount Rule: Customer has more than 500 points"));
assertTrue(results.contains("First Order Special Discount Rule: First Time customer with high value order"));
}
@Test
void whenNoRulesTriggered_thenEmptyListReturned() {
Customer customer = new Customer("Max", 50, false);
Order order = new Order(200.0, customer);
RuleEngine engine = new RuleEngine(List.of(new LoyaltyDiscountRule(), new FirstOrderHighValueSpecialDiscountRule()));
List<String> results = engine.evaluate(order);
assertTrue(results.isEmpty());
}
As we can observe here, in the first test case, the rules are satisfied based on the Customer and the Order inputs. On the other hand, since the input parameters do not fulfill the business requirements, none of the rules are satisfied in the second test case.
5. Comparison of Rule Engine Approaches
We explored how rules are evaluated in both implementations, and summarised the key features and trade-offs of each approach:
| Aspect | SpEL Approach | POJO Approach |
|---|---|---|
| Compile-time safety | No Compile-time safety – errors caught only at runtime | Has Compile-time safety |
| Refactoring | High refactoring costs when underlying attributes change | Refactor-friendly code |
| Debugging | Challenging to maintain and debug complex rules | Easy to debug |
| Flexibility | Flexible for frequent rule changes | Less flexible when rules change frequently |
| Rule Updates | No code change required for rule updates | Adding or updating rules requires a code change and redeployment |
6. Conclusion
In this article, we’ve explored how to build a simple Java-based rule engine from scratch.
We started with the SpEL-based engine, which evaluates dynamic rules at runtime, but it can be challenging to maintain and debug, and provides no compile-time checks.
Next, we explored a POJO-based engine, which provides more type safety and clarity; however, it introduces a more rigid system of rules.
The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.

















