Building a Flexible Java Rule Engine: Design, Code, and Tips

This article walks through the design and implementation of a Java rule engine, illustrating how to abstract rules, create a reusable executor with AND/OR logic, and demonstrates the full code, while discussing its advantages, drawbacks, and practical usage in real‑world applications.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Building a Flexible Java Rule Engine: Design, Code, and Tips

Business Scenario

Recently I received a small requirement to extend the existing trial‑user application rules. The original logic was expressed with a series of if‑else statements such as:

if (isOverseasUser) {
    return false;
}
if (fraudUser) {
    return false;
}
if (unpaidUser && outOfServicePeriod) {
    return false;
}
if (referralUser || paidUser || internalUser) {
    return true;
}

From this we can see that the workflow is based on AND/OR relationships, needs short‑circuit evaluation, and the current implementation, while functional, is hard to maintain.

Rule Executor Design

I designed a reusable rule executor (V1) that separates rule definition, data transfer object, and execution logic.

Rule Abstraction

Diagram
Diagram

The core interfaces and abstract classes are:

@Data
public class RuleDto {
    private String address;
    private int age;
}
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;
    }
}
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;
    }
}
public class NationalityRule extends AbstractRule {
    @Override
    protected <T> T convert(RuleDto dto) {
        NationalityRuleDto nr = new NationalityRuleDto();
        if (dto.getAddress().startsWith(MATCH_ADDRESS_START)) {
            nr.setNationality(MATCH_NATIONALITY_START);
        }
        return (T) nr;
    }
    @Override
    protected <T> boolean executeRule(T t) {
        System.out.println("NationalityRule invoke!");
        NationalityRuleDto nr = (NationalityRuleDto) t;
        if (nr.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 execution
        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, and the separation of rule, data, and executor makes the caller code tidy.

The convert method in the abstract rule allows custom data transformation for specific rule scenarios.

Disadvantages

All rules share a common DTO, creating data coupling; a better approach is to construct dedicated data for each rule.

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.

Design PatternsJavaRule EngineBackend Developmentcode refactoring
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.