Replace if-else Sprawl with a 100KB Java Rule Engine
Preface
Recently, during a code review, I discovered a very common problem — a single business method had seven or eight layers of nested if...else, and the code line count soared to over 300 lines.
The complexity of business rules naturally grows over time.
Add a rule today, modify one tomorrow, delete one the day after — hardcoding with if...else is digging a pit for yourself.
So, is there a way to extract business rules from the code, making it clean, maintainable, and dynamically adjustable?
Yes.
The tool I'm introducing today is specifically designed for this — Easy Rules.
I hope it helps you.
More project practices on my technical website: susan.net.cn/project
1. First, Look at the "Seven Deadly Sins" of if...else
Before formally introducing Easy Rules, let's look at a typical "if...else hell" case:
public class DiscountService {
public double calculateDiscount(User user, Order order) {
double discount = 0.0;
// Rule 1: VIP users get 10% off
if (user.isVip()) {
discount = 0.9;
}
// Rule 2: Order amount over 1000, additional 5% off
if (order.getAmount() > 1000) {
discount = discount * 0.95;
}
// Rule 3: New user's first order gets 20% off
if (user.isNewUser() && user.getOrderCount() == 0) {
discount = 0.8;
}
// Rule 4: During Double 11, sitewide 10% off
if (isDoubleEleven()) {
discount = discount * 0.9;
}
// Rule 5: Existing users with orders over 5000 get 15% off
if (!user.isNewUser() && order.getAmount() > 5000) {
discount = 0.85;
}
// Rule 6, Rule 7, Rule 8...
// More and more, code gets longer and longer
return discount;
}
}
What's wrong with this code?
First, poor readability. The more rules, the longer the method. Anyone who inherits it wants to curse.
Second, poor maintainability. To change one rule, you have to find the corresponding if branch and modify it carefully, afraid of affecting other logic.
Third, poor testability. To cover all combinations of rules, the number of test cases grows exponentially.
Fourth, poor extensibility. Every new rule requires modifying source code, recompiling, and redeploying.
Fifth, business and code are coupled. Business people wanting to adjust rules have to beg developers to change the code.
Sixth, duplicate logic everywhere. The same condition checks might appear in several places.
Seventh, the service must be restarted after modification. Online rule adjustment flexibility is almost zero.
Some might say: "Can't I just write a configuration table and query these rules from a database?" Indeed, configuration can solve part of the problem. But a true rule engine doesn't just "store rules in a database" — it also needs to solve a series of problems like rule composition, priority, condition evaluation, and automatic execution. Moving if...else to a database, if...else is still if...else, just in a different place.
So what to do?
A rule engine is designed for this.
2. What is Easy Rules?
Easy Rules is a simple yet powerful Java rule engine, open-sourced and maintained by the j-easy team.
Its design is inspired by the "rule engine" concept proposed by Martin Fowler.
In a very classic article, Martin Fowler said:
"You can build a simple rules engine yourself. All you need is to create a bunch of objects with conditions and actions, store them in a collection, and run through them to evaluate the conditions and execute the actions."
Easy Rules does exactly this — it provides the Rule abstraction to create rules with conditions and actions, and the RulesEngine API to run a series of rules.
2.1 Easy Rules in One Sentence
Easy Rules is a tool that "moves if...else out of the code and manages them as rule objects."
It lets you define rules like this:
@Rule(name = "VIP Discount Rule", priority = 1)
public class VipDiscountRule {
@Condition
public boolean isVip(@Fact("user") User user) {
return user.isVip();
}
@Action
public void applyDiscount(@Fact("order") Order order) {
order.setDiscount(0.9);
}
}
And execute them like this:
RulesEngine engine = new DefaultRulesEngine();
engine.fire(rules, facts);
Much cleaner, isn't it?
2.2 Core Features
Easy Rules has the following core features:
- Lightweight: No complex dependencies or configuration, the JAR is only about 100KB
- POJO Development: Define rules using plain Java classes with annotations
- Multiple Definition Methods: Supports annotations, fluent API, expression languages (MVEL/SpEL/JEXL), YAML/JSON configuration
- Composite Rules: Supports combining multiple simple rules into complex rules
- Easy Integration: Can be easily integrated into frameworks like Spring Boot
3. Core Concepts
Easy Rules is built around four core abstractions:
3.1 Rule
The Rule interface is the most core abstraction of Easy Rules. A rule contains:
- name: The unique name of the rule
- description: A brief description of the rule
- priority: The priority of the rule (smaller number means higher priority)
- condition: The condition — triggers the rule when it returns
true - action: The action — executed when the condition is met
public interface Rule {
boolean evaluate(Facts facts); // Evaluate condition
void execute(Facts facts) throws Exception; // Execute action
// getters for name, description, priority...
}
3.2 Facts
Facts is the data context during rule execution, essentially a key-value container. You put data in, and rules retrieve data from it:
Facts facts = new Facts();
facts.put("user", user);
facts.put("order", order);
facts.put("rain", true);
3.3 Rules
Rules is an ordered container for rules, responsible for managing a set of rules. Rules are automatically sorted by priority:
Rules rules = new Rules();
rules.register(new VipDiscountRule());
rules.register(new NewUserDiscountRule());
rules.register(new DoubleElevenRule());
3.4 RulesEngine
RulesEngine is the core engine for executing rules. Easy Rules provides two implementations:
| Engine Type | Execution Strategy | Applicable Scenarios |
|---|---|---|
| DefaultRulesEngine | Executes in priority order, executes when condition met | Most common scenarios |
| InferenceRulesEngine | Forward chaining, repeatedly executes until no rules can be triggered | Scenarios with dependencies and chain reactions between rules |
// Default engine
RulesEngine engine = new DefaultRulesEngine();
engine.fire(rules, facts);
4. Four Ways to Define Rules
Easy Rules provides four ways to define rules, allowing flexible choice based on actual scenarios.
4.1 Method 1: Annotations (Most Common)
Applicable Scenarios: Fixed rules, clear logic, most common business scenarios.
Use @Rule, @Condition, @Action, @Fact annotations to define rules:
@Rule(name = "Weather Rule", description = "Bring an umbrella if it rains", priority = 1)
public class WeatherRule {
@Condition
public boolean isRaining(@Fact("rain") boolean rain) {
return rain;
}
@Action
public void takeUmbrella() {
System.out.println("It's raining, remember to bring an umbrella!");
}
}
Annotation Descriptions:
@Rule: Marks a class as a rule, can specifyname,description, andpriority@Condition: Marks a condition method, must returnboolean, only one allowed@Action: Marks an action method, can have multiple, specify execution order viaorder@Fact: Marks a parameter, retrieves value by name from theFactscontainer
Execution code:
public class Application {
public static void main(String[] args) {
// 1. Prepare facts
Facts facts = new Facts();
facts.put("rain", true);
// 2. Register rules
Rules rules = new Rules();
rules.register(new WeatherRule());
// 3. Execute engine
RulesEngine engine = new DefaultRulesEngine();
engine.fire(rules, facts);
// Output: It's raining, remember to bring an umbrella!
}
}
4.2 Method 2: Fluent API (Dynamic Rules)
Applicable Scenarios: Rules need to be generated dynamically, rule conditions can only be determined at runtime.
Use the RuleBuilder fluent API to build rules:
Rule dynamicRule = new RuleBuilder()
.name("High Temperature Alert Rule")
.description("Remind to prevent heatstroke when temperature exceeds 30 degrees")
.priority(2)
.when(facts -> facts.get("temperature") > 30)
.then(facts -> System.out.println("Hot weather, be careful of heatstroke!"))
.build();
The when() method receives a Predicate<Facts>, and the then() method receives a Consumer<Facts>. This approach is very suitable for scenarios requiring dynamic rule generation at runtime.
4.3 Method 3: Expression Language (Most Flexible)
Applicable Scenarios: Rules change frequently, hoping non-developers can also modify rules.
Easy Rules supports MVEL, SpEL, JEXL three expression languages:
// Using MVEL expression
Rule mvelRule = new MVELRule()
.name("Member Discount Rule")
.description("Members with orders over 100 get 10% off")
.when("user.vip == true && order.amount > 100")
.then("order.discount = 0.9");
The biggest advantage of expression languages is: rules can be stored as strings (database, configuration files, admin backend), and modifying rules does not require recompilation and redeployment.
4.4 Method 4: YAML/JSON Configuration (Configuration-based)
Applicable Scenarios: Want to completely extract rules into configuration files, maintained by business personnel.
Define rules via YAML or JSON files:
# rules.yml
- name: "New User First Order Discount"
description: "New user's first order gets 20% off"
priority: 3
condition: "user.newUser == true && user.orderCount == 0"
actions:
- "order.discount = 0.8"
Load configuration files via MVELRuleFactory or SpELRuleFactory:
MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader());
Rule rule = ruleFactory.createRule(new FileReader("rules.yml"));
5. Underlying Principles
5.1 Overall Architecture
Easy Rules' architecture can be divided into four layers:
5.2 DefaultRulesEngine: Execution Flow
DefaultRulesEngine is the most commonly used engine implementation, its execution flow is as follows:
5.3 InferenceRulesEngine: Forward Chaining
InferenceRulesEngine implements the forward chaining algorithm. Its core logic is:
- Enter the inference loop
- Under the current
Facts, find all rules with conditions evaluating totrue(candidate rules) - If no candidate rules, exit the loop
- Otherwise, execute all candidate rules (which may modify
Facts) - Return to step 2, continue looping
This mode is particularly suitable for scenarios where dependencies exist between rules — the execution result of one rule may become the trigger condition for another.
5.4 Engine Parameter Configuration
Engine behavior can be finely controlled via RulesEngineParameters:
RulesEngineParameters parameters = new RulesEngineParameters()
.skipOnFirstAppliedRule(true) // Skip subsequent rules after the first rule executes
.skipOnFirstFailedRule(true) // Skip subsequent rules after the first rule fails
.skipOnFirstNonTriggeredRule(false) // Whether to skip when the first rule is not triggered
.priorityThreshold(10); // Only execute rules with priority <= 10
RulesEngine engine = new DefaultRulesEngine(parameters);
5.5 Listener Mechanism
Easy Rules provides a listener mechanism to insert custom logic at various stages of rule execution:
public class LoggingRuleListener implements RuleListener {
@Override
public boolean beforeEvaluate(Rule rule, Facts facts) {
System.out.println("Starting to evaluate rule: " + rule.getName());
return true; // Return false to skip this rule
}
@Override
public void afterEvaluate(Rule rule, Facts facts, boolean evaluationResult) {
System.out.println("Rule " + rule.getName() + " evaluation result: " + evaluationResult);
}
@Override
public void onSuccess(Rule rule, Facts facts) {
System.out.println("Rule " + rule.getName() + " executed successfully");
}
@Override
public void onFailure(Rule rule, Facts facts, Exception exception) {
System.err.println("Rule " + rule.getName() + " execution failed: " + exception.getMessage());
}
}
Listeners can be used for various scenarios such as logging, performance monitoring, rule hit rate statistics, and debugging.
6. Composite Rules
Some might ask: A single rule can only handle one condition, what about complex business logic?
Easy Rules provides a composite rule mechanism to combine multiple simple rules into complex rules. It offers three types of composite rules:
6.1 UnitRuleGroup (AND Logic)
"Execute only if all rules are satisfied"
Use Case: Requires multiple preconditions to all be met before executing an action. For example, "User is VIP AND order amount exceeds 1000 AND product is in stock" — all three conditions are indispensable.
UnitRuleGroup unitGroup = new UnitRuleGroup("All Satisfied Rule Group");
unitGroup.addRule(new VipRule());
unitGroup.addRule(new AmountRule());
unitGroup.addRule(new StockRule());
// Only when all three rules are satisfied will the group execute
6.2 ActivationRuleGroup
"If any one is satisfied, execute the first one"
Use Case: Multiple mutually exclusive discount rules, only one can take effect. For example, when both "New user first order 20% off" and "VIP user 10% off" are satisfied, only the one with higher priority executes.
ActivationRuleGroup activationGroup = new ActivationRuleGroup("Discount Rule Group");
activationGroup.addRule(new NewUserDiscountRule()); // priority=3
activationGroup.addRule(new VipDiscountRule()); // priority=1
// Only the rule with the highest priority will execute (VIP discount)
6.3 ConditionalRuleGroup (Conditional Logic)
"The first satisfied rule determines whether to execute subsequent rules"
Use Case: Decide whether to execute a subsequent rule chain based on the result of the first rule.
ConditionalRuleGroup conditionalGroup = new ConditionalRuleGroup("Conditional Rule Group");
conditionalGroup.addRule(new CheckUserTypeRule()); // First rule: determine user type
conditionalGroup.addRule(new VipDiscountRule()); // Subsequent rules: execute based on type
conditionalGroup.addRule(new NormalDiscountRule());
// Only if the first rule is satisfied will subsequent rules continue to execute
7. Maven Dependency
Introducing Easy Rules into a project is very simple:
<!-- Easy Rules Core Library -->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>4.1.0</version>
</dependency>
<!-- Support for YAML/JSON rule definition -->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-support</artifactId>
<version>4.1.0</version>
</dependency>
<!-- Support for MVEL expression language -->
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-mvel</artifactId>
<version>4.1.0</version>
</dependency>
Note: The Easy Rules project has been in maintenance mode since December 2020, with the latest stable version being 4.1.x. All users are recommended to upgrade to this version.
8. Pros and Cons
Pros
1. Extremely Lightweight The JAR is only about 100KB, with no complex dependencies or configuration. Startup speed drops from seconds to milliseconds, very suitable for fast Pod startup in Kubernetes environments.
2. Very Low Learning Curve Based on POJO and annotation programming model, Java developers can get started with almost zero threshold. No need to learn complex DSLs (Domain Specific Languages).
3. Multiple Rule Definition Methods Supports four methods: annotations, fluent API, expression languages (MVEL/SpEL/JEXL), YAML/JSON, covering the full spectrum from "hardcoded" to "fully configuration-based".
4. Composite Rule Support Supports three types of composite rules: AND logic, exclusive OR logic, conditional logic, allowing complex business logic to be composed from simple rules.
5. Listener Mechanism Provides a complete rule execution lifecycle listener, convenient for logging, performance monitoring, and debugging.
6. Zero Configuration, Out-of-the-Box No external configuration files needed, just introduce the dependency and start using.
7. Seamless Integration with Spring Boot
Can be easily integrated into Spring Boot projects, registering rules and engines via @Bean.
Cons
1. Rule Modification Still Requires Restart After business rule changes, Java code still needs to be modified and the service restarted, unless combined with dynamic class loading or expression language solutions.
2. Does Not Support Complex Rule Chains and Decision Tables Compared to heavyweight rule engines like Drools, Easy Rules has limited capabilities in advanced scenarios like complex rule chains and decision tables.
3. Project in Maintenance Mode Easy Rules has been in maintenance mode since December 2020, currently only supporting version 4.1.x. New feature development has basically stopped, but existing features are stable enough.
4. Not Suitable for Ultra-Large Rule Sets When the number of rules reaches thousands, performance may not be as good as Drools using the Rete algorithm.
5. No Visual Management Interface Easy Rules itself does not provide a visual rule management interface, requiring self-development.
9. Applicable Scenarios
Highly Recommended Scenarios
| Scenario | Typical Applications |
|---|---|
| E-commerce Promotion Rules | Discounts, coupons, member privileges |
| Risk Control Systems | User scoring, anomaly detection, anti-fraud |
| Data Validation | Form validation, data integrity checks |
| Conditional Judgments | Conditional branches for user registration, login, etc. |
| Dynamic Decision Making | Intelligent recommendations, personalized services |
| Game AI | NPC behavior rules, intelligent decision making |
| Configuration Management | Managing business rules via YAML/JSON files |
Less Suitable Scenarios
| Scenario | Reason |
|---|---|
| Ultra-large rule sets (thousands) | Performance inferior to professional engines like Drools |
| Need for visual rule management | Easy Rules does not provide a visual interface |
| Need for real-time hot update of rules | Requires self-implementation combined with dynamic class loading or expression languages |
| Very complex decision tables | Easy Rules has limited capabilities |
A practical selection suggestion: If you're unsure how complex the rules will become in the future, start with Easy Rules and smoothly migrate to Drools when rules become more complex.
More project practices on my technical website: susan.net.cn/project
10. Final Words
Back to the original question: Why eliminate if...else?
It's not that if...else itself is problematic — it's the most basic control structure of programming languages, there's nothing wrong with it.
The problem is, when business rules keep growing, if...else turns into a "code garbage mountain" — piling higher and higher, messier and messier, until no one dares to touch it.
What Easy Rules does is "move" if...else out of the code, turning them into independent, composable, manageable rule objects.
It's not a "silver bullet" — it doesn't solve all problems, it just changes "rule management" from "hardcoded" to "managed with objects".
But this one step alone can transform your code from "if...else hell" into an elegant architecture with "clear, maintainable, and extensible rules".
If you're maintaining an old system bursting with if...else, or designing a new system with frequently changing rules, spend 10 minutes running Easy Rules' official examples.
Experience the feeling of "rules as objects" — you'll find that code can be this refreshing.
Open Source Address and Official Resources:
- GitHub: https://github.com/j-easy/easy-rules
- Official Documentation: https://www.easyrules.org/
Top 1 from juejin.cn, machine-translated. The original thread is authoritative.
Haha, seeing this feels so familiar. When I was doing Java, I also stacked a pile of if-else — a single coupon calculation method was almost 400 lines 😂 Later I switched to Python for crawlers and price monitoring systems, and found that Python actually has a wilder way to handle rule complexity — just write a yaml config file, validate it with pydantic, then use a simple dispatch function with reflection to call it. It's not as engineered as Easy Rules, but it's enough for small to medium scale. But honestly, if I were still on the Java side back then, Easy Rules is indeed way better than pure if-else, at least you don't have to recompile and redeploy every time you add a rule.