Observer vs Pub/Sub : Les différences clés
1. Tổng quan
Observer et Pub/Sub résolvent tous deux le problème « un événement se produit, et beaucoup de places doivent le savoir ». Beaucoup d’entre vous les utilisez indifféremment car ils se ressemblent dans l’idée, mais ils diffèrent dans leur implémentation et leur degré de couplage. Cet article vous aide à distinguer clairement les deux concepts, avec du code Java illustrant chaque type, et à savoir quand choisir un Observer pur, et quand utiliser Pub/Sub via un message broker.
2. Observer Pattern est quoi
Observer est l’un des patterns comportementaux (behavioral patterns) classiques du GoF. L’idée : un Subject (ou Publisher) maintient une liste d’Observer, quand l’état change, le Subject appelle directement la méthode sur chaque Observer enregistré.
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);
}
}
}
Explication :
observersest la liste des références directes vers les objets intéressés.markAsPaid()appelleonOrderPaid()sur chaque observer dans l’ordre, dans le même thread, de manière synchrone.Ordersait exactement qui l’écoute — c’est le point clé pour distinguer avec Pub/Sub.
public class EmailNotifier implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
System.out.println("Envoyer un e-mail de confirmation pour la commande payée");
}
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
Envoyer un e-mail de confirmation pour la commande payée
2.1. Caractéristiques clés d’Observer
- Couplage direct :
Subjectmaintient une référence versObserver, ils doivent s’importer l’interface l’un l’autre. - Synchrone : l’appel
notifybloque jusqu’à ce que tous les observers aient fini de traiter. - Dans le même processus : s’exécute généralement dans une seule JVM, pas via le réseau.
- Subject connaît l’existence de l’observer : si vous oubliez d’appeler
removeObserver(), vous risquez une fuite mémoire carSubjectmaintient une référence « active » vers un observer qui n’est plus utilisé.
Attention : Le pattern Observer classique n’a pas de mécanisme de désinscription sûr par nature. Si vous l’utilisez dans une application à long terme (par exemple, une UI component, listener enregistré puis détruit), n’oubliez pas d’appeler
removeObserver()quand l’objet est détruit, pour éviter les fuites mémoire.
3. Pub/Sub est quoi
Pub/Sub (Publish/Subscribe) sépare complètement le Publisher et le Subscriber via une couche intermédiaire — appelée broker, event bus, ou topic. Le publisher ne sait pas qui est abonné, il « publie » simplement le message sur un 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) {
// traiter le paiement...
eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
}
}
Explication :
OrderServicene sait pas siEmailNotifierouInventoryServiceexistent.- Il publie simplement sur le topic
"order.paid", le reste est la responsabilité du broker. - Les subscribers s’enregistrent auprès du broker, pas directement auprès de
OrderService.
3.1. Pub/Sub in-process avec Spring Events
Spring Boot fournit un event bus simple utilisant ApplicationEventPublisher. C’est du Pub/Sub au niveau application, sans avoir besoin d’un broker externe comme 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("Envoyer un e-mail pour la commande " + event.getOrderId());
}
}
Explication :
OrderServicepublie l’event viaApplicationEventPublisher— un bean géré par Spring, agissant comme un broker interne.EmailEventListenern’est pas connu deOrderService; Spring scanne lui-même les méthodes portant@EventListeneret route l’event vers le bon handler en se basant sur le type.- Par défaut,
@EventListeners’exécute de manière synchrone dans le même thread que le publisher — c’est un point de confusion fréquent, beaucoup pensent que Pub/Sub est toujours asynchrone.
Pour exécuter de manière véritablement asynchrone, ajoutez @Async :
@Component
public class EmailEventListener {
@Async
@EventListener
public void handle(OrderPaidEvent event) {
System.out.println("Envoyer un e-mail pour la commande " + event.getOrderId());
}
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
@EnableAsyncactive le proxy pour que Spring exécute les méthodes@Asyncsur un pool de threads séparé.- À ce moment,
payOrder()retourne immédiatement, sans attendre queEmailEventListenerfinisse de traiter.
3.2. Pub/Sub via un message broker (Kafka)
À plus grande échelle, le « broker » n’est plus un bean dans une JVM mais un système à part entière (Kafka, RabbitMQ, SQS…). Le publisher et le subscriber peuvent être dans deux services, deux processus, deux machines différentes.
<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("Le service email traite la commande " + event.orderId());
}
}
OrderServiceetEmailConsumerpeuvent être dans deux microservices complètement différents, déployés indépendamment.- Kafka garde les messages dans le topic, les consumers les lisent à leur propre rythme — c’est du vrai Pub/Sub avec buffer, replay, plusieurs subscribers indépendants par groupe.
4. Comparaison directe
| Critère | Observer | Pub/Sub |
|---|---|---|
| Couplage | Subject connaît directement Observer (via l’interface) | Publisher ne sait pas que Subscriber existe |
| Intermédiaire | Aucun, appel de méthode direct | Il existe un broker/event bus entre les deux |
| Synchrone/asynchrone | Synchrone par défaut, même thread | Peut être synchrone (Spring Event) ou asynchrone (Kafka, RabbitMQ) |
| Portée | Dans un processus/module | Dans un processus (event bus) ou entre plusieurs services/processus |
| Durabilité des messages | Aucune, si Subject meurt alors perte | Le broker externe (Kafka) stocke et permet le replay |
| Nombre de publisher-subscriber | Généralement 1 Subject - plusieurs Observer | Plusieurs Publisher - plusieurs Subscriber via plusieurs topics |
La différence la plus importante : Observer est une relation directe entre deux objets, tandis que Pub/Sub est une relation via une couche intermédiaire anonyme. Essentiellement, Pub/Sub peut être vu comme le pattern Observer « assoupli » en insérant un broker pour éliminer la dépendance directe.
5. Quand utiliser Observer, quand utiliser Pub/Sub
Utilisez Observer quand :
- La logique se trouve dans le même module/classe, le nombre d’observers est fixe et change rarement.
- Vous avez besoin d’une réponse immédiate et synchrone (par exemple, valider les données avant de les enregistrer).
- Vous n’avez pas besoin de découpler à outrance — par exemple, une UI component qui écoute les changements du model.
class ValidationObserver implements OrderObserver {
@Override
public void onOrderPaid(Order order) {
if (order.getStatus() == null) {
throw new IllegalStateException("La commande n'a pas de statut valide");
}
}
}
Utilisez Pub/Sub quand :
- Plusieurs services indépendants doivent connaître un événement mais ne doivent pas dépendre les uns des autres.
- Vous avez besoin de la capacité à ajouter/supprimer des subscribers sans modifier le code du publisher.
- Vous avez besoin de la durabilité des messages (si un subscriber est hors ligne, le message attend son retour en ligne).
- Le système est composé de microservices, communiquant via le réseau.
Attention : N’utilisez pas Kafka/RabbitMQ juste pour remplacer une
List<Observer>dans la même JVM — le coût d’exploitation du broker ne vaut pas le coup pour un petit problème. À l’inverse, n’essayez pas de forcer la logique cross-service dans un pattern Observer pur, car vous devrez réinventer l’intégralité du mécanisme de réseau, retry, sérialisation que le broker fournit déjà.
6. Tester le comportement d’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");
}
}
Explication :
- Utilisez des lambdas au lieu d’une classe implémentant
OrderObservercar l’interface n’a qu’une seule méthode — plus concis lors de l’écriture de tests. assertThat(...).containsExactly(...)confirme que l’ordre d’appel des observers correspond à l’ordre d’enregistrement — une caractéristique importante d’Observer synchrone que Pub/Sub via broker ne garantit pas (Kafka peut traiter en parallèle sur plusieurs instances de consumer).
7. Erreurs courantes
- Confondre Spring
@EventListeneravec asynchrone : par défaut, il s’exécute de manière synchrone, une erreur dans le listener lèvera une exception vers le publisher s’il n’est pas interceptée. - Observer retient une référence qu’on oublie de supprimer : cause une fuite mémoire, surtout dans les applications avec un cycle de vie d’objet court (request-scope) mais enregistré dans un singleton.
- Abuser de Pub/Sub pour une logique qui demande des transactions cohérentes : si le publisher et le subscriber doivent réussir ou échouer ensemble (atomique), Pub/Sub asynchrone ne convient pas — il faut revoir le pattern Saga ou transactional outbox.
- Ne pas nommer les topics clairement : dans un grand système, des topics avec des noms vagues comme
"event1"font que personne ne sait qui s’abonne à quoi, perdant tous les bénéfices du découplage.
8. Conclusion
Observer et Pub/Sub partagent le même objectif — notifier les changements à plusieurs parties intéressées — mais diffèrent dans leur degré de couplage et leur portée d’action. Observer convient pour les relations directes, synchrones, dans le même processus ; Pub/Sub convient quand vous avez besoin de découpler complètement le publisher du subscriber, surtout dans les systèmes de microservices qui nécessitent la durabilité des messages et la capacité à évoluer avec chaque consumer indépendant. Comprendre clairement cette différence vous aide à choisir le bon outil : vous n’avez pas toujours besoin de Kafka, et une simple List<Observer> n’est pas toujours suffisante pour votre problème.