Software Dev

Behavioral Design Patterns in Java: Strategy, Observer, Command, Chain of Responsibility, Template Method, State & More

Behavioral design patterns define how objects communicate and distribute responsibility at runtime. While creational patterns answer "how do I create objects?" and structural patterns answer "how do I compose them?", behavioral patterns answer "how do objects interact and collaborate to accomplish complex tasks?" In a Spring Boot microservices context, behavioral patterns are everywhere — from the strategy-based payment dispatcher to the chain-of-responsibility filter pipeline in every HTTP request. This guide covers eight core behavioral patterns with real Java code, Spring Boot wiring, and honest advice on when to reach for each one.

Md Sanwar Hossain April 9, 2026 22 min read Software Dev
Behavioral Design Patterns in Java

Table of Contents

  1. What Are Behavioral Patterns?
  2. Strategy Pattern — Interchangeable Algorithms
  3. Observer Pattern — Event-Driven Notifications
  4. Command Pattern — Encapsulate Requests
  5. Chain of Responsibility — Request Pipeline
  6. Template Method — Define Skeleton of Algorithm
  7. State Pattern — Context-Dependent Behavior
  8. Iterator Pattern — Traversal Abstraction
  9. Mediator Pattern — Decoupled Communication
  10. Spring Boot Real-World Mapping
  11. Comparison Table & Choosing the Right Pattern

1. What Are Behavioral Patterns?

Behavioral Design Patterns in Java | mdsanwarhossain.me
Behavioral Design Patterns in Java — mdsanwarhossain.me

Behavioral design patterns are concerned with the assignment of responsibilities between objects and the patterns of communication between them. The GoF (Gang of Four) catalog defines eleven behavioral patterns: Strategy, Observer, Command, Chain of Responsibility, Template Method, State, Iterator, Mediator, Memento, Visitor, and Interpreter. In day-to-day Java and Spring Boot work, the first eight are the workhorses you will encounter and implement regularly.

The common thread across all behavioral patterns is loose coupling between senders and receivers of behavior. A caller should not need to know which algorithm will execute, which objects are listening to an event, or how an object's response changes based on its internal state. This separation makes code extensible, testable, and understandable by new team members joining a service six months after it was written.

Context: All examples target Java 17+ and Spring Boot 3.x. Code uses records, sealed interfaces, and text blocks where they improve clarity.

The Three Behavioral Concerns

2. Strategy Pattern — Interchangeable Algorithms

Real problem: An e-commerce checkout service must support CreditCard, PayPal, and Crypto payments. Without a pattern, developers add a new if/else or switch branch to PaymentService for every new provider. Adding Apple Pay or Stripe means touching battle-tested code and re-testing everything — a classic Open/Closed violation.

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. The context class (OrderService) delegates to a PaymentStrategy without knowing which concrete implementation it holds. Adding a new payment method means creating a new class — no modification to existing code.

// Strategy interface
public interface PaymentStrategy {
    String supportedMethod();
    PaymentResult pay(PaymentRequest request);
}

// Concrete strategies
@Component
public class CreditCardStrategy implements PaymentStrategy {
    @Override public String supportedMethod() { return "CREDIT_CARD"; }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        // Charge card via Stripe SDK
        return PaymentResult.success("CC-" + UUID.randomUUID(), request.getAmount());
    }
}

@Component
public class PayPalStrategy implements PaymentStrategy {
    @Override public String supportedMethod() { return "PAYPAL"; }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        // Call PayPal REST API
        return PaymentResult.success("PP-" + UUID.randomUUID(), request.getAmount());
    }
}

@Component
public class CryptoStrategy implements PaymentStrategy {
    @Override public String supportedMethod() { return "CRYPTO"; }

    @Override
    public PaymentResult pay(PaymentRequest request) {
        // Broadcast to blockchain node
        return PaymentResult.success("BTC-" + UUID.randomUUID(), request.getAmount());
    }
}
// Context: Spring collects all PaymentStrategy beans into a Map automatically
@Service
@RequiredArgsConstructor
public class OrderService {
    private final Map<String, PaymentStrategy> paymentStrategies;
    private final OrderRepository orderRepository;

