Building a Flexible Java Rule Engine: Design, Code, and Tips
This article walks through the design and implementation of a Java rule engine, illustrating how to abstract rules, create a reusable executor with AND/OR logic, and demonstrates the full code, while discussing its advantages, drawbacks, and practical usage in real‑world applications.
Business Scenario
Recently I received a small requirement to extend the existing trial‑user application rules. The original logic was expressed with a series of if‑else statements such as:
if (isOverseasUser) {
return false;
}
if (fraudUser) {
return false;
}
if (unpaidUser && outOfServicePeriod) {
return false;
}
if (referralUser || paidUser || internalUser) {
return true;
}From this we can see that the workflow is based on AND/OR relationships, needs short‑circuit evaluation, and the current implementation, while functional, is hard to maintain.
Rule Executor Design
I designed a reusable rule executor (V1) that separates rule definition, data transfer object, and execution logic.
Rule Abstraction
The core interfaces and abstract classes are:
@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;
}
}
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;
}
}
public class NationalityRule extends AbstractRule {
@Override
protected <T> T convert(RuleDto dto) {
NationalityRuleDto nr = new NationalityRuleDto();
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
nr.setNationality(MATCH_NATIONALITY_START);
}
return (T) nr;
}
@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 = "中国";
}Executor Construction
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) {
boolean execute = rule.execute(dto);
if (!execute) {
return false;
}
}
return true;
}
private boolean or(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
boolean execute = rule.execute(dto);
if (execute) {
return true;
}
}
return false;
}
}Executor Invocation
public class RuleServiceTest {
@org.junit.Test
public void execute() {
// define rules
AgeRule ageRule = new AgeRule();
NameRule nameRule = new NameRule();
NationalityRule nationalityRule = new NationalityRule();
AddressRule addressRule = new AddressRule();
SubjectRule subjectRule = new SubjectRule();
// build DTO
RuleDto dto = new RuleDto();
dto.setAge(5);
dto.setName("张三");
dto.setAddress("北京");
dto.setSubject("数学");
// chain execution
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
Advantages
The engine is simple; each rule is independent, and the separation of rule, data, and executor makes the caller code tidy.
The convert method in the abstract rule allows custom data transformation for specific rule scenarios.
Disadvantages
All rules share a common DTO, creating data coupling; a better approach is to construct dedicated data 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.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
