How Observer Differs From Pub/Sub
1. Overview
Observer and Pub/Sub both solve the problem of “one event happens, many places need to know”. Many of you mix up these two terms because they’re similar in concept, but differ in implementation and coupling level. This article helps you clearly distinguish both concepts, with Java code examples for each type, and know when to choose pure Observer, when to use Pub/Sub through a message broker.
2. What is Observer Pattern
Observer is one of the classic behavioral patterns in GoF. The idea: a Subject (or Publisher) keeps a list of Observers, when the state changes, Subject directly calls methods on each registered Observer.
public interface OrderObserver {
void onOrderPaid(Order order);
}
public class Order {
private final List<OrderObserver> observers = new ArrayList<>();
private String status = "NEW";
public void addObserver(OrderObserver observer) {
observers.add(observer);
}
public void markAsPaid() {
this.status = "PAID";
for (OrderObserver observer : observers) {
observer.onOrderPaid(this);
}
}
}
Explanation:
observersis a list of direct references to interested objects.markAsPaid()callsonOrderPaid()on each observer sequentially, in the same thread, synchronously.Orderknows exactly who is listening to it — this is the key point to distinguish from Pub/Sub.
public class EmailNotifier implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
System.out.println("Send confirmation email for paid order");
}
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
Send confirmation email for paid order
2.1. Core Characteristics of Observer
- Direct coupling:
Subjectholds reference toObserver, must import each other’s interfaces. - Synchronous: the
notifycall blocks until all observers finish processing. - Same process: typically runs in one JVM, not over network.
- Subject knows observers exist: if you forget to call
removeObserver(), you easily get memory leaks becauseSubjectkeeps a “live” reference to observers no longer in use.
Note: Classic Observer pattern doesn’t have a built-in safe unsubscribe mechanism. If you use it in long-lived applications (e.g., UI component, listener registers then destroys), remember to call
removeObserver()when the object is destroyed, to avoid memory leaks.
3. What is Pub/Sub
Pub/Sub (Publish/Subscribe) completely decouples Publisher and Subscriber through an intermediary layer — called a broker, event bus, or topic. Publisher doesn’t know who is subscribing, just “shoots” a message into a topic/channel.
public record OrderPaidEvent(String orderId, double amount) {}
public class OrderService {
private final EventBus eventBus;
public OrderService(EventBus eventBus) {
this.eventBus = eventBus;
}
public void payOrder(String orderId, double amount) {
// handle payment...
eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
}
}
Explanation:
OrderServicedoesn’t know ifEmailNotifierorInventoryServiceexist or not.- It only publishes to topic
"order.paid", the rest is the broker’s responsibility. - Subscriber registers with the broker, not directly with
OrderService.
3.1. In-Process Pub/Sub with Spring Events
Spring Boot has a simple built-in event bus using ApplicationEventPublisher. This is Pub/Sub at application level, without needing external brokers like Kafka/RabbitMQ.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
public class OrderPaidEvent extends ApplicationEvent {
private final String orderId;
public OrderPaidEvent(String orderId) {
super(orderId);
this.orderId = orderId;
}
public String getOrderId() {
return orderId;
}
}
@Service
public class OrderService {
private final ApplicationEventPublisher publisher;
public OrderService(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
public void payOrder(String orderId) {
publisher.publishEvent(new OrderPaidEvent(orderId));
}
}
@Component
public class EmailEventListener {
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("Send email for order " + event.getOrderId());
}
}
Explanation:
OrderServicepublishes event viaApplicationEventPublisher— a bean managed by Spring, acting as an internal broker.EmailEventListeneris not known toOrderService; Spring automatically scans methods with@EventListenerand routes events to the correct handler based on type.- By default,
@EventListenerruns synchronously in the same thread as the publisher — this is a confusing point, many think Pub/Sub is always asynchronous.
To run asynchronously, add @Async:
@Component
public class EmailEventListener {
@Async
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("Send email for order " + event.getOrderId());
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
@EnableAsyncactivates a proxy so Spring runs@Asyncmethods on a separate thread pool.- Now
payOrder()returns immediately, without waiting forEmailEventListenerto finish.
3.2. Pub/Sub via Message Broker (Kafka)
At larger scale, “broker” is no longer a bean in one JVM but a separate system (Kafka, RabbitMQ, SQS…). Publisher and subscriber can be in two services, two processes, two different machines.
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
@Service
public class OrderService {
private final KafkaTemplate<String, OrderPaidEvent> kafkaTemplate;
public OrderService(KafkaTemplate<String, OrderPaidEvent> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void payOrder(String orderId, double amount) {
kafkaTemplate.send("order-paid-topic", new OrderPaidEvent(orderId, amount));
}
}
@Component
public class EmailConsumer {
@KafkaListener(topics = "order-paid-topic", groupId = "email-service")
public void consume(OrderPaidEvent event) {
System.out.println("Email service processes order " + event.orderId());
}
}
OrderServiceandEmailConsumercan be in two completely different microservices, deployed independently.- Kafka keeps messages in topics, consumers read at their own pace — this is real Pub/Sub, with buffering, replay, multiple independent subscribers per group.
4. Direct Comparison
| Criterion | Observer | Pub/Sub |
|---|---|---|
| Coupling | Subject knows Observer directly (via interface) | Publisher doesn’t know if Subscriber exists |
| Intermediary | None, calls method directly | Has broker/event bus in between |
| Sync/Async | Default synchronous, same thread | Can be sync (Spring Event) or async (Kafka, RabbitMQ) |
| Scope | Within one process/module | Within process (event bus) or across multiple services/processes |
| Message durability | None, if Subject dies then lost | External broker (Kafka) can persist, allows replay |
| Publisher-Subscriber count | Usually 1 Subject - many Observers | Many Publishers - many Subscribers via multiple topics |
The most important difference: Observer is a direct relationship between two objects, Pub/Sub is a relationship through an intermediary anonymous layer. In essence, Pub/Sub can be seen as Observer pattern “loosened” by inserting a broker to remove direct dependency.
5. When to Use Observer, When to Use Pub/Sub
Use Observer when:
- Logic stays in the same module/class, number of observers is fixed and changes rarely.
- You need immediate response, synchronously (e.g., validate data before saving).
- You don’t need to decouple too much — e.g., UI component listening for model changes.
class ValidationObserver implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
if (order.getStatus() == null) {
throw new IllegalStateException("Order does not have valid status");
}
}
}
Use Pub/Sub when:
- Multiple independent services need to know about an event but shouldn’t depend on each other.
- Need ability to add/remove subscribers without changing publisher code.
- Need message durability (if subscriber is down, message waits until subscriber comes online).
- System is microservices, communication over network.
Note: Don’t use Kafka/RabbitMQ just to replace a
List<Observer>in the same JVM — the cost of operating a broker doesn’t justify small problems. Conversely, don’t try to force cross-service logic into pure Observer pattern, because you’ll have to rewrite all the network, retry, serialization mechanisms that the broker already provides.
6. Testing Observer Behavior
class OrderObserverTest {
@Test
void shouldNotifyAllObserversWhenOrderIsPaid() {
Order order = new Order();
List<String> notified = new ArrayList<>();
order.addObserver(o -> notified.add("email"));
order.addObserver(o -> notified.add("inventory"));
order.markAsPaid();
assertThat(notified).containsExactly("email", "inventory");
}
}
Explanation:
- Use lambda instead of a class implementing
OrderObserverbecause the interface has only one method — cleaner when writing tests. assertThat(...).containsExactly(...)verifies observers are called in the order they were registered — an important characteristic of synchronous Observer that Pub/Sub via broker doesn’t guarantee (Kafka can process in parallel across multiple consumer instances).
7. Common Mistakes
- Thinking Spring
@EventListeneris asynchronous: by default it runs synchronously, errors in the listener throw exceptions back to publisher if not caught. - Observer holds reference and you forget to remove: causes memory leaks, especially in applications with short object lifespans (request-scope) but registering on singletons.
- Overusing Pub/Sub for logic requiring consistent transactions: if publisher and subscriber must both succeed or both fail (atomic), asynchronous Pub/Sub doesn’t fit — you need to reconsider Saga or transactional outbox patterns.
- Not naming topics clearly: in large systems, topics named vaguely like
"event1"means nobody knows who subscribes to what, losing all the benefit of decoupling.
8. Conclusion
Observer and Pub/Sub share one goal — notifying multiple interested parties about changes — but differ in coupling level and scope. Observer suits direct, synchronous relationships within the same process; Pub/Sub suits when you need to completely decouple publisher from subscriber, especially in microservices systems needing message durability and ability to scale each consumer independently. Understanding this distinction helps you choose the right tool: not every problem needs Kafka, and a simple List<Observer> doesn’t always suffice for your problem.