Worin unterscheidet sich Observer von Pub/Sub?

1. Übersicht

Observer und Pub/Sub lösen beide die Aufgabe „Ein Ereignis tritt auf, viele Stellen müssen davon erfahren”. Viele verwechseln diese beiden Begriffe, weil sie in der Idee ähnlich sind, aber in der Implementierung und dem Grad der Entkopplung unterscheiden. Dieser Artikel hilft dir, die beiden Konzepte klar zu unterscheiden, mit konkreten Java-Code-Beispielen für jeden Typ, und zu wissen, wann du das reine Observer-Pattern verwenden solltest und wann du Pub/Sub über einen Message Broker nutzen solltest.

2. Was ist das Observer Pattern?

Observer ist eines der klassischen verhaltensorientierten Patterns (behavioral pattern) aus dem GoF. Die Idee: Ein Subject (oder Publisher) speichert eine Liste von Observern. Wenn sich der Zustand ändert, ruft das Subject direkt die Methode auf jedem registrierten Observer auf.

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);
        }
    }
}

Erklärung:

  • observers ist eine Liste mit direkten Referenzen zu den interessierten Objekten.
  • markAsPaid() ruft onOrderPaid() auf jedem Observer nacheinander auf, im selben Thread, synchron.
  • Order kennt genau, wer es abhört — das ist der Kernpunkt zur Unterscheidung von Pub/Sub.
public class EmailNotifier implements OrderObserver {
    @Override
    public void onOrderPaid(Order order) {
        System.out.println("Bestätigungs-E-Mail für bezahlte Bestellung senden");
    }
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
Bestätigungs-E-Mail für bezahlte Bestellung senden

2.1. Kern-Merkmale des Observer Patterns

  • Direkte Entkopplung: Das Subject hält eine Referenz zum Observer, muss die Interface des anderen importieren.
  • Synchron: Der notify-Aufruf blockiert, bis alle Observer fertig sind.
  • Im selben Prozess: Läuft typischerweise in einer JVM, nicht über das Netzwerk.
  • Subject kennt den Observer: Wenn du vergisst, removeObserver() aufzurufen, läufst du Gefahr, einen Memory Leak zu bekommen, da das Subject eine „lebende” Referenz zum ungenutzten Observer hält.

Hinweis: Das klassische Observer Pattern hat keinen eingebauten sicheren Unsubscribe-Mechanismus. Wenn du es in langlebigen Anwendungen verwendest (z.B. UI-Komponenten, registrierte Listener, die zerstört werden), denk daran, removeObserver() beim Zerstören des Objekts aufzurufen, um Memory Leaks zu vermeiden.

3. Was ist Pub/Sub?

Pub/Sub (Publish/Subscribe) trennt Publisher und Subscriber vollständig durch eine Zwischenschicht — genannt Broker, Event Bus oder Topic. Der Publisher weiß nicht, wer subscribed, er „schießt” die Nachricht nur in ein 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) {
        // Zahlung verarbeiten...
        eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
    }
}

Erklärung:

  • OrderService weiß nicht, ob EmailNotifier oder InventoryService existieren.
  • Es publisht nur in das Topic "order.paid". Der Rest ist Aufgabe des Brokers.
  • Der Subscriber meldet sich beim Broker an, nicht direkt beim OrderService.

3.1. Pub/Sub innerhalb eines Prozesses mit Spring Events

Spring Boot hat schon einen einfachen Event Bus mit ApplicationEventPublisher. Das ist Pub/Sub auf Anwendungsebene, ohne externen Broker wie 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("E-Mail für Bestellung " + event.getOrderId() + " senden");
    }
}

Erklärung:

  • OrderService publisht das Event über ApplicationEventPublisher — ein von Spring verwaltetes Bean, das als interner Broker fungiert.
  • EmailEventListener ist dem OrderService nicht bekannt; Spring scannt automatisch Methoden mit @EventListener und routet Events basierend auf dem Typ.
  • Standardmäßig läuft @EventListener synchron im selben Thread wie der Publisher — das ist ein häufiger Verwirrungspunkt, viele denken, Pub/Sub sei immer asynchron.

Für echte Asynchronität @Async hinzufügen:

@Component
public class EmailEventListener {

    @Async
    @EventListener
    public void handle(OrderPaidEvent event) {
        System.out.println("E-Mail für Bestellung " + event.getOrderId() + " senden");
    }
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
  • @EnableAsync aktiviert den Proxy, damit Spring die @Async-Methode in einem separaten Thread Pool läuft.
  • Jetzt gibt payOrder() sofort zurück, wartet nicht, dass EmailEventListener fertig ist.

3.2. Pub/Sub über einen Message Broker (Kafka)

In größerem Maßstab ist der „Broker” keine Bean in der JVM mehr, sondern ein separates System (Kafka, RabbitMQ, SQS…). Publisher und Subscriber können in zwei Services, zwei Prozessen, zwei verschiedenen Maschinen sein.

<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 verarbeitet Bestellung " + event.orderId());
    }
}
  • OrderService und EmailConsumer können in zwei völlig verschiedenen Microservices sein, unabhängig deployed.
  • Kafka speichert die Nachricht im Topic, Consumer lesen mit eigener Geschwindigkeit — das ist echtes Pub/Sub, mit Puffer, Replay, mehreren unabhängigen Subscribern pro Gruppe.

