¿En qué se diferencia Observer de Pub/Sub?

1. Descripción general

Observer y Pub/Sub ambos resuelven el problema “un evento ocurre, muchos lugares necesitan saberlo”. Muchas personas usan estos dos términos indistintamente porque son similares en idea, pero difieren en implementación y grado de acoplamiento. Este artículo te ayuda a distinguir claramente entre los dos conceptos, con código Java que ilustra cada tipo, y saber cuándo elegir Observer puro, y cuándo usar Pub/Sub a través de un message broker.

2. ¿Qué es Observer Pattern?

Observer es uno de los patrones de comportamiento (behavioral pattern) clásicos en GoF. La idea: un Subject (o Publisher) mantiene una lista de Observers, cuando el estado cambia, Subject llama directamente el método en cada Observer registrado.

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

Explicación:

  • observers es la lista de referencias directas a objetos interesados.
  • markAsPaid() llama onOrderPaid() en cada observer en orden, en el mismo thread, de forma síncrona.
  • Order sabe exactamente quién lo está escuchando — este es el punto clave para distinguirlo de Pub/Sub.
public class EmailNotifier implements OrderObserver {
    @Override
    public void onOrderPaid(Order order) {
        System.out.println("Enviar correo de confirmación para la orden pagada");
    }
}
Order order = new Order();
order.addObserver(new EmailNotifier());
order.markAsPaid();
Enviar correo de confirmación para la orden pagada

2.1. Características fundamentales de Observer

  • Acoplamiento directo: Subject mantiene referencia a Observer, deben importarse mutuamente.
  • Síncrono: la llamada a notify se bloquea hasta que todos los observers terminen de procesar.
  • Mismo proceso: normalmente se ejecuta en una JVM, sin pasar por la red.
  • Subject conoce al Observer: si olvidas llamar a removeObserver(), fácilmente tendrás fuga de memoria porque Subject mantiene una referencia “viva” a un observer que ya no se usa.

Nota: El patrón Observer clásico no tiene un mecanismo integrado de desuscripción segura. Si lo usas en una aplicación de larga duración (por ejemplo, componentes UI, listeners registrados luego destruidos), recuerda llamar a removeObserver() cuando el objeto sea destruido, para evitar fugas de memoria.

3. ¿Qué es Pub/Sub?

Pub/Sub (Publish/Subscribe) separa completamente Publisher y Subscriber a través de una capa intermedia — llamada broker, event bus, o topic. El Publisher no sabe quién está suscrito, solo “dispara” un mensaje a un topic/canal.

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) {
        // procesar pago...
        eventBus.publish("order.paid", new OrderPaidEvent(orderId, amount));
    }
}

Explicación:

  • OrderService no sabe si EmailNotifier o InventoryService existen o no.
  • Solo publica al topic "order.paid", el resto es responsabilidad del broker.
  • El Subscriber se registra con el broker, no directamente con OrderService.

3.1. Pub/Sub en-proceso con Spring Events

Spring Boot tiene un event bus simple usando ApplicationEventPublisher. Este es Pub/Sub a nivel de aplicación, sin necesidad de un broker externo como 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("Enviar correo para la orden " + event.getOrderId());
    }
}

Explicación:

  • OrderService publica el evento a través de ApplicationEventPublisher — un bean administrado por Spring, jugando el rol de broker interno.
  • EmailEventListener no es conocido por OrderService; Spring automáticamente escanea los métodos con @EventListener y enruta el evento al handler correcto basado en el tipo.
  • Por defecto, @EventListener se ejecuta síncronamente en el mismo thread que el publisher — este es un punto fácil de confundir, muchas personas piensan que Pub/Sub siempre es asíncrono.

Para ejecutar de forma asíncrona, agrega @Async:

@Component
public class EmailEventListener {

    @Async
    @EventListener
    public void handle(OrderPaidEvent event) {
        System.out.println("Enviar correo para la orden " + event.getOrderId());
    }
}
@Configuration
@EnableAsync
public class AsyncConfig {
}
  • @EnableAsync activa un proxy para que Spring ejecute el método @Async en un thread pool separado.
  • Ahora payOrder() retorna inmediatamente, sin esperar a que EmailEventListener termine de procesar.

3.2. Pub/Sub mediante message broker (Kafka)

A mayor escala, el “broker” no es un bean dentro de la JVM sino un sistema separado (Kafka, RabbitMQ, SQS…). El Publisher y el Subscriber pueden estar en dos servicios, dos procesos, dos máquinas diferentes.

