Master Conditional Logic with Strategy, SPI, Chain of Responsibility & Rule Engine Patterns
This article explains how to replace tangled if‑else code by applying four elegant design solutions—Strategy pattern, Java SPI mechanism, Chain of Responsibility, and a rule engine—showing concepts, advantages, and practical Java examples for cleaner, more maintainable software.
1 Strategy Pattern
01 Concept
The Strategy Pattern (a behavioral design pattern) defines a family of algorithms, encapsulates each one, and makes them interchangeable, allowing the algorithm to vary independently from the client that uses it.
It is useful when a feature can be implemented by multiple algorithms and the appropriate one must be selected at runtime based on context or conditions.
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 pattern’s design introduces three roles:
Context – the environment class that holds a reference to a Strategy.
Strategy – an abstract interface defining the algorithm.
ConcreteStrategy – concrete classes implementing the algorithm.
02 Example
Step 1: Create the Strategy interface
public interface Strategy {
int doOperation(int num1, int num2);
}Step 2: Implement concrete strategies
public class OperationAdd implements Strategy {
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract 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;
}
}Step 3: Create the Context class
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);
}
}Step 4: Use the Context to switch strategies
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 OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
context.setStrategy(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}Advantages of the Strategy pattern include perfect support for the Open/Closed principle, easy addition of new algorithms, and elimination of complex conditional statements. Drawbacks are that the client must be aware of all concrete strategy classes and the pattern can lead to a proliferation of small classes.
2 SPI Mechanism
01 Concept
SPI (Service Provider Interface) is a service‑discovery mechanism that loads implementations of an interface at runtime by reading fully‑qualified class names from configuration files.
02 Java SPI – JDBC Driver
Before JDBC 4.0 developers manually loaded the driver class with Class.forName("com.mysql.jdbc.Driver"). After JDBC 4.0 the driver is discovered via SPI, eliminating the explicit loading step.
// 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);With SPI the driver manager reads META-INF/services/java.sql.Driver files, creates a ServiceLoader<Driver>, and iterates over the discovered drivers.
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while (driversIterator.hasNext()) {
driversIterator.next(); // each call loads the concrete driver class
}The driver that can handle the supplied JDBC URL returns a non‑null connection; the first such driver is used.
03 Dubbo SPI – On‑Demand Loading
Dubbo implements its own SPI (ExtensionLoader) to overcome Java SPI’s limitation of loading all implementations eagerly. Dubbo’s configuration uses key‑value pairs placed under META-INF/dubbo.
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.BumblebeeExample usage:
ExtensionLoader<Robot> loader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = loader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = loader.getExtension("bumblebee");
bumblebee.sayHello();Dubbo SPI provides full decoupling, allows adding new implementations without changing core code, and supports runtime dynamic discovery.
3 Chain of Responsibility Pattern
01 Concept
The Chain of Responsibility pattern passes a request along a chain of handlers; each handler may process the request or forward it to the next handler.
Typical use cases include multiple objects capable of handling the same request, where the concrete handler is chosen at runtime, or when the processing order matters.
02 Example – Composite Payment
A real‑world example refactors a payment flow that originally contained many nested if/else statements.
1. Define the abstract handler
public interface PaymentHandler {
boolean canProcess(String userId);
PaymentResult processPayment(String userId, double amount);
}
public class PaymentResult {
private boolean success;
private double paidAmount;
private String message;
public PaymentResult(boolean success, double paidAmount, String message) {
this.success = success;
this.paidAmount = paidAmount;
this.message = message;
}
// getters omitted for brevity
}2. Concrete handlers
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 payment succeeded(%.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; // all users can use personal account
}
@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 payment succeeded(%.2f), remaining: %.2f", MAX_PERSONAL_PAYMENT, amount - MAX_PERSONAL_PAYMENT));
}
}
}3. Composite context (the chain)
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 r = handler.processPayment(userId, remaining);
results.put(handler.getClass().getSimpleName(), r);
if (r.isSuccess()) {
remaining -= r.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. Test class
public class CompositePaymentSystem {
public static void main(String[] args) {
CompositePaymentContext ctx = new CompositePaymentContext();
ctx.addHandler(new CorporatePaymentHandler()); // corporate first
ctx.addHandler(new PersonalPaymentHandler()); // then personal
// Test case 1: corporate user, amount within corporate limit
System.out.println("
=== Test 1 ===");
ctx.executePayment("emp_789", 3000).forEach((k, v) ->
System.out.printf("[%s] %s (paid: %.2f)%n", k, v.getMessage(), v.getPaidAmount()));
// Test case 2: corporate user, amount exceeds corporate limit – needs both handlers
System.out.println("
=== Test 2 ===");
ctx.executePayment("emp_789", 6000).forEach((k, v) ->
System.out.printf("[%s] %s (paid: %.2f)%n", k, v.getMessage(), v.getPaidAmount()));
// Test case 3: personal user only
System.out.println("
=== Test 3 ===");
ctx.executePayment("user123", 1500).forEach((k, v) ->
System.out.printf("[%s] %s (paid: %.2f)%n", k, v.getMessage(), v.getPaidAmount()));
}
}4 Rule Engine
01 Concept
In e‑commerce, marketing rules such as “spend 1000, get 200 off; spend 500, get 100 off” are often hard‑coded with if/else. When rules change frequently, maintaining code becomes costly. A rule engine separates business rules from code, allowing non‑technical users to edit them.
Reduces development effort.
Enables business users to configure rules independently.
Improves rule transparency and deployment speed.
02 Example – AviatorScript
Rule stored as a script:
if (amount >= 1000) {
return 200;
} elsif (amount >= 500) {
return 100;
} else {
return 0;
}Utility 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 1005 Summary
In everyday development we often face “condition explosion” where many if/else branches make code hard to read and maintain. The article presented four design patterns—Strategy, SPI, Chain of Responsibility, and Rule Engine—to address this problem, each with concepts, advantages, and concrete Java examples.
01 Strategy Pattern – Flexible algorithm switching
Core idea: encapsulate algorithms, make them interchangeable, follow Open/Closed principle.
02 SPI Mechanism – Elegant service discovery
Core idea: load implementations via configuration files, enabling plug‑in architectures.
03 Chain of Responsibility – Linear processing flow
Core idea: decouple sender and receiver, allow multiple handlers to process a request.
04 Rule Engine – Dynamic business rule management
Core idea: externalize rules so they can be edited without code changes.
When choosing a solution, always respect design principles such as Single Responsibility, Open/Closed, high cohesion & low coupling, and KISS to produce maintainable, extensible code.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