4. Direkter Vergleich

Kriterium Observer Pub/Sub
Entkopplung Subject kennt Observer direkt (über Interface) Publisher kennt Subscriber nicht
Zwischenschicht Keine, direkte Methoden-Aufrufe Hat Broker/Event Bus dazwischen
Synchron/Asynchron Standardmäßig synchron, selber Thread Kann synchron sein (Spring Event) oder asynchron (Kafka, RabbitMQ)
Reichweite Innerhalb eines Prozesses/Moduls Innerhalb eines Prozesses (Event Bus) oder über mehrere Services/Prozesse
Nachrichten-Persistenz Keine, wenn Subject stirbt, ist die Nachricht weg Broker speichert (Kafka) und erlaubt Replay
Anzahl Publisher-Subscriber Typisch 1 Subject - viele Observer Viele Publisher - viele Subscriber über mehrere Topics

Der wichtigste Unterschied: Observer ist eine direkte Beziehung zwischen zwei Objekten, Pub/Sub ist eine Beziehung durch eine anonyme Zwischenschicht. Im Grunde ist Pub/Sub das Observer Pattern, bei dem der Coupling durch Einfügung eines Brokers gelockert wird.

5. Wann Observer verwenden, wann Pub/Sub?

Observer verwenden, wenn:

  • Die Logik im selben Modul/Class liegt, die Anzahl der Observer ist fix und ändert sich selten.
  • Du brauchst sofortige Rückmeldung, synchron (z.B. Daten validieren, bevor sie gespeichert werden).
  • Du nicht zu viel entkoppeln musst — z.B. UI-Komponente hört auf Model-Änderungen.
class ValidationObserver implements OrderObserver {
    @Override
    public void onOrderPaid(Order order) {
        if (order.getStatus() == null) {
            throw new IllegalStateException("Bestellung hat keinen gültigen Status");
        }
    }
}

Pub/Sub verwenden, wenn:

  • Mehrere unabhängige Services müssen von einem Event wissen, sollten aber nicht voneinander abhängen.
  • Du die Möglichkeit brauchst, Subscriber hinzuzufügen/zu entfernen, ohne den Publisher-Code zu ändern.
  • Du brauchst Nachrichten-Persistenz (wenn ein Subscriber gerade offline ist, wartet die Nachricht, bis der Subscriber wieder online kommt).
  • Das System ist Microservices, Kommunikation über das Netzwerk.

Hinweis: Nutze Kafka/RabbitMQ nicht nur, um eine List<Observer> in derselben JVM zu ersetzen — die Betriebskosten des Brokers lohnen sich für solche kleinen Aufgaben nicht. Umgekehrt: Versuche nicht, Cross-Service-Logik in reines Observer Pattern zu zwingen, weil du sonst alles selbst reimplementieren musst — Netzwerk, Retry, Serialisierung — was der Broker schon erledigt.

6. Observer-Verhalten testen

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");
    }
}

Erklärung:

  • Lambda statt Class-Implementation der OrderObserver-Interface, weil die Interface nur eine Methode hat — kompakter in Tests.
  • assertThat(...).containsExactly(...) bestätigt, dass die Observer in der Reihenfolge aufgerufen wurden, in der sie registriert wurden — eine wichtige Eigenschaft von synchronem Observer, die Pub/Sub über einen Broker nicht garantiert (Kafka kann mehrere Consumer-Instanzen parallel verarbeiten).

7. Häufige Fehler

  • Confusing Spring @EventListener mit asynchron: Standardmäßig läuft es synchron, Fehler im Listener werfen Exception zurück zum Publisher, wenn nicht abgefangen.
  • Observer mit Referenz, die man vergisst zu entfernen: Erzeugt Memory Leak, besonders in Anwendungen mit kurzem Object-Lifecycle (Request-Scope), aber registriert bei Singleton.
  • Pub/Sub missbrauchen für Logik, die atomare Konsistenz braucht: Wenn Publisher und Subscriber zusammen erfolgreich sein oder zusammen fehlschlagen müssen (atomic), passt asynchrones Pub/Sub nicht — schau dir Saga oder transactional outbox an.
  • Unklar benannte Topics: In großen Systemen führen vage Topicnamen wie "event1" dazu, dass niemand weiß, wer was subscribed, der Decoupling-Vorteil ist weg.

8. Fazit

Observer und Pub/Sub verfolgen das gleiche Ziel — Änderungen an mehrere interessierte Parteien mitteilen — unterscheiden sich aber in Entkopplungsgrad und Reichweite. Observer passt zu direkten, synchronen Beziehungen innerhalb eines Prozesses; Pub/Sub passt, wenn du Publisher und Subscriber vollständig entkoppeln brauchst, besonders in Microservice-Systemen, die Nachrichten-Persistenz und unabhängig skalierbare Consumer brauchen. Das klare Verständnis dieser Unterschiede hilft dir, das richtige Werkzeug zu wählen: nicht jedes Problem braucht Kafka, und nicht jedes Problem lässt sich mit List<Observer> lösen.

design-patternobserverpub-subjavaspring-boot