Lesson 14 of 17 3 min

LLD Masterclass: The Observer Pattern in Production Systems

Learn how to implement the Observer pattern for event-driven decoupled systems. From basic Java interfaces to Spring ApplicationEvents and thread-safety considerations.

Introduction to the Observer Pattern

In a modern backend system, you often need to perform multiple actions when a single event occurs. For example, when a user completes a purchase:

  1. Update the Inventory.
  2. Send an Email Receipt.
  3. Notify the Shipping Service.
  4. Update the Loyalty Points.

If you put all this logic into a single OrderService, you violate the Single Responsibility Principle (SRP). The code becomes a "God Class" that is hard to test and maintain.

The Observer Pattern allows a "Subject" to notify a list of "Observers" (listeners) automatically when its state changes, without knowing who they are.

1. The Classic Java Implementation

The basic pattern consists of a Subject interface and an Observer interface.

public interface Observer {
    void update(String event);
}

public class EmailNotificationObserver implements Observer {
    @Override
    public void update(String event) {
        System.out.println("Sending Email: " + event);
    }
}

public class Subject {
    private List<Observer> observers = new ArrayList<>();

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

    public void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

The Problem with the Classic Approach

  • Thread Safety: What if multiple threads add/remove observers or trigger notifications simultaneously?
  • Tight Coupling: You still have to manually register observers.
  • Synchronous Execution: If one observer is slow (e.g., sending an email), the entire Subject is blocked.

2. Production Pattern: Spring @EventListener

In a real Spring Boot production system, we rarely implement the interfaces manually. We use Application Events.

Step 1: Define the Event

public class OrderPlacedEvent {
    private final String orderId;
    // Constructor and Getter
}

Step 2: Publish the Event

@Service
public class OrderService {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    public void placeOrder(String orderId) {
        // Business logic...
        eventPublisher.publishEvent(new OrderPlacedEvent(orderId));
    }
}

Step 3: Listen to the Event

@Component
public class InventoryListener {
    @EventListener
    public void handleOrder(OrderPlacedEvent event) {
        System.out.println("Updating inventory for: " + event.getOrderId());
    }
}

@Component
public class EmailListener {
    @EventListener
    @Async // Make it non-blocking!
    public void handleOrder(OrderPlacedEvent event) {
        // Long running email task
    }
}

3. Senior Level Considerations

A. Asynchronicity (@Async)

By default, Spring events are synchronous. If an observer fails, the original transaction might roll back. Use @Async to ensure that failure in a non-critical observer (like analytics) doesn't crash the main business process.

B. Transactional Bound Events (@TransactionalEventListener)

What if the database transaction fails after you've sent the "Order Successful" email? Use @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) to ensure observers only run after the database has successfully saved the data.

C. Error Handling

Always wrap your observer logic in a try-catch block. An unhandled exception in an observer can block the thread or, if synchronous, prevent the main task from completing.

When to use Observer vs. Message Queues (Kafka)?

  • Use Observer (Local Events): For logic within the same microservice. It's faster, has zero network overhead, and is easier to manage.
  • Use Kafka/RabbitMQ: For communication between different microservices.

Final Takeaways

  • The Observer pattern is the foundation of Decoupled Architecture.
  • Use Spring's built-in event system instead of manual implementations.
  • Always think about Transactional boundaries and Error handling in observers.

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.