How to Build a Flexible Rule Engine with Java: Design, Implementation, and Tips

This article walks through a real‑world scenario of extending trial‑user eligibility rules, explains the logical flow using AND/OR short‑circuiting, and presents a modular Java rule engine design with abstract templates, concrete rules, a service builder, and a test case, highlighting its advantages and drawbacks.

Programmer DD
Programmer DD
Programmer DD
How to Build a Flexible Rule Engine with Java: Design, Implementation, and Tips

Business Scenario

Recently a small requirement emerged to extend existing trial‑user application rules. The conditions are expressed as a series of checks that return false for overseas users, fraudulent users, unpaid users outside service periods, and return true for referred, paid, or internally recommended users.

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

The logic relies on AND/OR relationships with short‑circuit evaluation, and while the current implementation works, its maintainability is poor.

Rule Engine Design

The engine is split into a data transfer object, rule abstractions, concrete rule implementations, and a service that orchestrates rule execution.

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

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 can 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) {
                // AND failed once
                return false;
            }
        }
        // All AND passed
        return true;
    }

    private boolean or(RuleDto dto, List<BaseRule> ruleList) {
        for (BaseRule rule : ruleList) {
            boolean execute = rule.execute(dto);
            if (execute) {
                // OR succeeded
                return true;
            }
        }
        // OR none succeeded
        return false;
    }
}

Engine 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 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 of Pros and Cons

Advantages

Simple and modular; each rule is independent, separating rule logic, data, and executor for clean usage.

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

Disadvantages

Rules share a common DTO, creating data coupling; it is better to construct immutable data structures ahead of time.

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 engineSoftware ArchitectureBackend DevelopmentCode Example
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

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.