<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("Servicio de correo procesa la orden " + event.orderId());
    }
}
  • OrderService y EmailConsumer pueden estar en dos microservicios completamente diferentes, desplegados independientemente.
  • Kafka mantiene el mensaje en el topic, el consumer lo lee a su propia velocidad — este es Pub/Sub real, con buffer, replay, múltiples subscribers independientes por grupo.

4. Comparación directa

Criterio Observer Pub/Sub
Acoplamiento Subject conoce directamente a Observer (a través de interfaz) Publisher no sabe si Subscriber existe
Intermediario Ninguno, llama método directamente Hay broker/event bus entre ellos
Síncrono/Asíncrono Por defecto síncrono, mismo thread Puede ser síncrono (Spring Event) o asíncrono (Kafka, RabbitMQ)
Alcance Dentro de un proceso/módulo Dentro de proceso (event bus) o entre múltiples servicios/procesos
Durabilidad de mensajes Ninguna, si Subject muere se pierden Broker externo (Kafka) almacena, permite replay
Cantidad de publisher-subscriber Generalmente 1 Subject - múltiples Observer Múltiples Publisher - múltiples Subscriber sobre múltiples topics

La diferencia más importante: Observer es una relación directa entre dos objetos, Pub/Sub es una relación a través de una capa intermedia anónima. En esencia, Pub/Sub puede verse como Observer pattern “relajado” al insertar un broker para eliminar la dependencia directa.

5. Cuándo usar Observer, cuándo usar Pub/Sub

Usa Observer cuando:

  • La lógica está en el mismo módulo/clase, el número de observers es fijo y cambia poco.
  • Necesitas respuesta inmediata y síncrona (por ejemplo, validar datos antes de guardar).
  • No necesitas desacoplar demasiado — por ejemplo, componentes UI escuchando cambios del modelo.
class ValidationObserver implements OrderObserver {
    @Override
    public void onOrderPaid(Order order) {
        if (order.getStatus() == null) {
            throw new IllegalStateException("La orden no tiene un estado válido");
        }
    }
}

Usa Pub/Sub cuando:

  • Múltiples servicios independientes necesitan saber sobre un evento pero no deben depender uno del otro.
  • Necesitas la capacidad de agregar/eliminar subscribers sin cambiar el código del publisher.
  • Necesitas durabilidad de mensajes (si un subscriber está caído, el mensaje aún espera cuando se conecte de nuevo).
  • El sistema es microservicios, comunicándose por red.

Nota: No uses Kafka/RabbitMQ solo para reemplazar una List<Observer> dentro de la misma JVM — el costo de operar un broker no vale para problemas pequeños. Por el contrario, no intentes embutir lógica entre servicios en Observer pattern puro, porque tendrías que escribir todo el mecanismo de red, reintentos, serialización que el broker ya hace.

6. Testear el comportamiento de 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");
    }
}

Explicación:

  • Usa lambda en lugar de clase implementando OrderObserver porque la interfaz tiene solo un método — más limpio al escribir tests.
  • assertThat(...).containsExactly(...) confirma que el orden de llamadas a observers es correcto según el orden de registro — una característica importante de Observer síncrono que Pub/Sub sobre broker no garantiza (Kafka puede procesar en paralelo en múltiples instancias de consumer).

7. Errores comunes

  • Confundir Spring @EventListener con asíncrono: por defecto se ejecuta síncronamente, una excepción en el listener se lanzará de vuelta al publisher si no la capturas.
  • Observer mantiene referencia pero se olvida de removerla: causa fuga de memoria, especialmente en aplicaciones con ciclo de vida corto de objetos (request-scope) pero registrados en un singleton.
  • Abusar de Pub/Sub para lógica que necesita transacción consistente: si el publisher y el subscriber deben ambos tener éxito o ambos fallar (atómico), Pub/Sub asíncrono no es apropiado — revisa patrones como Saga u outbox transaccional.
  • No nombrar topics claramente: en sistemas grandes, topics con nombres vagos como "event1" hacen que nadie sepa quién se suscribe a qué, perdiéndose los beneficios del desacoplamiento.

8. Conclusión

Observer y Pub/Sub comparten un objetivo — notificar cambios a múltiples partes interesadas — pero difieren en grado de acoplamiento y alcance de operación. Observer es apropiado para relaciones directas, síncronas, dentro del mismo proceso; Pub/Sub es apropiado cuando necesitas desacoplar completamente el publisher del subscriber, especialmente en sistemas de microservicios que requieren durabilidad de mensajes y escalabilidad en cada consumer independiente. Entender claramente esta diferencia te ayuda a elegir la herramienta correcta: no siempre necesitas Kafka, y no siempre una List<Observer> es suficiente para tu problema.

patrón-de-diseñoobserverpub-subjavaspring-boot