How to Build a Flexible Rule Engine with AND/OR Logic in Java
This article walks through the design and implementation of a Java rule engine that supports both AND and OR logical relationships, demonstrates short‑circuit evaluation, and shows how to structure rules, a service, and test code for maintainable, extensible business logic.
Rule Engine Overview
We start from a business scenario where trial‑user eligibility is determined by a series of conditions such as overseas user, fraudulent user, unpaid user, referral user, etc. The logic can be expressed with if statements that return true or false.
The main workflow relies on AND or OR relationships between rules.
If any condition fails in an AND chain, execution short‑circuits and stops.
While a quick patch is possible, the original design would become hard to maintain.
Consequently, a refactor is proposed.
Rule Engine Design
Architecture Diagram
Abstraction of Rules
// 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 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;
}
}
// Constants
public class RuleConstant {
public static final String MATCH_ADDRESS_START = "北京";
public static final String MATCH_NATIONALITY_START = "中国";
}Engine 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:
// AND relationship – all must pass
System.out.println("execute key = " + 1);
if (!and(dto, ruleList)) {
return false;
}
break;
case OR:
// OR relationship – any pass
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) {
// short‑circuit on first failure
return false;
}
}
return true;
}
private boolean or(RuleDto dto, List<BaseRule> ruleList) {
for (BaseRule rule : ruleList) {
boolean execute = rule.execute(dto);
if (execute) {
// short‑circuit on first success
return true;
}
}
return false;
}
}Engine Invocation Example
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 calls to construct and execute
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, separating rule logic, data, and executor for clean usage.
Using a convert method in the abstract rule allows each concrete rule to adapt the DTO to its specific needs.
Disadvantages
Rules share a common DTO, creating hidden data dependencies; it is better to construct dedicated data objects for each rule to avoid tight coupling.
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.
Java Interview Crash Guide
Dedicated to sharing Java interview Q&A; follow and reply "java" to receive a free premium Java interview guide.
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.
