# Publish-Subscribe Pattern

## Problem

When a component produces events, hardcoding calls to every interested consumer creates tight coupling. Adding a new consumer requires modifying the producer. The producer must know about all consumers, their interfaces, and handle failures for each one.

## Solution

Introduce a **message broker** (or topic) between publishers and subscribers. Publishers emit messages to a named topic without knowing who will receive them. Subscribers register interest in topics and receive all messages published to those topics. This is a **fan-out** pattern — each message is delivered to all subscribers.

## When to Use

- Event notifications across microservices
- Broadcasting state changes (user created, order placed)
- When new consumers are added frequently
- Audit logging, analytics, and monitoring
- When producers and consumers should evolve independently

## When to Avoid

- When exactly-one delivery is required (use message queue)
- Simple request-response communication
- Tight latency requirements where broker overhead matters
- When there's always exactly one consumer (message queue is simpler)

## Pseudocode

```
class PubSub:
    subscribers = {}  # topic → [callback, ...]

    function subscribe(topic, callback):
        subscribers[topic].add(callback)

    function publish(topic, message):
        for callback in subscribers[topic]:
            callback(message)

    function unsubscribe(topic, callback):
        subscribers[topic].remove(callback)
```

## Python Implementation

```python
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Callable, Any
import time


@dataclass
class Message:
    topic: str
    payload: Any
    timestamp: float = field(default_factory=time.time)


class PubSub:
    def __init__(self):
        self._subscribers: dict[str, list[Callable]] = defaultdict(list)
        self._message_count = 0

    def subscribe(self, topic: str, callback: Callable[[Message], None]) -> None:
        self._subscribers[topic].append(callback)
        print(f"  [PubSub] Subscribed to '{topic}': {callback.__name__}")

    def unsubscribe(self, topic: str, callback: Callable) -> None:
        self._subscribers[topic].remove(callback)

    def publish(self, topic: str, payload: Any) -> int:
        message = Message(topic=topic, payload=payload)
        subscribers = self._subscribers.get(topic, [])
        self._message_count += 1
        print(f"  [PubSub] Publishing to '{topic}' ({len(subscribers)} subscribers)")

        delivered = 0
        for callback in subscribers:
            try:
                callback(message)
                delivered += 1
            except Exception as exc:
                print(f"  [PubSub] Subscriber error: {exc}")
        return delivered

    @property
    def stats(self):
        return {
            "topics": len(self._subscribers),
            "total_subscribers": sum(len(s) for s in self._subscribers.values()),
            "messages_published": self._message_count,
        }


# --- Demo ---
bus = PubSub()

# Subscribers
def email_notifier(msg: Message):
    print(f"    [Email] Sending email for {msg.topic}: {msg.payload}")

def audit_logger(msg: Message):
    print(f"    [Audit] Logging {msg.topic}: {msg.payload}")

def analytics_tracker(msg: Message):
    print(f"    [Analytics] Tracking {msg.topic}: {msg.payload}")

def inventory_handler(msg: Message):
    print(f"    [Inventory] Updating stock: {msg.payload}")


print("=== Setting up subscriptions ===")
bus.subscribe("user.created", email_notifier)
bus.subscribe("user.created", audit_logger)
bus.subscribe("user.created", analytics_tracker)
bus.subscribe("order.placed", email_notifier)
bus.subscribe("order.placed", audit_logger)
bus.subscribe("order.placed", inventory_handler)

print()
print("=== Publishing 'user.created' (3 subscribers) ===")
bus.publish("user.created", {"user_id": 42, "name": "Alice"})

print()
print("=== Publishing 'order.placed' (3 subscribers) ===")
bus.publish("order.placed", {"order_id": "ORD-001", "total": 99.99})

print()
print("=== Publishing to topic with no subscribers ===")
delivered = bus.publish("product.updated", {"product_id": 7})
print(f"    Delivered to {delivered} subscribers")

print()
print(f"=== Stats: {bus.stats} ===")
```

**Output:**
```
=== Setting up subscriptions ===
  [PubSub] Subscribed to 'user.created': email_notifier
  [PubSub] Subscribed to 'user.created': audit_logger
  [PubSub] Subscribed to 'user.created': analytics_tracker
  [PubSub] Subscribed to 'order.placed': email_notifier
  [PubSub] Subscribed to 'order.placed': audit_logger
  [PubSub] Subscribed to 'order.placed': inventory_handler

=== Publishing 'user.created' (3 subscribers) ===
  [PubSub] Publishing to 'user.created' (3 subscribers)
    [Email] Sending email for user.created: {'user_id': 42, 'name': 'Alice'}
    [Audit] Logging user.created: {'user_id': 42, 'name': 'Alice'}
    [Analytics] Tracking user.created: {'user_id': 42, 'name': 'Alice'}

=== Publishing 'order.placed' (3 subscribers) ===
  [PubSub] Publishing to 'order.placed' (3 subscribers)
    [Email] Sending email for order.placed: {'order_id': 'ORD-001', 'total': 99.99}
    [Audit] Logging order.placed: {'order_id': 'ORD-001', 'total': 99.99}
    [Inventory] Updating stock: {'order_id': 'ORD-001', 'total': 99.99}

=== Publishing to topic with no subscribers ===
  [PubSub] Publishing to 'product.updated' (0 subscribers)
    Delivered to 0 subscribers

=== Stats: {'topics': 3, 'total_subscribers': 6, 'messages_published': 3} ===
```