    // Spring injects List<PaymentStrategy> — convert to lookup map in constructor
    public OrderService(List<PaymentStrategy> strategies, OrderRepository orderRepository) {
        this.paymentStrategies = strategies.stream()
            .collect(Collectors.toMap(PaymentStrategy::supportedMethod, s -> s));
        this.orderRepository = orderRepository;
    }

    public OrderConfirmation placeOrder(Order order, String paymentMethod) {
        PaymentStrategy strategy = paymentStrategies.get(paymentMethod);
        if (strategy == null) {
            throw new UnsupportedPaymentMethodException("Unsupported: " + paymentMethod);
        }
        PaymentResult result = strategy.pay(new PaymentRequest(order.getTotal(), order.getCurrency()));
        order.setPaymentReference(result.getReference());
        orderRepository.save(order);
        return OrderConfirmation.from(order, result);
    }
}
Adding Apple Pay: Create @Component public class ApplePayStrategy implements PaymentStrategy. Spring auto-discovers and injects it. Zero changes to OrderService.

Benefits & When to Use

3. Observer Pattern — Event-Driven Notifications

Real problem: A stock trading platform needs to notify multiple subscribers — a mobile push alert service, an email alert service, and a risk monitor — whenever a stock price crosses a threshold. If StockMarket directly calls each service, adding a new subscriber requires modifying StockMarket. The publisher should not know about its subscribers.

The Observer pattern defines a one-to-many dependency: when the subject (publisher) changes state, all registered observers (subscribers) are notified automatically. This is the foundation of event-driven architecture and is directly reflected in Spring's ApplicationEventPublisher.

// Observer interface
public interface StockObserver {
    void onPriceChange(String ticker, BigDecimal oldPrice, BigDecimal newPrice);
}

// Subject (Publisher)
@Component
public class StockMarket {
    private final Map<String, BigDecimal> prices = new ConcurrentHashMap<>();
    private final List<StockObserver> observers = new CopyOnWriteArrayList<>();

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

    public void removeObserver(StockObserver observer) {
        observers.remove(observer);
    }

    public void updatePrice(String ticker, BigDecimal newPrice) {
        BigDecimal oldPrice = prices.getOrDefault(ticker, BigDecimal.ZERO);
        prices.put(ticker, newPrice);
        observers.forEach(o -> o.onPriceChange(ticker, oldPrice, newPrice));
    }
}

// Concrete observers
@Component
public class PriceAlertService implements StockObserver {
    private final NotificationGateway notificationGateway;

    @Override
    public void onPriceChange(String ticker, BigDecimal oldPrice, BigDecimal newPrice) {
        if (newPrice.compareTo(oldPrice) > 0) {
            notificationGateway.sendPushAlert(ticker + " rose to " + newPrice);
        }
    }
}

@Component
public class RiskMonitor implements StockObserver {
    private static final BigDecimal DROP_THRESHOLD = new BigDecimal("0.05");

    @Override
    public void onPriceChange(String ticker, BigDecimal oldPrice, BigDecimal newPrice) {
        if (oldPrice.compareTo(BigDecimal.ZERO) > 0) {
            BigDecimal dropRatio = oldPrice.subtract(newPrice).divide(oldPrice, 4, RoundingMode.HALF_UP);
            if (dropRatio.compareTo(DROP_THRESHOLD) > 0) {
                log.warn("RISK ALERT: {} dropped {}% — triggering circuit breaker", ticker, dropRatio.multiply(BigDecimal.valueOf(100)));
            }
        }
    }
}
// Spring Boot equivalent using ApplicationEventPublisher
// Event POJO
public record StockPriceChangedEvent(String ticker, BigDecimal oldPrice, BigDecimal newPrice) {}

// Publisher
@Service
@RequiredArgsConstructor
public class StockMarketService {
    private final ApplicationEventPublisher eventPublisher;

    public void updatePrice(String ticker, BigDecimal newPrice) {
        BigDecimal old = priceStore.get(ticker);
        priceStore.put(ticker, newPrice);
        eventPublisher.publishEvent(new StockPriceChangedEvent(ticker, old, newPrice));
    }
}

