Fundamentals 19 min read

Master Strategy, Observer, and Chain of Responsibility with Spring & MQ examples

This article walks through the three core behavioral design patterns—Strategy, Observer, and Chain of Responsibility—explaining their intent, showing problematic anti‑patterns, providing step‑by‑step Spring and MQ code implementations, mapping them to real‑world frameworks, and offering interview‑style comparison questions to solidify understanding.

Tinker Programmer
Tinker Programmer
Tinker Programmer
Master Strategy, Observer, and Chain of Responsibility with Spring & MQ examples

Strategy Pattern – Eliminating if‑else Hell

The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable so that conditional branches can be removed and algorithms can vary independently.

Anti‑example

public class PromotionService {
    public double calculate(String promotionType, double originalPrice) {
        if ("FULL_REDUCTION".equals(promotionType)) {
            // 满 200 减 30
            return originalPrice >= 200 ? originalPrice - 30 : originalPrice;
        } else if ("DISCOUNT".equals(promotionType)) {
            // 八折优惠
            return originalPrice * 0.8;
        } else if ("SECKILL".equals(promotionType)) {
            // 秒杀价五折
            return originalPrice * 0.5;
        } else if ("NEW_USER".equals(promotionType)) {
            // 新人立减 15 元
            return originalPrice - 15;
        }
        // 产品经理说:下周还要加"拼团"和"积分兑换"……
        return originalPrice;
    }
}

This class quickly becomes a nightmare because every new promotion forces a change in the same method, risking broken logic, making testing hard, and preventing reuse.

Solution Steps

Define a strategy interface.

public interface PromotionStrategy {
    double calculate(double originalPrice);
}

Implement each promotion as a separate class.

// Full‑reduction strategy
@Component("FULL_REDUCTION")
public class FullReductionStrategy implements PromotionStrategy {
    @Override
    public double calculate(double originalPrice) {
        return originalPrice >= 200 ? originalPrice - 30 : originalPrice;
    }
}

// Discount strategy
@Component("DISCOUNT")
public class DiscountStrategy implements PromotionStrategy {
    @Override
    public double calculate(double originalPrice) {
        return originalPrice * 0.8;
    }
}

// Seckill strategy
@Component("SECKILL")
public class SeckillStrategy implements PromotionStrategy {
    @Override
    public double calculate(double originalPrice) {
        return originalPrice * 0.5;
    }
}

Use Spring’s automatic map injection to create a strategy factory without any registration code.

@Service
public class PromotionContext {
    /** Spring automatically scans all PromotionStrategy beans and injects them into this map with the bean name as the key. */
    private final Map<String, PromotionStrategy> strategyMap;

    public PromotionContext(Map<String, PromotionStrategy> strategyMap) {
        this.strategyMap = strategyMap;
    }

    public double calculate(String promotionType, double originalPrice) {
        PromotionStrategy strategy = strategyMap.get(promotionType);
        if (strategy == null) {
            throw new IllegalArgumentException("Unsupported promotion type: " + promotionType);
        }
        return strategy.calculate(originalPrice);
    }
}

Client code becomes clean: <code>double finalPrice = promotionContext.calculate("DISCOUNT", 299.0); </code> The map‑injection technique is a common refactoring suggestion in large‑scale Java code reviews.

Real‑world framework mappings

JDK – Comparator used in Collections.sort(list, comparator) is a strategy.

Spring – Resource implementations ( ClassPathResource, FileSystemResource, UrlResource) are different strategies for resource loading.

Spring Security – PasswordEncoder implementations ( BCryptPasswordEncoder, Pbkdf2PasswordEncoder) are encryption strategies.

MyBatis – TypeHandler implementations are strategies for type conversion.

Observer Pattern – Decoupling Events from Handlers

The observer pattern defines a one‑to‑many dependency so that when an object changes state, all its dependents are automatically notified, thereby decoupling the event source from its processors.

Anti‑example

@Service
public class OrderService {
    @Autowired private SmsService smsService;
    @Autowired private InventoryService inventoryService;
    @Autowired private PointsService pointsService;
    @Autowired private BigDataService bigDataService;

    public Order createOrder(CreateOrderRequest req) {
        Order order = orderDao.save(req); // core business
        // 💩 Directly invoke four downstream services
        smsService.sendOrderNotification(order);
        inventoryService.deduct(order.getSkuId(), order.getQuantity());
        pointsService.grant(order.getUserId(), order.getAmount());
        bigDataService.log(order);
        // Product manager: add push service and risk callbacks next week…
        return order;
    }
}

