Building a Flexible Java Rule Engine with AND/OR Logic

This article walks through designing and implementing a Java rule engine that supports AND/OR relationships and short‑circuit evaluation, presents the core abstractions, shows concrete rule examples, and evaluates the pros and cons of the approach.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Building a Flexible Java Rule Engine with AND/OR Logic

Business Scenario

Recently a small requirement emerged to extend an existing trial‑user application rule. The logic checks whether a user is overseas, a fraudulent user, an unpaid user outside service periods, or a referred/paid/internal‑recommended user, returning true only for the latter cases.

if (是否海外用户) {
    return false;
}
if (刷单用户) {
    return false;
}
if (未付费用户 && 不再服务时段) {
    return false;
}
if (转介绍用户 || 付费用户 || 内推用户) {
    return true;
}

The analysis highlights three points: the main flow relies on AND/OR relationships, a short‑circuit mechanism is needed when a condition fails, and while the current extension is simple, its long‑term maintainability is poor.

Rule Executor Design

The solution introduces a version V1 rule executor. The core design abstracts rules, provides a conversion hook, and implements execution templates.

Rule Abstraction and Implementation

@Data
public class RuleDto {
    private String address;
    private int age;
    // other fields...
}

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;
    }
}

// Example concrete rule – 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;
    }
}

// Example concrete rule – NationalityRule
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;
    }
}

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 calls to build 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);
    }
}

Conclusion

The rule executor offers simplicity and modularity—each rule is independent, and the separation of rule, data, and executor keeps the caller tidy. The template’s convert method allows rule‑specific data transformation. However, the design introduces data coupling through a shared DTO, which can be problematic; constructing immutable data objects beforehand is recommended.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendDesign PatternsJavarule engineAND/OR Logic
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.