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 :

  • observers est la liste des références directes vers les objets intéressés.
  • markAsPaid() appelle onOrderPaid() sur chaque observer dans l’ordre, dans le même thread, de manière synchrone.
  • Order sait 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 : Subject maintient une référence vers Observer, ils doivent s’importer l’interface l’un l’autre.
  • Synchrone : l’appel notify bloque 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 car Subject maintient 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 :

  • OrderService ne sait pas si EmailNotifier ou InventoryService existent.
  • 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 :

  • OrderService publie l’event via ApplicationEventPublisher — un bean géré par Spring, agissant comme un broker interne.
  • EmailEventListener n’est pas connu de OrderService ; Spring scanne lui-même les méthodes portant @EventListener et route l’event vers le bon handler en se basant sur le type.
  • Par défaut, @EventListener s’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 {
}
  • @EnableAsync active le proxy pour que Spring exécute les méthodes @Async sur un pool de threads séparé.
  • À ce moment, payOrder() retourne immédiatement, sans attendre que EmailEventListener finisse 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());
    }
}
  • OrderService et EmailConsumer peuvent ê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 OrderObserver car 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 @EventListener avec 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.

design-patternobserverpub-subjavaspring-boot