## Go Implementation

```go
package main

import (
	"fmt"
	"sync"
	"time"
)

type Message struct {
	Topic     string
	Payload   interface{}
	Timestamp time.Time
}

type Subscriber func(msg Message)

type PubSub struct {
	mu          sync.RWMutex
	subscribers map[string][]Subscriber
}

func NewPubSub() *PubSub {
	return &PubSub{subscribers: make(map[string][]Subscriber)}
}

func (ps *PubSub) Subscribe(topic string, sub Subscriber) {
	ps.mu.Lock()
	defer ps.mu.Unlock()
	ps.subscribers[topic] = append(ps.subscribers[topic], sub)
}

func (ps *PubSub) Publish(topic string, payload interface{}) int {
	ps.mu.RLock()
	subs := ps.subscribers[topic]
	ps.mu.RUnlock()

	msg := Message{Topic: topic, Payload: payload, Timestamp: time.Now()}
	fmt.Printf("  [PubSub] Publishing to '%s' (%d subscribers)\n", topic, len(subs))

	delivered := 0
	for _, sub := range subs {
		sub(msg)
		delivered++
	}
	return delivered
}

func main() {
	ps := NewPubSub()

	ps.Subscribe("user.created", func(m Message) {
		fmt.Printf("    [Email] %s: %v\n", m.Topic, m.Payload)
	})
	ps.Subscribe("user.created", func(m Message) {
		fmt.Printf("    [Audit] %s: %v\n", m.Topic, m.Payload)
	})
	ps.Subscribe("order.placed", func(m Message) {
		fmt.Printf("    [Email] %s: %v\n", m.Topic, m.Payload)
	})
	ps.Subscribe("order.placed", func(m Message) {
		fmt.Printf("    [Inventory] %s: %v\n", m.Topic, m.Payload)
	})

	fmt.Println("=== Publishing 'user.created' ===")
	ps.Publish("user.created", map[string]interface{}{"user_id": 42, "name": "Alice"})

	fmt.Println()
	fmt.Println("=== Publishing 'order.placed' ===")
	ps.Publish("order.placed", map[string]interface{}{"order_id": "ORD-001"})

	fmt.Println()
	fmt.Println("=== Publishing to empty topic ===")
	n := ps.Publish("product.updated", map[string]interface{}{"id": 7})
	fmt.Printf("    Delivered to %d subscribers\n", n)
}
```

## JavaScript Implementation

```javascript
class PubSub {
  constructor() {
    this._subscribers = new Map();
    this._messageCount = 0;
  }

  subscribe(topic, callback) {
    if (!this._subscribers.has(topic)) {
      this._subscribers.set(topic, []);
    }
    this._subscribers.get(topic).push(callback);
    console.log(`  [PubSub] Subscribed to '${topic}': ${callback.name || "anonymous"}`);
  }

  publish(topic, payload) {
    const subs = this._subscribers.get(topic) || [];
    this._messageCount++;
    const msg = { topic, payload, timestamp: Date.now() };
    console.log(`  [PubSub] Publishing to '${topic}' (${subs.length} subscribers)`);

    let delivered = 0;
    for (const cb of subs) {
      try { cb(msg); delivered++; }
      catch (e) { console.log(`  [PubSub] Subscriber error: ${e.message}`); }
    }
    return delivered;
  }
}

// --- Demo ---
const ps = new PubSub();

ps.subscribe("user.created", function emailNotifier(msg) {
  console.log(`    [Email] ${msg.topic}: ${JSON.stringify(msg.payload)}`);
});
ps.subscribe("user.created", function auditLogger(msg) {
  console.log(`    [Audit] ${msg.topic}: ${JSON.stringify(msg.payload)}`);
});
ps.subscribe("order.placed", function inventoryHandler(msg) {
  console.log(`    [Inventory] ${msg.topic}: ${JSON.stringify(msg.payload)}`);
});

console.log("\n=== Publishing 'user.created' ===");
ps.publish("user.created", { userId: 42, name: "Alice" });

console.log("\n=== Publishing 'order.placed' ===");
ps.publish("order.placed", { orderId: "ORD-001", total: 99.99 });

console.log("\n=== Empty topic ===");
const n = ps.publish("product.updated", { id: 7 });
console.log(`    Delivered to ${n} subscribers`);
```

## Related Patterns

- **Observer** — Observer is the in-process equivalent of Pub-Sub. Pub-Sub adds a message broker for decoupled, cross-process delivery. Use Observer within a process; Pub-Sub across processes.
- **Message Queue** — Pub-Sub broadcasts to all subscribers; Message Queue delivers to one consumer (competing consumers). Combine: Pub-Sub fans out; each fan-out leg goes into a Message Queue.
- **Event Bus** — Event Bus is an in-process Pub-Sub: same concept, no network. Observer and Event Bus are both in-process pub-sub alternatives.
- **Dead Letter Queue** — When Pub-Sub delivery fails repeatedly, Dead Letter Queue receives undeliverable messages.
- **Event Sourcing** — Event Sourcing uses Pub-Sub to broadcast events to multiple consumers (projections, sagas, external systems).
