How to Build a Flexible Java Rule Engine: Design, Templates, and Execution

This article explains how to design and implement a rule engine in Java using abstract base rules, template methods, and logical connectors (AND, OR, NOT), providing code examples, execution flow, and a discussion of its advantages and drawbacks.

Ops Development Stories
Ops Development Stories
Ops Development Stories
How to Build a Flexible Java Rule Engine: Design, Templates, and Execution

Business Background

Recently a small requirement emerged to extend existing trial user application rules. The scenario involves evaluating users based on conditions such as overseas status, fraudulent activity, payment status, referral status, and more.

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

The conclusions are:

The main process relies on and or or relationships.

If a condition does not match, the subsequent flow can be short‑circuited.

Modifying the existing code is feasible for small changes, but long‑term maintainability would suffer.

Therefore, a refactor is planned.

Rule Executor

The design abstracts rules, defines a rule template, and allows concrete rule implementations. A version V1 is presented, showing the overall design and implementation flow.

Design of the Rule Executor

The design draws inspiration from the Strategy and Specification patterns, converting natural‑language rules into executable code. In a DDD context, a DSL or rule template can be used to customize specific rule strategies.

The processing steps are:

Construct business data such as user basics and status.

Obtain the specific rule list from a rule factory based on the current context.

Invoke the rule execution method to obtain the result.

During execution, handle logical connectors like and, or, and not.

Abstract Rules and Template Definition

Define BaseRule as an abstract rule with an execute method. AbstractRule provides a template with extension points convert and executeRule. Concrete implementations such as AddressRule and NationalityRule extend this template.

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

Core Construction of the Rule Executor

The RuleService class links rules and provides a fluent API to compose AND, OR, and NOT relationships. It stores rule groups in a map and executes them according to their logical connector.

public class RuleService {
    private Map<Integer, List<BaseRule>> hashMap = new HashMap<>();
    private static final int NOT = 2;
    private static final int AND = 1;
    private static final int OR = 0;
    private RuleDto ruleDto;

    public static RuleService create(RuleDto ruleDto) {
        RuleService ruleService = new RuleService();
        ruleService.ruleDto = ruleDto;
        return 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 RuleService not(List<BaseRule> ruleList) {
        hashMap.put(NOT, ruleList);
        return this;
    }
    public boolean execute() {
        return this.execute(ruleDto);
    }
    private 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 (!andRule(dto, ruleList)) return false;
                    break;
                case OR:
                    System.out.println("execute key = " + 0);
                    if (!orRule(dto, ruleList)) return false;
                    break;
                case NOT:
                    System.out.println("execute key = " + 2);
                    if (!notRule(dto, ruleList)) return false;
                    break;
                default:
                    break;
            }
        }
        return true;
    }
    private boolean andRule(RuleDto dto, List<BaseRule> ruleList) {
        for (BaseRule rule : ruleList) {
            if (!rule.execute(dto)) return false;
        }
        return true;
    }
    private boolean orRule(RuleDto dto, List<BaseRule> ruleList) {
        for (BaseRule rule : ruleList) {
            if (rule.execute(dto)) return true;
        }
        return false;
    }
    private boolean notRule(RuleDto dto, List<BaseRule> ruleList) {
        return !andRule(dto, ruleList);
    }
}

public class RuleServices {
    public static RuleService isValidStudent(RuleDto ruleDto) {
        AgeRule ageRule = new AgeRule();
        NameRule nameRule = new NameRule();
        NationalityRule nationalityRule = new NationalityRule();
        AddressRule addressRule = new AddressRule();
        SubjectRule subjectRule = new SubjectRule();
        Flag110Rule flag110Rule = new Flag110Rule();
        return RuleService
                .create(ruleDto)
                .and(Arrays.asList(nationalityRule, nameRule, addressRule))
                .or(Arrays.asList(ageRule, subjectRule))
                .not(Collections.singletonList(flag110Rule));
    }
}

Client Invocation Code

The client builds the DTO, obtains the rule service from the factory, and executes the rule chain.

public class RuleServiceTest {
    @org.junit.Test
    public void execute() {
        // 1. Build DTO
        RuleDto dto = new RuleDto();
        dto.setAge(5);
        dto.setName("张三");
        dto.setAddress("北京");
        dto.setSubject("数学");
        // 2. Initialize rule service
        RuleService ruleService = RuleServices.isValidStudent(dto);
        // 3. Execute
        boolean ruleResult = ruleService.execute();
        System.out.println("this student rule execute result :" + ruleResult);
    }
}

Summary

Advantages :

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

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

Disadvantages :

Rules share the same DTO, creating data coupling; modifying the DTO arbitrarily is not ideal, suggesting the use of intermediate storage for temporary data.

References

https://www.codenong.com/30430818

https://cloud.tencent.com/developer/article/1528935

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 Patternsrule engineSoftware Architecture
Ops Development Stories
Written by

Ops Development Stories

Maintained by a like‑minded team, covering both operations and development. Topics span Linux ops, DevOps toolchain, Kubernetes containerization, monitoring, log collection, network security, and Python or Go development. Team members: Qiao Ke, wanger, Dong Ge, Su Xin, Hua Zai, Zheng Ge, Teacher Xia.

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.