Every new downstream requirement forces a change in OrderService , turning it into a “big mess” and increasing the risk of production bugs.

First Evolution – Manual Observer

// Observer interface
public interface OrderObserver {
    void onOrderCreated(Order order);
}

// Subject maintains a list of observers
@Service
public class OrderService {
    private final List<OrderObserver> observers = new ArrayList<>();

    public void addObserver(OrderObserver observer) {
        observers.add(observer);
    }

    public Order createOrder(CreateOrderRequest req) {
        Order order = orderDao.save(req);
        observers.forEach(o -> o.onOrderCreated(order));
        return order;
    }
}

// Concrete observers
@Component
public class SmsObserver implements OrderObserver {
    @Override
    public void onOrderCreated(Order order) {
        smsService.sendOrderNotification(order);
    }
}

@Component
public class InventoryObserver implements OrderObserver {
    @Override
    public void onOrderCreated(Order order) {
        inventoryService.deduct(order.getSkuId(), order.getQuantity());
    }
}

After this refactor, OrderService only depends on the OrderObserver interface; adding a new handler only requires a new implementation class.

Second Evolution – Spring Events (Built‑in Observer)

// Event object
public class OrderCreatedEvent extends ApplicationEvent {
    private final Order order;
    public OrderCreatedEvent(Object source, Order order) {
        super(source);
        this.order = order;
    }
    public Order getOrder() { return order; }
}

// Publisher
@Service
public class OrderService {
    @Autowired private ApplicationEventPublisher eventPublisher;
    public Order createOrder(CreateOrderRequest req) {
        Order order = orderDao.save(req);
        eventPublisher.publishEvent(new OrderCreatedEvent(this, order)); // one line
        return order;
    }
}

// Listener (can be async)
@Component
public class SmsListener {
    @EventListener
    @Async
    public void onOrderCreated(OrderCreatedEvent event) {
        smsService.sendOrderNotification(event.getOrder());
    }
}

@Component
public class InventoryListener {
    @EventListener
    public void onOrderCreated(OrderCreatedEvent event) {
        inventoryService.deduct(event.getOrder().getSkuId(), event.getOrder().getQuantity());
    }
}

Adding a new downstream processor now only requires a method annotated with @EventListener ; the publishing code never changes.

Third Evolution – Message Queue (Distributed Observer)

For cross‑service scenarios, a broker such as RocketMQ or Kafka replaces the in‑process event bus, providing reliable, asynchronous delivery.

【Order Service】 ─── Send Message ───▶ 【RocketMQ Broker】 ───▶ 【SMS Service】 consumes
                                            ───▶ 【Inventory Service】 consumes
                                            ───▶ 【Points Service】 consumes

The producer/consumer model is essentially a distributed observer pattern, with the broker adding buffering and persistence.

Framework mappings

Spring – ApplicationEvent + ApplicationListener / @EventListener JDK – java.util.Observable + java.util.Observer (deprecated after JDK 9)

Guava – EventBus provides a lightweight in‑process event bus.

RocketMQ / Kafka – Distributed publish‑subscribe, the external version of the observer pattern.

Chain of Responsibility – Building an Extensible Processing Pipeline

The chain of responsibility links multiple handler objects so that a request can pass along the chain until one handler processes it, decoupling the sender from the processors and allowing dynamic composition.

Anti‑example

public RiskResult checkRisk(OrderRequest request) {
    if (blacklistService.isBlocked(request.getUserId())) {
        return RiskResult.reject("User is blacklisted");
    } else if (request.getAmount() > 50000) {
        return RiskResult.reject("Amount exceeds limit");
    } else if (frequencyService.isOverLimit(request.getUserId())) {
        return RiskResult.reject("Too many requests");
    } else if (deviceService.isSuspicious(request.getDeviceId())) {
        return RiskResult.reject("Device suspicious");
    }
    // More checks keep being added…
    return RiskResult.pass();
}

Changing the order, adding, or removing a check requires editing the method, which quickly becomes unmanageable.

Solution Steps

Define an abstract handler.

public abstract class RiskHandler {
    protected RiskHandler nextHandler;
    public RiskHandler setNext(RiskHandler next) {
        this.nextHandler = next;
        return next;
    }
    public abstract RiskResult handle(OrderRequest request);
    protected RiskResult passToNext(OrderRequest request) {
        return (nextHandler != null) ? nextHandler.handle(request) : RiskResult.pass();
    }
}