// Subscriber — no coupling to StockMarketService
@Component
public class PriceAlertListener {
    @EventListener
    public void handlePriceChange(StockPriceChangedEvent event) {
        // React to price change
    }

    @EventListener
    @Async  // non-blocking
    public void handleAsync(StockPriceChangedEvent event) { /* ... */ }
}
Thread safety: Use CopyOnWriteArrayList for the observer list if observers can be added/removed concurrently. For Spring Boot, prefer @Async listeners to avoid blocking the publisher thread — and always configure a dedicated ThreadPoolTaskExecutor for async events.

Benefits & When to Use

4. Command Pattern — Encapsulate Requests

Real problem: An order management system needs undo/redo capabilities. A customer service agent should be able to cancel the last action — whether it was placing an order, applying a discount, or updating a shipping address. Without a pattern, every action is a direct method call with no history and no way to reverse it.

The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations. Each command knows both how to execute the action and how to reverse it.

// Command interface
public interface OrderCommand {
    void execute();
    void undo();
    String getDescription();
}

// Concrete command: Place Order
public class PlaceOrderCommand implements OrderCommand {
    private final OrderRepository orderRepository;
    private final Order order;
    private UUID savedOrderId;

    public PlaceOrderCommand(OrderRepository repo, Order order) {
        this.orderRepository = repo;
        this.order = order;
    }

    @Override
    public void execute() {
        orderRepository.save(order);
        this.savedOrderId = order.getId();
        log.info("Order placed: {}", savedOrderId);
    }

    @Override
    public void undo() {
        if (savedOrderId != null) {
            orderRepository.deleteById(savedOrderId);
            log.info("Order placement undone: {}", savedOrderId);
        }
    }

    @Override
    public String getDescription() { return "Place order " + order.getId(); }
}

// Concrete command: Cancel Order
public class CancelOrderCommand implements OrderCommand {
    private final OrderRepository orderRepository;
    private final UUID orderId;
    private Order cancelledOrderSnapshot;

    public CancelOrderCommand(OrderRepository repo, UUID orderId) {
        this.orderRepository = repo;
        this.orderId = orderId;
    }

    @Override
    public void execute() {
        cancelledOrderSnapshot = orderRepository.findById(orderId).orElseThrow();
        orderRepository.updateStatus(orderId, OrderStatus.CANCELLED);
        log.info("Order cancelled: {}", orderId);
    }

    @Override
    public void undo() {
        if (cancelledOrderSnapshot != null) {
            orderRepository.updateStatus(orderId, cancelledOrderSnapshot.getStatus());
            log.info("Order cancellation undone: {}", orderId);
        }
    }

    @Override
    public String getDescription() { return "Cancel order " + orderId; }
}
// Invoker with undo history stack
@Service
public class OrderInvoker {
    private final Deque<OrderCommand> history = new ArrayDeque<>();

    public void executeCommand(OrderCommand command) {
        command.execute();
        history.push(command);
    }

    public void undoLast() {
        if (history.isEmpty()) {
            log.warn("No commands to undo");
            return;
        }
        OrderCommand lastCommand = history.pop();
        lastCommand.undo();
        log.info("Undone: {}", lastCommand.getDescription());
    }

    public void undoAll() {
        while (!history.isEmpty()) {
            undoLast();
        }
    }

    public List<String> getCommandHistory() {
        return history.stream().map(OrderCommand::getDescription).toList();
    }
}
Spring Batch mapping: Spring Batch's JobLauncher and Step model is essentially the Command pattern at scale — each Job is an encapsulated unit of work that can be restarted, retried, or skipped independently.

Benefits & When to Use

5. Chain of Responsibility — Request Pipeline

Real problem: Every incoming HTTP request must pass through authentication, rate limiting, and logging — in that order. Each concern is independent, but they must be applied sequentially. Hard-coding all three checks in a single service method creates a maintenance nightmare and makes it impossible to reorder or remove a step without touching business logic.

The Chain of Responsibility pattern passes a request along a chain of handlers. Each handler decides to process the request, pass it to the next handler, or stop the chain entirely. This is exactly how Spring Security's filter chain and MVC interceptors work.

