How to Build a Flexible Java Rule Engine: Design, Templates, and Execution
This article explains how to design and implement a rule engine in Java using abstract base rules, template methods, and logical connectors (AND, OR, NOT), providing code examples, execution flow, and a discussion of its advantages and drawbacks.
Business Background
Recently a small requirement emerged to extend existing trial user application rules. The scenario involves evaluating users based on conditions such as overseas status, fraudulent activity, payment status, referral status, and more.
if (是否海外用户) {
return false;
}
if (刷单用户) {
return false;
}
if (未付费用户 && 不再服务时段) {
return false;
}
if (转介绍用户 || 付费用户 || 内推用户) {
return true;
} else {
return false;
}The conclusions are:
The main process relies on and or or relationships.
If a condition does not match, the subsequent flow can be short‑circuited.
Modifying the existing code is feasible for small changes, but long‑term maintainability would suffer.
Therefore, a refactor is planned.
Rule Executor
The design abstracts rules, defines a rule template, and allows concrete rule implementations. A version V1 is presented, showing the overall design and implementation flow.
Design of the Rule Executor
The design draws inspiration from the Strategy and Specification patterns, converting natural‑language rules into executable code. In a DDD context, a DSL or rule template can be used to customize specific rule strategies.
The processing steps are:
Construct business data such as user basics and status.
Obtain the specific rule list from a rule factory based on the current context.
Invoke the rule execution method to obtain the result.
During execution, handle logical connectors like and, or, and not.
Abstract Rules and Template Definition
Define BaseRule as an abstract rule with an execute method. AbstractRule provides a template with extension points convert and executeRule. Concrete implementations such as AddressRule and NationalityRule extend this template.
// Business data
@Data
public class RuleDto {
private String address;
private int age;
}
// Rule abstraction
public interface BaseRule {
boolean execute(RuleDto dto);
}
// Rule template
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;
}
}
// Concrete rule – example 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;
}
}
// Concrete rule – example 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 nationalityRuleDto = (NationalityRuleDto) t;
if (nationalityRuleDto.getNationality().startsWith(MATCH_NATIONALITY_START)) {
return true;
}
return false;
}
}
// Constants
public class RuleConstant {
public static final String MATCH_ADDRESS_START = "北京";
public static final String MATCH_NATIONALITY_START = "中国";
}Core Construction of the Rule Executor
The RuleService class links rules and provides a fluent API to compose AND, OR, and NOT relationships. It stores rule groups in a map and executes them according to their logical connector.
public class RuleService {
private Map<Integer, List<BaseRule>> hashMap = new HashMap<>();
private static final int NOT = 2;
private static final int AND = 1;
private static final int OR = 0;
private RuleDto ruleDto;
public static RuleService create(RuleDto ruleDto) {
RuleService ruleService = new RuleService();
ruleService.ruleDto = ruleDto;
return 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 RuleService not(List<BaseRule> ruleList) {
hashMap.put(NOT, ruleList);
return this;
}
public boolean execute() {
return this.execute(ruleDto);
}
private 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 (!andRule(dto, ruleList)) return false;
break;
case OR:
System.out.println("execute key = " + 0);
if (!orRule(dto, ruleList)) return false;
break;
case NOT:
System.out.println("execute key = " + 2);
if (!notRule(dto, ruleList)) return false;
break;
default:
break;
}
}
return true;
}
private boolean andRule(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
if (!rule.execute(dto)) return false;
}
return true;
}
private boolean orRule(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
if (rule.execute(dto)) return true;
}
return false;
}
private boolean notRule(RuleDto dto, List<BaseRule> ruleList) {
return !andRule(dto, ruleList);
}
}
public class RuleServices {
public static RuleService isValidStudent(RuleDto ruleDto) {
AgeRule ageRule = new AgeRule();
NameRule nameRule = new NameRule();
NationalityRule nationalityRule = new NationalityRule();
AddressRule addressRule = new AddressRule();
SubjectRule subjectRule = new SubjectRule();
Flag110Rule flag110Rule = new Flag110Rule();
return RuleService
.create(ruleDto)
.and(Arrays.asList(nationalityRule, nameRule, addressRule))
.or(Arrays.asList(ageRule, subjectRule))
.not(Collections.singletonList(flag110Rule));
}
}Client Invocation Code
The client builds the DTO, obtains the rule service from the factory, and executes the rule chain.
public class RuleServiceTest {
@org.junit.Test
public void execute() {
// 1. Build DTO
RuleDto dto = new RuleDto();
dto.setAge(5);
dto.setName("张三");
dto.setAddress("北京");
dto.setSubject("数学");
// 2. Initialize rule service
RuleService ruleService = RuleServices.isValidStudent(dto);
// 3. Execute
boolean ruleResult = ruleService.execute();
System.out.println("this student rule execute result :" + ruleResult);
}
}Summary
Advantages :
Simple and modular – each rule is independent, separating rule logic, data, and executor for clean client usage.
The convert method in the template allows custom data transformation for specific rule needs.
Disadvantages :
Rules share the same DTO, creating data coupling; modifying the DTO arbitrarily is not ideal, suggesting the use of intermediate storage for temporary data.
References
https://www.codenong.com/30430818
https://cloud.tencent.com/developer/article/1528935
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.
Ops Development Stories
Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.
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.
