Stage 6: Design and clean code, lesson 4 of 4

Design patterns: Strategy, Observer and Template method

Intermediate3 min read@since 16Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

Strategy: put interchangeable algorithms behind one interface and pick one at run time. With lambdas, a strategy is often just a Function or a small interface. Comparator is a strategy.

Observer (publish and subscribe): when something happens, notify everyone who registered interest, without the publisher knowing who they are. Spring's ApplicationEventPublisher with @EventListener, and Kafka at a larger scale, follow this pattern.

Template method: a base class fixes the steps of an algorithm and lets subclasses fill in some of them. Spring's JdbcTemplate follows the idea: it handles connections and errors while you supply the query and the row mapping.

Others you'll meet: Adapter (wrap an incompatible API), Decorator (add behaviour by wrapping, like BufferedReader around a Reader) and Proxy (how Spring implements @Transactional).

Example

Java
// Strategy with lambdas
public enum Discount {
    NONE(p -> p), FESTIVE(p -> p * 0.8), STUDENT(p -> p * 0.5);
    private final DoubleUnaryOperator rule;
    Discount(DoubleUnaryOperator rule) { this.rule = rule; }
    public double apply(double price) { return rule.applyAsDouble(price); }
}
double price = Discount.FESTIVE.apply(1999);       // 1599.2

// Observer with Spring events
public record OrderPaid(long orderId, String email) {}

@Service
class PaymentService {
    private final ApplicationEventPublisher events;
    PaymentService(ApplicationEventPublisher events) { this.events = events; }

    void markPaid(long orderId, String email) {
        // ... update the order
        events.publishEvent(new OrderPaid(orderId, email));   // doesn't know who listens
    }
}

@Component
class ReceiptMailer {
    @TransactionalEventListener                     // runs after the transaction commits
    void on(OrderPaid e) { /* send the receipt email */ }
}

@Component
class EnrollmentGranter {
    @EventListener
    void on(OrderPaid e) { /* unlock the course */ }
}

Common mistake

Sending emails from a plain event listener inside the transaction. If the transaction rolls back, the email has already gone; listen after commit.

Under the hood

A plain @EventListener runs synchronously in the publisher's thread and transaction. Use @TransactionalEventListener (after commit by default) so emails aren't sent for orders that later roll back, and add @Async if listeners are slow. Patterns are vocabulary, not goals: saying "this is a strategy" in a review or interview communicates a design in one sentence.

Check yourself

Which pattern does java.util.Comparator represent?

How this connects

Where this leads

You've reached the end of this thread. Try a learning path for what's next.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.