// Abstract handler
public abstract class RequestHandler {
    private RequestHandler next;

    public RequestHandler setNext(RequestHandler next) {
        this.next = next;
        return next;  // enables fluent chaining
    }

    public final void handle(HttpRequestContext ctx) {
        if (doHandle(ctx) && next != null) {
            next.handle(ctx);
        }
    }

    // Returns true to continue chain, false to stop
    protected abstract boolean doHandle(HttpRequestContext ctx);
}

// Auth handler
public class AuthFilter extends RequestHandler {
    private final TokenValidator tokenValidator;

    @Override
    protected boolean doHandle(HttpRequestContext ctx) {
        String token = ctx.getHeader("Authorization");
        if (token == null || !tokenValidator.isValid(token)) {
            ctx.setResponse(401, "Unauthorized");
            return false;  // stop the chain
        }
        ctx.setPrincipal(tokenValidator.extract(token));
        return true;
    }
}

// Rate limit handler
public class RateLimitFilter extends RequestHandler {
    private final RateLimiter rateLimiter;

    @Override
    protected boolean doHandle(HttpRequestContext ctx) {
        String clientId = ctx.getPrincipal().getId();
        if (!rateLimiter.tryAcquire(clientId)) {
            ctx.setResponse(429, "Too Many Requests");
            return false;
        }
        return true;
    }
}

// Logging handler — always passes through
public class LogFilter extends RequestHandler {
    @Override
    protected boolean doHandle(HttpRequestContext ctx) {
        log.info("[{}] {} {} from {}", Instant.now(), ctx.getMethod(), ctx.getPath(), ctx.getRemoteAddr());
        return true;
    }
}
// Assembling the chain
@Configuration
public class FilterChainConfig {
    @Bean
    public RequestHandler requestPipeline(AuthFilter auth, RateLimitFilter rate, LogFilter log) {
        auth.setNext(rate).setNext(log);
        return auth;  // entry point
    }
}

// Spring Boot equivalent: OncePerRequestFilter chain
@Component
@Order(1)
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        String token = request.getHeader("Authorization");
        if (token != null && jwtService.isValid(token)) {
            SecurityContextHolder.getContext().setAuthentication(jwtService.getAuthentication(token));
        }
        filterChain.doFilter(request, response);  // continue chain
    }
}
Spring Security Filter Chain: Spring Security's entire security model is a Chain of Responsibility. Each SecurityFilterChain bean contains an ordered list of filters. Use @Order or SecurityFilterChain ordering to control the pipeline without modifying individual filters.

Benefits & When to Use

6. Template Method — Define Skeleton of Algorithm

Behavioral Patterns Spring Boot | mdsanwarhossain.me
Spring Boot Behavioral Patterns — mdsanwarhossain.me

Real problem: A reporting service generates CSV, PDF, and Excel reports. All three formats share the same algorithm skeleton: connect to the data source, fetch the data, apply formatting, and write the output. The only differences are the formatting and writing steps. Without a pattern, this skeleton is copy-pasted into three classes, and bug fixes in the "fetch" step must be applied three times.

The Template Method pattern defines the skeleton of an algorithm in a base class, deferring the variable steps to subclasses. The base class controls the sequence; subclasses fill in the specifics. This is inheritance-based, in contrast to Strategy which uses composition.

// Abstract base — defines the skeleton
public abstract class ReportGenerator {

    // Template method — final so subclasses cannot break the skeleton
    public final Report generate(ReportRequest request) {
        DataSet data = fetchData(request);         // shared
        data = filterAndSort(data, request);       // shared
        Object formatted = formatData(data);       // hook — subclass
        byte[] output = writeOutput(formatted);   // hook — subclass
        return new Report(output, getContentType(), getFileExtension());
    }

    // Shared steps
    private DataSet fetchData(ReportRequest request) {
        return dataSource.query(request.getQuery());
    }

    private DataSet filterAndSort(DataSet data, ReportRequest request) {
        return data.applyFilters(request.getFilters())
                   .sort(request.getSortField(), request.getSortOrder());
    }

