Observer khác gì Pub/Sub?
1. Tổng quan
Observer và Pub/Sub đều giải quyết bài toán “một sự kiện xảy ra, nhiều nơi cần biết”. Nhiều bạn dùng lẫn hai từ này vì chúng giống nhau ở ý tưởng, nhưng khác nhau ở cách triển khai và mức độ coupling. Bài này giúp bạn phân biệt rõ hai khái niệm, có code Java minh họa cho từng loại, và biết khi nào nên chọn Observer thuần, khi nào nên dùng Pub/Sub qua message broker.
2. Observer Pattern là gì
Observer là một trong các pattern hành vi (behavioral pattern) cổ điển trong GoF. Ý tưởng: một Subject (hay Publisher) giữ danh sách các Observer, khi state thay đổi, Subject gọi trực tiếp method trên từng Observer đã đăng ký.
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);
}
}
}
Giải thích:
observerslà danh sách tham chiếu trực tiếp tới các đối tượng quan tâm.markAsPaid()gọionOrderPaid()trên từng observer theo thứ tự, trong cùng thread, đồng bộ.Orderbiết chính xác ai đang lắng nghe nó — đây là điểm mấu chốt để phân biệt với Pub/Sub.
public class EmailNotifier implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
System.out.println("Gửi email xác nhận cho order đã thanh toán");
}
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
Gửi email xác nhận cho order đã thanh toán
2.1. Đặc điểm cốt lõi của Observer
- Coupling trực tiếp:
Subjectgiữ reference tớiObserver, phải import interface của nhau. - Đồng bộ: lời gọi
notifyblock cho tới khi tất cả observer xử lý xong. - Trong cùng process: thường chạy trong một JVM, không qua network.
- Subject biết observer tồn tại: nếu quên gọi
removeObserver(), bạn dễ gặp memory leak vìSubjectgiữ reference “sống” tới observer đã hết dùng.
Lưu ý: Observer pattern cổ điển không tự có cơ chế unsubscribe an toàn. Nếu bạn dùng trong ứng dụng dài hạn (ví dụ UI component, listener đăng ký rồi destroy), hãy nhớ gọi
removeObserver()khi object bị hủy, tránh rò rỉ bộ nhớ.
3. Pub/Sub là gì
Pub/Sub (Publish/Subscribe) tách Publisher và Subscriber ra hoàn toàn thông qua một lớp trung gian — gọi là broker, event bus, hoặc topic. Publisher không biết ai đang subscribe, chỉ “bắn” message vào một 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) {
// xử lý thanh toán...
eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
}
}
Giải thích:
OrderServicekhông biếtEmailNotifierhayInventoryServicecó tồn tại hay không.- Nó chỉ publish vào topic
"order.paid", việc còn lại là nhiệm vụ của broker. - Subscriber đăng ký với broker, không đăng ký trực tiếp với
OrderService.
3.1. Pub/Sub trong-process với Spring Events
Spring Boot có sẵn một event bus đơn giản dùng ApplicationEventPublisher. Đây là Pub/Sub ở mức application, chưa cần broker ngoài như 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("Gửi email cho order " + event.getOrderId());
}
}
Giải thích:
OrderServicepublish event quaApplicationEventPublisher— một bean do Spring quản lý, đóng vai trò broker nội bộ.EmailEventListenerkhông đượcOrderServicebiết tới; Spring tự quét các method có@EventListenervà route event đến đúng handler dựa trên type.- Theo mặc định,
@EventListenerchạy đồng bộ cùng thread với publisher — đây là điểm dễ gây nhầm lẫn, nhiều người nghĩ Pub/Sub luôn bất đồng bộ.
Muốn chạy bất đồng bộ thật, thêm @Async:
@Component
public class EmailEventListener {
@Async
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("Gửi email cho order " + event.getOrderId());
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
@EnableAsynckích hoạt proxy để Spring chạy method@Asynctrên thread pool riêng.- Lúc này
payOrder()trả về ngay, không chờEmailEventListenerxử lý xong.
3.2. Pub/Sub qua message broker (Kafka)
Ở quy mô lớn hơn, “broker” không còn là một bean trong JVM mà là một hệ thống riêng (Kafka, RabbitMQ, SQS…). Publisher và subscriber có thể ở hai service, hai process, hai máy khác nhau.
<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("Service email xử lý order " + event.orderId());
}
}
OrderServicevàEmailConsumercó thể nằm trong hai microservice hoàn toàn khác nhau, deploy độc lập.- Kafka giữ message trong topic, consumer đọc theo tốc độ riêng — đây là Pub/Sub thật, có buffer, replay, nhiều subscriber độc lập theo group.
4. So sánh trực tiếp
| Tiêu chí | Observer | Pub/Sub |
|---|---|---|
| Coupling | Subject biết trực tiếp Observer (qua interface) | Publisher không biết Subscriber tồn tại |
| Trung gian | Không có, gọi method trực tiếp | Có broker/event bus đứng giữa |
| Đồng bộ/bất đồng bộ | Mặc định đồng bộ, cùng thread | Có thể đồng bộ (Spring Event) hoặc bất đồng bộ (Kafka, RabbitMQ) |
| Phạm vi | Trong một process/module | Trong process (event bus) hoặc xuyên nhiều service/process |
| Độ bền message | Không có, nếu Subject chết thì mất | Broker ngoài (Kafka) lưu được, cho phép replay |
| Số lượng publisher-subscriber | Thường 1 Subject - nhiều Observer | Nhiều Publisher - nhiều Subscriber qua nhiều topic |
Điểm khác biệt quan trọng nhất: Observer là quan hệ trực tiếp giữa hai object, Pub/Sub là quan hệ qua một lớp trung gian ẩn danh. Về bản chất, Pub/Sub có thể coi là Observer pattern được “nới lỏng” bằng cách chèn thêm broker để loại bỏ sự phụ thuộc trực tiếp.
5. Khi nào dùng Observer, khi nào dùng Pub/Sub
Dùng Observer khi:
- Logic nằm trong cùng module/class, số lượng observer cố định và ít thay đổi.
- Bạn cần phản hồi ngay lập tức, đồng bộ (ví dụ validate dữ liệu trước khi lưu).
- Không cần decouple quá mức — ví dụ UI component lắng nghe thay đổi của model.
class ValidationObserver implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
if (order.getStatus() == null) {
throw new IllegalStateException("Order chưa có status hợp lệ");
}
}
}
Dùng Pub/Sub khi:
- Nhiều service độc lập cần biết về một sự kiện nhưng không nên phụ thuộc lẫn nhau.
- Cần khả năng thêm/xóa subscriber mà không đổi code publisher.
- Cần độ bền message (nếu subscriber đang down, message vẫn chờ khi subscriber online lại).
- Hệ thống là microservices, giao tiếp qua network.
Lưu ý: Đừng dùng Kafka/RabbitMQ chỉ để thay thế một
List<Observer>trong cùng JVM — chi phí vận hành broker không đáng cho bài toán nhỏ. Ngược lại, đừng cố nhồi logic cross-service vào Observer pattern thuần, vì bạn sẽ phải tự viết lại toàn bộ cơ chế network, retry, serialization mà broker đã làm sẵn.
6. Test hành vi Observer
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");
}
}
Giải thích:
- Dùng lambda thay cho class implement
OrderObservervì interface chỉ có một method — gọn hơn khi viết test. assertThat(...).containsExactly(...)xác nhận thứ tự gọi observer đúng như thứ tự đăng ký — đặc điểm quan trọng của Observer đồng bộ mà Pub/Sub qua broker không đảm bảo (Kafka có thể xử lý song song ở nhiều consumer instance).
7. Lỗi hay gặp
- Nhầm Spring
@EventListenerlà bất đồng bộ: mặc định nó chạy đồng bộ, lỗi trong listener sẽ ném exception ngược lên publisher nếu không bắt. - Observer giữ reference mà quên gỡ: gây memory leak, đặc biệt trong ứng dụng có vòng đời object ngắn (request-scope) nhưng đăng ký vào singleton.
- Lạm dụng Pub/Sub cho logic cần transaction nhất quán: nếu publisher và subscriber phải cùng thành công hoặc cùng fail (atomic), Pub/Sub bất đồng bộ không phù hợp — cần xem lại pattern Saga hoặc transactional outbox.
- Không đặt tên topic rõ ràng: trong hệ thống lớn, topic đặt tên mơ hồ như
"event1"khiến không ai biết ai subscribe cái gì, mất hết lợi ích decouple.
8. Kết luận
Observer và Pub/Sub cùng chung một mục tiêu — thông báo thay đổi cho nhiều bên quan tâm — nhưng khác nhau ở mức độ coupling và phạm vi hoạt động. Observer phù hợp cho quan hệ trực tiếp, đồng bộ, trong cùng process; Pub/Sub phù hợp khi cần decouple hoàn toàn publisher khỏi subscriber, đặc biệt trong hệ thống microservices cần độ bền message và khả năng mở rộng theo từng consumer độc lập. Hiểu rõ sự khác biệt này giúp bạn chọn đúng công cụ: không phải lúc nào cũng cần Kafka, và không phải lúc nào List<Observer> cũng đủ cho bài toán của bạn.