Skip to content
How I Replaced 16 If-Else Blocks with Spring Boot Strategy Pattern (And Why It Works)

How I Replaced 16 If-Else Blocks with Spring Boot Strategy Pattern (And Why It Works)

This tutorial demonstrates how to refactor a 16-branch if-else chain in a Spring Boot order service using the Strategy Pattern, showing interface definition, strategy implementations, auto-registration via Spring's List injection, factory pattern for O(1) lookup, and common pitfalls to avoid.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast

The Problem: 16 If-Else Branches in One Method

The article opens with a real-world scenario: a new colleague inherits an order module where a single calculateDiscount method contains 16 if-else branches handling different order types (NORMAL, SECKILL, GROUP, PRESALE, etc.). Each branch embeds its own discount logic — e.g., normal orders get ¥10 off over ¥100, seckill orders get a flat ¥5, group orders get ¥30 off over ¥200, presale orders get 10% of amount. The method spans three screens and exhibits three classic problems:

Adding a new order type requires modifying this method and retesting all branches — high risk.

Reading the logic for one type forces scrolling through all preceding branches.

Unit testing a single discount rule requires executing the entire method.

This is the "branch explosion" anti-pattern. The solution presented is the Strategy Pattern integrated with Spring Boot's dependency injection.

Step 1: Define the Strategy Interface

The core interface OrderDiscountStrategy declares two methods:

public interface OrderDiscountStrategy {
    String getOrderType();
    BigDecimal calculateDiscount(Order order);
}
getOrderType()

returns the order type string (e.g., "NORMAL") and serves as the key for later Map-based lookup. The author emphasizes that omitting this method forces the factory to reintroduce if-else logic, defeating the purpose.

Step 2: Extract Each Branch into a Separate @Component Class

Four concrete implementations are shown, each annotated with @Component so Spring manages them:

NormalOrderStrategy : returns "NORMAL"; applies ¥10 discount when amount ≥ ¥100.

SeckillOrderStrategy : returns "SECKILL"; returns flat ¥5.

GroupOrderStrategy : returns "GROUP"; applies ¥30 discount when amount ≥ ¥200.

PresaleOrderStrategy : returns "PRESALE"; returns 10% of order amount.

Each class is short, single-responsibility, and independently testable. The @Component annotation is critical — without it, Spring will not detect the bean.

Step 3: Build a Factory That Auto-Registers All Strategies at Startup

OrderStrategyFactory

uses constructor injection of List<OrderDiscountStrategy>. Spring automatically populates this list with every @Component implementing the interface. The constructor iterates the list and builds a Map<String, OrderDiscountStrategy> keyed by getOrderType().

@Component
public class OrderStrategyFactory {
    private final Map<String, OrderDiscountStrategy> strategyMap = new HashMap<>();

    @Autowired
    public OrderStrategyFactory(List<OrderDiscountStrategy> strategies) {
        for (OrderDiscountStrategy strategy : strategies) {
            strategyMap.put(strategy.getOrderType(), strategy);
        }
    }

    public OrderDiscountStrategy getStrategy(String orderType) {
        return strategyMap.get(orderType);
    }
}

Lookup becomes O(1). The author warns of two common pitfalls: (1) forgetting @Component on a strategy class, causing it to be missing from the injected list; (2) package scanning not covering the strategy classes, resulting in an empty list and runtime failures.

Step 4: Refactor the Original Service

The OrderService now injects OrderStrategyFactory and delegates:

@Service
public class OrderService {
    @Autowired
    private OrderStrategyFactory strategyFactory;

    public BigDecimal calculateDiscount(Order order) {
        OrderDiscountStrategy strategy = strategyFactory.getStrategy(order.getType());
        if (strategy == null) {
            return BigDecimal.ZERO;
        }
        return strategy.calculateDiscount(order);
    }
}

The 16-branch method collapses to a few lines. Adding a new order type means writing a new strategy class with @Component — zero changes to existing code. This embodies the Open/Closed Principle.

Why Not Just Use switch?

A comparison table contrasts if-else/switch vs. Strategy Pattern across five dimensions:

Adding a type : modify original method (high risk) vs. add a new class (no touch to old code).

Unit testing : only whole-method test vs. each strategy testable in isolation.

Code length : grows unbounded vs. each class stays short.

Team collaboration : merge conflicts on one file vs. parallel work on separate files.

Readability : high cognitive load vs. one-glance clarity.

The author advises: use if-else for 2–3 simple branches; switch to Strategy Pattern when branches exceed five or each branch exceeds ten lines, especially if modification frequency is high and each change feels risky.

Three Common Pitfalls for Beginners

Missing @Component on strategy class → not injected → null pointer or "strategy not found".

Case mismatch in getOrderType() : strategy returns "normal" but order carries "NORMAL" → Map lookup fails. Recommendation: use uppercase constants or enums.

Manually instantiating strategies with new : new NormalOrderStrategy() bypasses Spring, so any @Autowired fields inside the strategy remain null. Strategies must be Spring-managed beans.

Going Further: Enum-Based Type Safety

To eliminate string typos, define an enum:

public enum OrderTypeEnum {
    NORMAL("普通订单"),
    SECKILL("秒杀订单"),
    GROUP("拼团订单"),
    PRESALE("预售订单");
    private final String desc;
    OrderTypeEnum(String desc) { this.desc = desc; }
    public String getDesc() { return desc; }
}

Strategies return OrderTypeEnum.NORMAL.name(), and the order DTO uses the same enum. Mismatches become compile-time errors.

Summary Checklist

Define interface with getOrderType() and business method.

Implement each branch as a separate @Component class.

Create factory with constructor-injected List<Interface>; build Map at startup.

Service delegates to factory.getStrategy(type).calculateDiscount(order).

This pattern applies broadly: payment channels, message push providers, file parsers, etc. The article concludes by inviting readers to share their worst if-else encounters.

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 PatternBackend DevelopmentSpring BootDependency InjectionRefactoringClean Code
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.