    // Hook methods — must be implemented by subclasses
    protected abstract Object formatData(DataSet data);
    protected abstract byte[] writeOutput(Object formatted);
    protected abstract String getContentType();
    protected abstract String getFileExtension();
}

// CSV subclass
@Component
public class CsvReportGenerator extends ReportGenerator {
    @Override
    protected Object formatData(DataSet data) {
        return data.toCsvRows();
    }

    @Override
    protected byte[] writeOutput(Object formatted) {
        return String.join("\n", (List<String>) formatted).getBytes(StandardCharsets.UTF_8);
    }

    @Override protected String getContentType() { return "text/csv"; }
    @Override protected String getFileExtension() { return ".csv"; }
}

// PDF subclass
@Component
public class PdfReportGenerator extends ReportGenerator {
    private final PdfWriter pdfWriter;

    @Override
    protected Object formatData(DataSet data) {
        return data.toPdfTable();
    }

    @Override
    protected byte[] writeOutput(Object formatted) {
        return pdfWriter.render((PdfTable) formatted);
    }

    @Override protected String getContentType() { return "application/pdf"; }
    @Override protected String getFileExtension() { return ".pdf"; }
}

// Excel subclass
@Component
public class ExcelReportGenerator extends ReportGenerator {
    @Override
    protected Object formatData(DataSet data) { return data.toExcelWorkbook(); }

    @Override
    protected byte[] writeOutput(Object formatted) {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ((Workbook) formatted).write(out);
            return out.toByteArray();
        } catch (IOException e) { throw new ReportGenerationException(e); }
    }

    @Override protected String getContentType() { return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; }
    @Override protected String getFileExtension() { return ".xlsx"; }
}
Template Method vs Strategy: Template Method uses inheritance — the skeleton lives in the base class, variations in subclasses. Strategy uses composition — the algorithm is delegated to an injected object. Prefer Strategy when you need to switch algorithms at runtime. Use Template Method when the skeleton is fixed and only a few steps vary. Spring's JdbcTemplate, RestTemplate, and HibernateTemplate are all Template Method implementations.

7. State Pattern — Context-Dependent Behavior

Real problem: An order lifecycle has distinct states: PENDING, CONFIRMED, SHIPPED, DELIVERED, and CANCELLED. In each state, the valid operations and transitions differ. A PENDING order can be confirmed or cancelled; a SHIPPED order cannot be confirmed; a DELIVERED order cannot be cancelled. Without a pattern, this logic becomes a massive nest of if (status == PENDING) ... else if (status == SHIPPED) blocks that grow exponentially with each new state.

The State pattern allows an object to alter its behavior when its internal state changes. The object will appear to change its class. Each state is represented by a separate class that implements a common interface. The context (Order) delegates all behavior to its current state object.

// State interface
public interface OrderState {
    void confirm(Order order);
    void ship(Order order);
    void deliver(Order order);
    void cancel(Order order);
    String getStatusName();
}

// Pending state — can confirm or cancel
public class PendingState implements OrderState {
    @Override
    public void confirm(Order order) {
        log.info("Order {} confirmed", order.getId());
        order.setState(new ConfirmedState());
    }

    @Override public void ship(Order order) {
        throw new InvalidStateTransitionException("Cannot ship a pending order");
    }

    @Override public void deliver(Order order) {
        throw new InvalidStateTransitionException("Cannot deliver a pending order");
    }

    @Override
    public void cancel(Order order) {
        log.info("Order {} cancelled from PENDING", order.getId());
        order.setState(new CancelledState());
    }

    @Override public String getStatusName() { return "PENDING"; }
}

// Confirmed state — can ship or cancel
public class ConfirmedState implements OrderState {
    @Override public void confirm(Order order) {
        throw new InvalidStateTransitionException("Order is already confirmed");
    }

    @Override
    public void ship(Order order) {
        log.info("Order {} shipped", order.getId());
        order.setState(new ShippedState());
    }

    @Override public void deliver(Order order) {
        throw new InvalidStateTransitionException("Cannot deliver before shipping");
    }

    @Override
    public void cancel(Order order) {
        log.info("Order {} cancelled from CONFIRMED", order.getId());
        order.setState(new CancelledState());
    }

