Master Java Design Patterns: Strategy, SPI, Chain of Responsibility & Rule Engine

Learn how to replace tangled if‑else logic with four elegant solutions—Strategy pattern, Java SPI, Chain of Responsibility, and a rule engine—complete with concepts, UML diagrams, Java code examples, advantages, drawbacks, and practical usage scenarios for building maintainable, extensible backend systems.

macrozheng
macrozheng
macrozheng
Master Java Design Patterns: Strategy, SPI, Chain of Responsibility & Rule Engine

1 Strategy Pattern

01 Concept

Strategy Pattern is a behavioral design pattern that defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the clients that use it.

In practice, when a feature can be implemented by multiple algorithms, the appropriate algorithm is selected at runtime based on the environment or conditions.

This essentially replaces large if/else blocks.

Single Responsibility Principle : a class should have only one reason to change.

Open/Closed Principle : software entities should be open for extension but closed for modification.

The design introduces three roles:

Context – the environment class.

Strategy – the abstract strategy interface.

ConcreteStrategy – a concrete implementation of a specific algorithm.

Corresponding sequence diagram:

Simple code demonstration:

public interface Strategy {
    int doOperation(int num1, int num2);
}

public class OperationAdd implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 + num2;
    }
}

public class OperationMultiply implements Strategy {
    @Override
    public int doOperation(int num1, int num2) {
        return num1 * num2;
    }
}

public class Context {
    private Strategy strategy;
    public void setStrategy(Strategy strategy) {
        this.strategy = strategy;
    }
    public int executeStrategy(int num1, int num2) {
        return strategy.doOperation(num1, num2);
    }
}

public class Main {
    public static void main(String[] args) {
        Context context = new Context();
        context.setStrategy(new OperationAdd());
        System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
        context.setStrategy(new OperationMultiply());
        System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
    }
}

Advantages: perfect support for the Open/Closed principle, easy addition of new algorithms, and elimination of complex conditional statements.

Disadvantages: clients must know all concrete strategy classes and may need to manage many strategy classes.

2 SPI Mechanism

01 Concept

SPI (Service Provider Interface) is a service‑discovery mechanism. Implementations are listed by their fully‑qualified class names in configuration files, and a service loader reads these files to load the classes at runtime, enabling dynamic replacement of implementations.

02 Java SPI: JDBC Driver

Before JDBC 4.0, developers manually loaded the driver class with Class.forName("com.mysql.jdbc.Driver"). Since JDBC 4.0, the driver is loaded automatically via Java SPI.

// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url, username, password);

Driver loading involves four steps:

Read driver definitions from system properties.

Use SPI to obtain implementation class names.

Iterate over the discovered implementations and instantiate them.

Instantiate the class that matches the JDBC URL.

Key code snippets:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while (driversIterator.hasNext()) {
    driversIterator.next();
}

If multiple drivers are found, the first one that accepts the URL is used.

03 Dubbo SPI (On‑Demand Loading)

Dubbo implements its own SPI to overcome Java SPI’s limitation of loading all implementations at once. Configuration files are placed under META-INF/dubbo with key‑value pairs, allowing selective loading.

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

Example usage:

ExtensionLoader<Robot> loader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = loader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = loader.getExtension("bumblebee");
bumblebee.sayHello();

Dubbo SPI also adds IOC and AOP features.

3 Chain of Responsibility Pattern

01 Concept

Chain of Responsibility is a behavioral pattern that passes a request along a chain of handlers. Each handler decides either to process the request or to forward it to the next handler.

Typical use cases include multiple objects capable of handling the same request, dynamic determination of the handler, and ordered processing where the sequence matters.

02 Example – Composite Payment

Payment processing often requires checking various accounts (corporate, personal, coupons) and applying the first applicable strategy. The following code demonstrates a full chain implementation.

public interface PaymentHandler {
    boolean canProcess(String userId);
    PaymentResult processPayment(String userId, double amount);
}

