Design and Implementation of a Java Rule Engine with AND/OR Logic

This article presents a Java-based rule engine design that separates rule definitions, data transfer objects, and execution logic, demonstrating AND/OR composition, abstract rule templates, concrete implementations, and a fluent API for building and executing complex rule chains.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Design and Implementation of a Java Rule Engine with AND/OR Logic

The article introduces a rule engine refactoring scenario where existing trial‑user application rules need to be extended, highlighting the need for short‑circuit evaluation and maintainable logic.

if (是否海外用户) {
    return false;
}

if (刷单用户) {
    return false;
}

if (未付费用户 && 不再服务时段) {
    return false;
}

if (转介绍用户 || 付费用户 || 内推用户) {
    return true;
}

To address this, a rule executor is designed with a V1 version that separates rule abstraction, templates, and concrete implementations. The core abstractions include a RuleDto data class, a BaseRule interface, and an AbstractRule class that handles conversion and delegation.

// 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 = "中国";
}

The executor is built with a fluent API that stores rule lists under AND (synchronous) or OR (short‑circuit) keys and iterates through them during execution.

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

A test class demonstrates how to instantiate concrete rules, create a RuleDto with sample data, and execute the composed rule chain using the fluent API.

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("数学");

        // Execute with fluent builder
        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);
    }
}

The conclusion lists the advantages of this approach—simplicity, modularity, and extensibility via the convert method—and its drawbacks, mainly the tight coupling to a shared DTO, suggesting that data should be prepared in advance for better decoupling.

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 PatternsJavaAND/OR LogicFluent API
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.