Implement each rule as a concrete handler.

@Component
public class BlacklistHandler extends RiskHandler {
    @Autowired private BlacklistService blacklistService;
    @Override
    public RiskResult handle(OrderRequest request) {
        if (blacklistService.isBlocked(request.getUserId())) {
            return RiskResult.reject("User is blacklisted");
        }
        return passToNext(request);
    }
}

@Component
public class AmountLimitHandler extends RiskHandler {
    private static final int MAX_AMOUNT = 50_000;
    @Override
    public RiskResult handle(OrderRequest request) {
        if (request.getAmount() > MAX_AMOUNT) {
            return RiskResult.reject("Amount exceeds 50k");
        }
        return passToNext(request);
    }
}

@Component
public class FrequencyHandler extends RiskHandler {
    @Autowired private FrequencyService frequencyService;
    @Override
    public RiskResult handle(OrderRequest request) {
        if (frequencyService.isOverLimit(request.getUserId())) {
            return RiskResult.reject("Request frequency too high");
        }
        return passToNext(request);
    }
}

Assemble the chain in a Spring configuration.

@Configuration
public class RiskChainConfig {
    @Autowired private BlacklistHandler blacklistHandler;
    @Autowired private AmountLimitHandler amountLimitHandler;
    @Autowired private FrequencyHandler frequencyHandler;

    @Bean
    public RiskHandler riskChain() {
        // Chain: blacklist → amount limit → frequency
        blacklistHandler.setNext(amountLimitHandler).setNext(frequencyHandler);
        return blacklistHandler; // head of the chain
    }
}

Client code invokes the chain head.

@Autowired private RiskHandler riskChain;

public void createOrder(OrderRequest request) {
    RiskResult result = riskChain.handle(request);
    if (!result.isPassed()) {
        throw new RiskRejectException(result.getReason());
    }
    // continue with order processing…
}

Framework mappings

Servlet – FilterChain processes a request through a series of filters.

Spring MVC – HandlerInterceptorChain provides pre‑ and post‑handle interceptors.

Spring Security – SecurityFilterChain composes authentication, authorization, CSRF filters, etc.

MyBatis – InterceptorChain allows plugins to intercept SQL execution stages.

Netty – ChannelPipeline is a handler chain for inbound/outbound events.

Interview Comparison Questions

Strategy vs. Chain of Responsibility

Execution count : Strategy uses a single algorithm at a time; Chain passes the request through multiple handlers.

When the choice is made : Strategy is selected by the client before invocation; Chain decides at runtime which handler processes the request.

Typical scenario : Strategy for selecting a payment method; Chain for a risk‑control flow where every step must be evaluated.

Core question : “Which algorithm to use?” vs. “Who should handle this request?”

Observer vs. Publish‑Subscribe

Coupling : Observer has direct references between subject and observers; Publish‑Subscribe decouples them via a broker.

Communication scope : Observer is usually in‑process; Publish‑Subscribe works across processes and services.

Reliability : Observer loses events if the process crashes; Publish‑Subscribe persists messages in the broker.

Typical implementations : Observer – Spring Events, Guava EventBus; Publish‑Subscribe – RocketMQ, Kafka, RabbitMQ.

Chain of Responsibility vs. Decorator

Flow direction : Chain can terminate early; Decorator always executes all layers and then returns to the core component.

Purpose : Chain filters or validates a request; Decorator adds additional behavior to an object.

End of chain : All handlers pass → request proceeds; Decorator ultimately delegates to the wrapped core object.

Key Takeaways

Use Strategy to solve “which one to choose” problems and keep algorithms independent of context.

Use Observer to solve “who should handle this event” problems and achieve loose coupling between publishers and listeners.

Use Chain of Responsibility to solve “who decides to allow or reject” problems, enabling flexible, composable processing pipelines.

All three patterns share the same core philosophy: decouple the varying part from the stable part .

Spring’s automatic Map<String, PromotionStrategy> injection, Spring Events, and message‑queue brokers are practical, production‑grade realizations of these patterns.

Understanding these patterns equips you to read and decode complex framework code such as Spring, MyBatis, Netty, and security filters.

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 Responsibilitydesign patternsjavaStrategy PatternSpringmessage queueObserver Pattern
Tinker Programmer
Written by

Tinker Programmer

Solving problems with code, sharing practical tech insights, and leveling up 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.