    @Override public String getStatusName() { return "CONFIRMED"; }
}

// Shipped state — can only deliver
public class ShippedState implements OrderState {
    @Override public void confirm(Order order) { throw new InvalidStateTransitionException("Already shipped"); }
    @Override public void ship(Order order) { throw new InvalidStateTransitionException("Already shipped"); }

    @Override
    public void deliver(Order order) {
        log.info("Order {} delivered", order.getId());
        order.setState(new DeliveredState());
    }

    @Override public void cancel(Order order) {
        throw new InvalidStateTransitionException("Cannot cancel a shipped order. Initiate a return instead.");
    }

    @Override public String getStatusName() { return "SHIPPED"; }
}

// Delivered and Cancelled states are terminal — all transitions throw
public class DeliveredState implements OrderState {
    @Override public void confirm(Order o) { throw new InvalidStateTransitionException("Order delivered"); }
    @Override public void ship(Order o)    { throw new InvalidStateTransitionException("Order delivered"); }
    @Override public void deliver(Order o) { throw new InvalidStateTransitionException("Already delivered"); }
    @Override public void cancel(Order o)  { throw new InvalidStateTransitionException("Cannot cancel delivered order"); }
    @Override public String getStatusName() { return "DELIVERED"; }
}

public class CancelledState implements OrderState {
    @Override public void confirm(Order o) { throw new InvalidStateTransitionException("Order cancelled"); }
    @Override public void ship(Order o)    { throw new InvalidStateTransitionException("Order cancelled"); }
    @Override public void deliver(Order o) { throw new InvalidStateTransitionException("Order cancelled"); }
    @Override public void cancel(Order o)  { throw new InvalidStateTransitionException("Already cancelled"); }
    @Override public String getStatusName() { return "CANCELLED"; }
}
// Context class — delegates to current state
@Entity
public class Order {
    @Id private UUID id;
    @Transient private OrderState state = new PendingState();  // not persisted directly

    @Enumerated(EnumType.STRING)
    private OrderStatus status = OrderStatus.PENDING;

    public void setState(OrderState newState) {
        this.state = newState;
        this.status = OrderStatus.valueOf(newState.getStatusName());
    }

    public void confirm()  { state.confirm(this); }
    public void ship()     { state.ship(this); }
    public void deliver()  { state.deliver(this); }
    public void cancel()   { state.cancel(this); }
    public String getStatusName() { return state.getStatusName(); }
}

Order State Transition Table

Current State confirm() ship() deliver() cancel()
PENDING → CONFIRMED ✗ Error ✗ Error → CANCELLED
CONFIRMED ✗ Error → SHIPPED ✗ Error → CANCELLED
SHIPPED ✗ Error ✗ Error → DELIVERED ✗ Error
DELIVERED ✗ Error ✗ Error ✗ Error ✗ Error
CANCELLED ✗ Error ✗ Error ✗ Error ✗ Error
Spring State Machine: For complex workflows with many states and transitions, Spring State Machine provides a production-grade framework that includes persistence, history states, and distributed state management. The raw State pattern is ideal for 3–7 states; beyond that, Spring State Machine pays dividends.

8. Iterator Pattern — Traversal Abstraction

Real problem: A reporting service needs to process millions of database records without loading them all into memory. Pagination is required, but callers should not deal with page numbers, cursors, or LIMIT/OFFSET clauses — they just want to iterate over results in a standard Java for loop.

The Iterator pattern provides a way to access elements of a collection sequentially without exposing its underlying representation. Java's java.util.Iterator<T> is the canonical implementation. By implementing it, any custom collection gains compatibility with the enhanced for loop and Java Streams.

// Paginating Iterator over database results
public class PageIterator<T> implements Iterator<T> {
    private final Function<PageRequest, List<T>> fetcher;
    private final int pageSize;
    private int currentPage = 0;
    private Queue<T> buffer = new ArrayDeque<>();
    private boolean exhausted = false;

    public PageIterator(Function<PageRequest, List<T>> fetcher, int pageSize) {
        this.fetcher = fetcher;
        this.pageSize = pageSize;
        loadNextPage();
    }

