Backend Development 8 min read

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.

Architect's Guide
Architect's Guide
Architect's Guide
How to Build a Flexible Java Rule Engine with AND/OR Logic

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.

backenddesign patternsJavarule engineCode Example
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

login 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.