Design and Implementation of a Java Rule Engine with Short‑Circuit Evaluation
This article explains a real‑world scenario of extending trial‑user eligibility rules, introduces a refactored Java rule‑engine architecture that separates data, abstract rule templates, concrete rule implementations, and a service supporting AND/OR short‑circuit logic, and evaluates its advantages and drawbacks.
The author received a requirement to extend an existing trial‑user application rule set, which originally consisted of a long chain of if‑else statements that were hard to maintain and lacked short‑circuit behavior.
Recognizing that the business logic is fundamentally based on AND/OR relationships, the author decided to redesign the rule processing component to support short‑circuit evaluation, improving both performance and maintainability.
Rule Engine Design (V1)
The design introduces a RuleDto data transfer object, a BaseRule interface, an abstract AbstractRule class that handles generic conversion and execution, and concrete rule classes such as AddressRule and NationalityRule. Constants for matching criteria are defined in RuleConstant.
// 业务数据
@Data
public class RuleDto {
private String address;
private int age;
}
// 规则抽象
public interface BaseRule {
boolean execute(RuleDto dto);
}
// 规则模板
public abstract class AbstractRule implements BaseRule {
protected <T> T convert(RuleDto dto) { return (T) dto; }
@Override
public boolean execute(RuleDto dto) { return executeRule(convert(dto)); }
protected <T> boolean executeRule(T t) { return true; }
}
// 具体规则‑ 例子1
public class AddressRule extends AbstractRule {
@Override
public boolean execute(RuleDto dto) {
System.out.println("AddressRule invoke!");
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
return true;
}
return false;
}
}
// 具体规则‑ 例子2
public class NationalityRule extends AbstractRule {
@Override
protected <T> T convert(RuleDto dto) {
NationalityRuleDto nationalityRuleDto = new NationalityRuleDto();
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
nationalityRuleDto.setNationality(MATCH_NATIONALITY_START);
}
return (T) nationalityRuleDto;
}
@Override
protected <T> boolean executeRule(T t) {
System.out.println("NationalityRule invoke!");
NationalityRuleDto nr = (NationalityRuleDto) t;
if (nr.getNationality().startsWith(MATCH_NATIONALITY_START)) {
return true;
}
return false;
}
}
// 常量定义
public class RuleConstant {
public static final String MATCH_ADDRESS_START = "北京";
public static final String MATCH_NATIONALITY_START = "中国";
}Rule Service Construction
The RuleService maintains a map of rule lists keyed by logical operators (AND = 1, OR = 0). It provides fluent and() and or() methods to register rule groups and an execute() method that iterates over the map, applying short‑circuit logic for each operator.
public class RuleService {
private Map<Integer, List<BaseRule>> hashMap = new HashMap<>();
private static final int AND = 1;
private static final int OR = 0;
public static RuleService create() { return new RuleService(); }
public RuleService and(List<BaseRule> ruleList) { hashMap.put(AND, ruleList); return this; }
public RuleService or(List<BaseRule> ruleList) { hashMap.put(OR, ruleList); return this; }
public boolean execute(RuleDto dto) {
for (Map.Entry<Integer, List<BaseRule>> item : hashMap.entrySet()) {
List<BaseRule> ruleList = item.getValue();
switch (item.getKey()) {
case AND:
System.out.println("execute key = " + 1);
if (!and(dto, ruleList)) return false;
break;
case OR:
System.out.println("execute key = " + 0);
if (!or(dto, ruleList)) return false;
break;
default:
break;
}
}
return true;
}
private boolean and(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
if (!rule.execute(dto)) return false;
}
return true;
}
private boolean or(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
if (rule.execute(dto)) return true;
}
return false;
}
}Engine Invocation Example
A JUnit test demonstrates creating concrete rule instances, populating a RuleDto, and chaining and() and or() calls before executing the rule service.
public class RuleServiceTest {
@org.junit.Test
public void execute() {
AgeRule ageRule = new AgeRule();
NameRule nameRule = new NameRule();
NationalityRule nationalityRule = new NationalityRule();
AddressRule addressRule = new AddressRule();
SubjectRule subjectRule = new SubjectRule();
RuleDto dto = new RuleDto();
dto.setAge(5);
dto.setName("张三");
dto.setAddress("北京");
dto.setSubject("数学");
boolean ruleResult = RuleService
.create()
.and(Arrays.asList(nationalityRule, nameRule, addressRule))
.or(Arrays.asList(ageRule, subjectRule))
.execute(dto);
System.out.println("this student rule execute result :" + ruleResult);
}
}Summary of Advantages and Disadvantages
Advantages: the architecture is simple, each rule is independent, and the separation of rule, data, and executor makes the caller code clean; the abstract convert method allows rule‑specific data transformation.
Disadvantages: all rules share the same DTO, creating hidden data dependencies; modifying the DTO directly can lead to coupling, so it is recommended to construct dedicated data objects for each rule.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
