Eliminate if‑else with Annotation‑Driven Strategy Pattern in Spring Boot

This article explains how to replace bulky if‑else statements with a clean, annotation‑based Strategy pattern in a Spring Boot application, covering interface definition, custom annotations, dynamic proxy handling, map injection, and the crucial hashCode/equals overrides for reliable handler lookup.

Java Backend Technology
Java Backend Technology
Java Backend Technology
Eliminate if‑else with Annotation‑Driven Strategy Pattern in Spring Boot

Strategy Pattern

Many articles claim that the Strategy pattern can replace cumbersome if‑else chains. The core idea is to define a common behavior (interface or abstract class) and provide different implementations for each strategy, letting the client select the appropriate implementation, often via a factory.

Annotation Implementation

This guide shows how to implement the Strategy pattern using custom annotations, with a concrete example of order processing.

First, define the Order entity:

@Data
public class Order {
    /** Order source */
    private String source;
    /** Payment method */
    private String payMethod;
    /** Order code */
    private String code;
    /** Order amount */
    private BigDecimal amount;
    // ... other fields
}

Assume the service currently contains a large if‑else block that handles orders differently based on their source (pc or mobile):

@Service
public class OrderService {
    public void orderService(Order order) {
        if (order.getSource().equals("pc")) {
            // handle PC order
        } else if (order.getSource().equals("mobile")) {
            // handle mobile order
        } else {
            // other logic
        }
    }
}

The goal is to remove this block.

Strategy pattern diagram
Strategy pattern diagram

1. Define a unified OrderHandler interface:

public interface OrderHandler {
    void handle(Order order);
}

2. Create a custom annotation to mark which handler processes which source:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Service
public @interface OrderHandlerType {
    String source();
}

3. Implement handlers for each source and annotate them:

@OrderHandlerType(source = "mobile")
public class MobileOrderHandler implements OrderHandler {
    @Override
    public void handle(Order order) {
        System.out.println("处理移动端订单");
    }
}

@OrderHandlerType(source = "pc")
public class PCOrderHandler implements OrderHandler {
    @Override
    public void handle(Order order) {
        System.out.println("处理PC端订单");
    }
}

4. Inject all handlers into a map keyed by the annotation value:

@Service
public class OrderService {
    private Map<String, OrderHandler> orderHandleMap;

    @Autowired
    public void setOrderHandleMap(List<OrderHandler> orderHandlers) {
        orderHandleMap = orderHandlers.stream()
            .collect(Collectors.toMap(
                handler -> AnnotationUtils.findAnnotation(handler.getClass(), OrderHandlerType.class).source(),
                v -> v,
                (v1, v2) -> v1));
    }

    public void orderService(Order order) {
        // ...pre‑processing
        OrderHandler handler = orderHandleMap.get(order.getSource());
        handler.handle(order);
        // ...post‑processing
    }
}

Now the original if‑else disappears, replaced by a simple map lookup.

When business requirements evolve (e.g., handling both source and payment method), the map key can be extended. One approach is to make the key the annotation itself:

private Map<OrderHandlerType, OrderHandler> orderHandleMap;

@Autowired
public void setOrderHandleMap(List<OrderHandler> orderHandlers) {
    orderHandleMap = orderHandlers.stream()
        .collect(Collectors.toMap(
            handler -> AnnotationUtils.findAnnotation(handler.getClass(), OrderHandlerType.class),
            v -> v,
            (v1, v2) -> v1));
}

To retrieve a handler based on both source and payment method, implement a concrete class that mimics the annotation proxy and overrides hashCode and equals exactly as the JDK dynamic proxy does:

public class OrderHandlerTypeImpl implements OrderHandlerType {
    private String source;
    private String payMethod;

    public OrderHandlerTypeImpl(String source, String payMethod) {
        this.source = source;
        this.payMethod = payMethod;
    }

    @Override
    public String source() { return source; }

    @Override
    public String payMethod() { return payMethod; }

    @Override
    public Class<? extends Annotation> annotationType() { return OrderHandlerType.class; }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (127 * "source".hashCode()) ^ source.hashCode();
        hash += (127 * "payMethod".hashCode()) ^ payMethod.hashCode();
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof OrderHandlerType)) return false;
        OrderHandlerType other = (OrderHandlerType) obj;
        return source.equals(other.source()) && payMethod.equals(other.payMethod());
    }
}

Using this implementation, the service can fetch the correct handler:

public void orderService(Order order) {
    OrderHandlerType key = new OrderHandlerTypeImpl(order.getSource(), order.getPayMethod());
    OrderHandler handler = orderHandleMap.get(key);
    handler.handle(order);
}

If hashCode and equals are not overridden, the map lookup fails with a NullPointerException because the proxy class used as the key differs from the custom implementation.

Dynamic proxy generation
Dynamic proxy generation

After adding the proper hashCode and equals methods, the handler is retrieved correctly, and the service logic remains unchanged regardless of future business variations.

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 PatternsJavaStrategy Patternspringdependency-injection
Java Backend Technology
Written by

Java Backend Technology

Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!

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.