    private void loadNextPage() {
        if (exhausted) return;
        List<T> page = fetcher.apply(PageRequest.of(currentPage++, pageSize));
        if (page.isEmpty() || page.size() < pageSize) {
            exhausted = true;
        }
        buffer.addAll(page);
    }

    @Override
    public boolean hasNext() {
        if (!buffer.isEmpty()) return true;
        if (exhausted) return false;
        loadNextPage();
        return !buffer.isEmpty();
    }

    @Override
    public T next() {
        if (!hasNext()) throw new NoSuchElementException();
        return buffer.poll();
    }
}

// Usage — callers see a simple Iterator
@Service
@RequiredArgsConstructor
public class ReportingService {
    private final OrderRepository orderRepository;

    public void exportAllOrders(OutputStream out) {
        Iterator<Order> iterator = new PageIterator<>(
            page -> orderRepository.findAll(page).getContent(),
            500  // fetch 500 records at a time
        );
        // Enhanced for loop works because we implement Iterator
        while (iterator.hasNext()) {
            Order order = iterator.next();
            writeCsvRow(out, order);
        }
    }
}
// Java enhanced for loop works if you implement Iterable<T>
public class PageIterable<T> implements Iterable<T> {
    private final Function<PageRequest, List<T>> fetcher;
    private final int pageSize;

    @Override
    public Iterator<T> iterator() {
        return new PageIterator<>(fetcher, pageSize);
    }
}

// Now callers can use for-each syntax:
for (Order order : new PageIterable<>(page -> repo.findAll(page).getContent(), 500)) {
    processOrder(order);
}

// Or convert to Stream
StreamSupport.stream(new PageIterable<>(fetcher, 500).spliterator(), false)
    .filter(o -> o.getStatus() == OrderStatus.PENDING)
    .forEach(this::processOrder);

Benefits & When to Use

9. Mediator Pattern — Decoupled Communication

Mediator Pattern Java | mdsanwarhossain.me
Mediator Pattern Java — mdsanwarhossain.me

Real problem: In a real-time chat room, users send messages to each other. If each User object holds references to all other users in the room, adding or removing a user requires updating N objects. With 100 users, each user knows 99 others — an O(N²) coupling explosion.

The Mediator pattern defines an object that encapsulates how a set of objects interact. Instead of users communicating directly, they communicate exclusively through a ChatMediator. Each user only knows the mediator, not other users. This reduces the connections from O(N²) to O(N).

// Mediator interface
public interface ChatMediator {
    void sendMessage(String message, User sender);
    void addUser(User user);
    void removeUser(User user);
}

// Concrete mediator
@Component
public class ChatRoom implements ChatMediator {
    private final Set<User> users = ConcurrentHashMap.newKeySet();

    @Override
    public void addUser(User user) {
        users.add(user);
        broadcast(user.getUsername() + " joined the room.", null);
    }

    @Override
    public void removeUser(User user) {
        users.remove(user);
        broadcast(user.getUsername() + " left the room.", null);
    }

    @Override
    public void sendMessage(String message, User sender) {
        broadcast("[" + sender.getUsername() + "]: " + message, sender);
    }

    private void broadcast(String message, User exclude) {
        users.stream()
             .filter(u -> !u.equals(exclude))
             .forEach(u -> u.receive(message));
    }
}

// Colleague — only knows the mediator
public class User {
    private final String username;
    private final ChatMediator mediator;

    public User(String username, ChatMediator mediator) {
        this.username = username;
        this.mediator = mediator;
    }

    public void send(String message) {
        mediator.sendMessage(message, this);  // delegates to mediator
    }

    public void receive(String message) {
        log.info("{} received: {}", username, message);
    }

    public String getUsername() { return username; }
}
// Spring equivalent: ApplicationContext as a Mediator
// Services publish events through ApplicationEventPublisher (mediator)
// Listeners subscribe to specific event types — no service-to-service dependencies

@Service
@RequiredArgsConstructor
public class OrderService {
    private final ApplicationEventPublisher publisher;  // the mediator