public class CorporatePaymentHandler implements PaymentHandler {
    private static final double MAX_CORPORATE_PAYMENT = 5000;
    @Override
    public boolean canProcess(String userId) {
        return userId.startsWith("emp_");
    }
    @Override
    public PaymentResult processPayment(String userId, double amount) {
        System.out.printf("Attempt corporate payment: user[%s], amount[%.2f]%n", userId, amount);
        if (amount <= MAX_CORPORATE_PAYMENT) {
            return new PaymentResult(true, amount, "Corporate payment succeeded");
        } else {
            return new PaymentResult(true, MAX_CORPORATE_PAYMENT,
                String.format("Corporate partial success (%.2f), remaining %.2f", MAX_CORPORATE_PAYMENT, amount - MAX_CORPORATE_PAYMENT));
        }
    }
}

public class PersonalPaymentHandler implements PaymentHandler {
    private static final double MAX_PERSONAL_PAYMENT = 1000;
    @Override
    public boolean canProcess(String userId) { return true; }
    @Override
    public PaymentResult processPayment(String userId, double amount) {
        System.out.printf("Attempt personal payment: user[%s], amount[%.2f]%n", userId, amount);
        if (amount <= MAX_PERSONAL_PAYMENT) {
            return new PaymentResult(true, amount, "Personal payment succeeded");
        } else {
            return new PaymentResult(true, MAX_PERSONAL_PAYMENT,
                String.format("Personal partial success (%.2f), remaining %.2f", MAX_PERSONAL_PAYMENT, amount - MAX_PERSONAL_PAYMENT));
        }
    }
}

public class CompositePaymentContext {
    private final List<PaymentHandler> handlers = new ArrayList<>();
    public void addHandler(PaymentHandler handler) { handlers.add(handler); }
    public Map<String, PaymentResult> executePayment(String userId, double totalAmount) {
        Map<String, PaymentResult> results = new LinkedHashMap<>();
        double remaining = totalAmount;
        for (PaymentHandler handler : handlers) {
            if (handler.canProcess(userId) && remaining > 0) {
                PaymentResult result = handler.processPayment(userId, remaining);
                results.put(handler.getClass().getSimpleName(), result);
                if (result.isSuccess()) {
                    remaining -= result.getPaidAmount();
                    if (remaining <= 0) break;
                }
            }
        }
        if (remaining > 0) {
            results.put("Remaining", new PaymentResult(false, remaining,
                String.format("Payment incomplete, remaining %.2f", remaining)));
        }
        return results;
    }
}

4 Rule Engine

01 Concept

Business rules (e.g., "Spend 1000, get 200 off; spend 500, get 100 off") are often hard‑coded with if/else statements, making frequent changes costly. A rule engine separates rules from code, allowing non‑technical users to edit them.

02 Example – AviatorScript

if (amount>=1000){
    return 200;
}elsif(amount>=500){
    return 100;
}else{
    return 0;
}

Java helper method to evaluate the script:

public static BigDecimal getDiscount(BigDecimal amount, String rule) {
    Map<String, Object> env = new HashMap<>();
    env.put("amount", amount);
    Expression expression = AviatorEvaluator.compile(DigestUtils.md5Hex(rule.getBytes()), rule, true);
    Object result = expression.execute(env);
    if (result != null) {
        return new BigDecimal(String.valueOf(result));
    }
    return null;
}

Test execution:

String rule = "if (amount>=1000){
    return 200;
}elsif(amount>=500){
    return 100;
}else{
    return 0;
}";
BigDecimal discount = getDiscount(new BigDecimal("600"), rule);
System.out.println("discount:" + discount); // prints 100

5 Summary

In real development, conditional logic often leads to tangled if/else code, reducing readability and maintainability. The article presents five elegant solutions:

Strategy Pattern – encapsulates algorithms and enables runtime switching.

SPI Mechanism – provides a plug‑in architecture for dynamic service discovery.

Chain of Responsibility – decouples sender and receiver, allowing flexible processing pipelines.

Rule Engine – externalizes business rules for easy modification without code changes.

Design Principles – Single Responsibility, Open/Closed, high cohesion & low coupling, and KISS guide the selection of the appropriate technique.

Choosing the right pattern based on the specific scenario yields maintainable, extensible, and low‑coupled code.

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.

Chain of ResponsibilityJavarule engineStrategy PatternSPI
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

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.