How to Build a Flexible Java Rule Engine with AND/OR Logic
This article walks through designing and implementing a Java rule engine that supports both AND and OR logical relationships, showing the core rule abstractions, concrete rule examples, the builder pattern for composing rules, and a discussion of its advantages and drawbacks.
Rule Execution Overview
We need to extend an existing trial‑user application rule with additional conditions such as overseas users, fraudulent users, unpaid users, and various referral or paid user types. The logic can be expressed with simple boolean checks that return false or true based on the criteria.
<code>if (是否海外用户) {
return false;
}
if (刷单用户) {
return false;
}
if (未付费用户 && 不再服务时段) {
return false;
}
if (转介绍用户 || 付费用户 || 内推用户) {
return true;
}
</code>To make the engine maintainable we introduce a short‑circuit mechanism that stops evaluation as soon as the overall result is determined.
Design of the Rule Engine
The engine is built around three core concepts:
BaseRule : an interface defining boolean execute(RuleDto dto) .
AbstractRule : an abstract class providing a conversion hook and delegating execution to executeRule .
Concrete rules : implementations such as AddressRule and NationalityRule that contain the actual business checks.
<code>// 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 – AddressRule
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 – NationalityRule
public class NationalityRule extends AbstractRule {
@Override
protected <T> T convert(RuleDto dto) {
NationalityRuleDto nrDto = new NationalityRuleDto();
if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
nrDto.setNationality(MATCH_NATIONALITY_START);
}
return (T) nrDto;
}
@Override
protected <T> boolean executeRule(T t) {
System.out.println("NationalityRule invoke!");
NationalityRuleDto nrDto = (NationalityRuleDto) t;
if (nrDto.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 = "中国";
}
</code>Builder and Execution
The RuleService class holds a map of rule lists keyed by logical relation (AND = 1, OR = 0). It provides fluent and() and or() methods to compose the rule sets and an execute() method that iterates over the map, applying the appropriate short‑circuit logic.
<code>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:
if (!and(dto, ruleList)) {
return false;
}
break;
case OR:
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;
}
}
</code>Usage Example
<code>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);
}
}
</code>Pros and Cons
Advantages : Simple and modular – each rule is independent, and the separation of rule, data, and executor makes the calling code tidy. The convert method in the template allows extending data for specific rules.
Disadvantages : All rules share the same DTO, creating hidden data dependencies; a better approach would be to construct dedicated data objects for each rule to avoid mutating a common DTO.
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.