    public void placeOrder(Order order) {
        orderRepository.save(order);
        publisher.publishEvent(new OrderPlacedEvent(order));  // notify all interested parties
    }
}

// Multiple listeners — mutually unaware of each other
@Component public class InventoryListener {
    @EventListener public void onOrderPlaced(OrderPlacedEvent e) { /* decrement stock */ }
}

@Component public class EmailListener {
    @EventListener public void onOrderPlaced(OrderPlacedEvent e) { /* send confirmation */ }
}

@Component public class LoyaltyListener {
    @EventListener public void onOrderPlaced(OrderPlacedEvent e) { /* award points */ }
}
Mediator vs Observer: Both reduce coupling. Observer is a one-to-many push model — the subject notifies all observers. Mediator is many-to-many — all colleagues communicate through a central hub that controls routing logic. Use Mediator when components need to communicate bidirectionally or when routing logic is non-trivial. Use Observer for simple event broadcasting.

10. Spring Boot Real-World Mapping

Every behavioral pattern has a natural home in the Spring ecosystem. Understanding these mappings lets you leverage Spring's built-in infrastructure rather than reinventing it. The following table shows how each pattern manifests in production Spring Boot applications.

Pattern Spring Feature / Idiom Key Benefit in Spring
Strategy @Component + Map<String, Strategy> injection Auto-discovery of strategies; zero-configuration dispatch
Observer ApplicationEventPublisher / @EventListener Decoupled domain events; @Async for non-blocking handlers
Command Spring Batch JobLauncher / TaskExecutor Restartable, retryable, parallelizable units of work
Chain of Responsibility Spring Security FilterChain / MVC HandlerInterceptor Ordered, composable request pipeline with short-circuit support
Template Method JdbcTemplate, RestTemplate, AbstractMessageConverterMethodArgumentResolver Boilerplate elimination; customization through callback/hook methods
State Spring State Machine (spring-statemachine) Persistent state, distributed guards, history states
Iterator Spring Data Page<T> / Streamable<T> / ScrollPosition Memory-efficient lazy loading; cursor-based pagination in Spring Data 3.x
Mediator ApplicationContext / ApplicationEventPublisher Central event bus; services publish to context, not to each other directly
Production tip: When introducing Spring State Machine for complex order or subscription workflows, enable persistence with a StateMachinePersist implementation backed by Redis or a relational database. This ensures state survives service restarts and pod rescheduling in Kubernetes.

11. Comparison Table & Choosing the Right Pattern

Choosing the wrong behavioral pattern is usually a symptom of misidentifying the core problem. Strategy and Template Method both vary algorithms, but by different mechanisms. Observer and Mediator both reduce coupling in multi-party communication, but with different routing control. Use the table below as a decision guide.

Pattern Core Purpose Spring Boot Equivalent When to Switch
Strategy Swap algorithms at runtime @Component map injection Use Template Method if skeleton is fixed and only 1–2 steps vary
Observer Broadcast state changes to N listeners @EventListener Use Mediator if routing logic is complex or bi-directional
Command Encapsulate requests with undo/queue Spring Batch Use Strategy if no undo/history is needed
Chain of Responsibility Sequential pipeline with short-circuit Spring Security FilterChain Use Strategy for parallel dispatch to a single handler
Template Method Fixed skeleton, variable steps via inheritance JdbcTemplate, RestTemplate Use Strategy if algorithm needs runtime swapping
State Object behavior changes with internal state Spring State Machine Use enum + switch for 2–3 states with no complex transitions
Iterator Decouple traversal from collection structure Spring Data Page / Streamable Use Java Stream directly for most in-memory traversals
Mediator Central hub for M:N communication ApplicationEventPublisher Use Observer for simple 1:N broadcasting without routing logic

Pattern Selection Decision Tree

Common anti-pattern: Using Strategy when you only have one algorithm that will never change. The extra interface and injection are pure overhead. Apply patterns in response to observed or anticipated variation — not speculatively.

Leave a Comment

Related Posts

Md Sanwar Hossain - Software Engineer
Md Sanwar Hossain

Software Engineer · Java · Spring Boot · Microservices

Last updated: April 9, 2026