Designing a Rule Engine for Multi‑Condition Decision Scenarios

The article explains why long chains of if‑else are hard to maintain, proposes a rule‑engine architecture with abstract BaseRule, concrete rule classes, and a RuleService that supports AND/OR short‑circuit execution, and evaluates its advantages and drawbacks with Java code examples.

IT Niuke
IT Niuke
IT Niuke
Designing a Rule Engine for Multi‑Condition Decision Scenarios

Business scenario

Original logic used a series of if‑else checks with short‑circuit behavior to decide whether a user is overseas, a fraud user, an unpaid user out of service period, or a referred/paid/internal‑recommended user. The requirements demanded a short‑circuit mechanism and highlighted maintainability problems of the monolithic if‑else chain.

Rule abstraction

A rule engine is built around three core types: RuleDto – a data‑transfer object that carries the input fields (e.g., address, age, name, subject). BaseRule – an interface defining a single method boolean execute(RuleDto dto). AbstractRule – an abstract class implementing BaseRule. It provides a generic convert helper that by default returns the original DTO and delegates execution to an abstract executeRule method that concrete rules override.

@Data
public class RuleDto {
    private String address;
    private int age;
    private String name;
    private String subject;
}

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

Concrete rule examples

Two concrete rules illustrate the pattern.

public class AddressRule extends AbstractRule {
    @Override
    public boolean execute(RuleDto dto) {
        System.out.println("AddressRule invoke!");
        return dto.getAddress().startsWith(RuleConstant.MATCH_ADDRESS_START);
    }
}

public class NationalityRule extends AbstractRule {
    @Override
    protected <T> T convert(RuleDto dto) {
        NationalityRuleDto nrDto = new NationalityRuleDto();
        if (dto.getAddress().startsWith(RuleConstant.MATCH_ADDRESS_START)) {
            nrDto.setNationality(RuleConstant.MATCH_NATIONALITY_START);
        }
        return (T) nrDto;
    }
    @Override
    protected <T> boolean executeRule(T t) {
        System.out.println("NationalityRule invoke!");
        NationalityRuleDto nrDto = (NationalityRuleDto) t;
        return nrDto.getNationality().startsWith(RuleConstant.MATCH_NATIONALITY_START);
    }
}

Constant values are defined in RuleConstant:

public class RuleConstant {
    public static final String MATCH_ADDRESS_START = "北京";
    public static final String MATCH_NATIONALITY_START = "中国";
}

Rule engine (RuleService)

The engine stores rule lists keyed by logical operators: AND = 1 and OR = 0. Fluent methods and(...) and or(...) add rule lists to an internal map. The execute method iterates over the map entries, dispatches to private and or or helpers, and returns false immediately when a short‑circuit condition fails.

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 = " + AND);
                    if (!and(dto, ruleList)) return false;
                    break;
                case OR:
                    System.out.println("execute key = " + 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;
    }
}

Engine usage example

A JUnit test assembles the engine, creates a RuleDto with sample data (age 5, name "张三", address "北京", subject "数学"), chains and and or calls, executes the rule set, and prints the result.

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

Observations

Advantages : simple structure; each rule is independent; clear separation of rule logic, data, and executor; the convert method in AbstractRule allows rule‑specific data transformation.

Drawbacks : all rules share a single RuleDto, creating data coupling; modifying the shared DTO directly is considered undesirable, suggesting that data objects should be constructed ahead of rule execution.

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.

backendJavaRule EnginedesignAND/OR
IT Niuke
Written by

IT Niuke

Focused on IT technology sharing, original and innovative content. IT Niuke, we grow together.

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.