system-design · intermediate

Publish/Subscribe Messaging — One Event, Many Interested Listeners

The Central Question

Consider an online checkout platform running on the MessageLab platform (messagelab.com) processing 100,000,000 asynchronous events per day:

1. Email Service: Send a receipt email to the customer.
2. Analytics Service: Stream the order record to the real-time revenue dashboard.
3. Search Indexer: Index the new purchase so it appears in the user's order history search.
4. Loyalty Service: Calculate and award reward points to the customer's account.

In a traditional point-to-point architecture, the Checkout API Service must contain explicit code invocation logic for every single downstream service.

When the marketing team launches a new Fraud Machine Learning Service six months later, developers must modify, re-test, and re-deploy the core Checkout API Service source code just to add a fifth downstream call!

Furthermore, if the Analytics Service crashes, its failure risks polluting or blocking the payment checkout flow.

To decouple producers of domain events from consumers of those events, systems deploy Publish/Subscribe Messaging (Pub/Sub).

Publish/Subscribe Messaging is a messaging pattern where Publishers emit named immutable facts called Events to a central Topic, and multiple independent Subscribers register interest to receive their own copy of every published event.

This lesson answers one central question: How does Pub/Sub execute multi-subscriber Fan-Out routing, how do log-based brokers use Partitioning to preserve per-key order, and how do engineering teams manage Schema Evolution without breaking downstream consumers?


Competing Consumers vs. Multi-Subscriber Fan-Out

It is critical to distinguish classic Point-to-Point Message Queues from Publish/Subscribe Topics:

flowchart TD
  subgraph Point-to-Point Message Queue (Single Execution)
    QProducer[Producer] --> Queue[(Task Queue)]
    Queue --> QW1[Worker 1]
    Queue --> QW2[Worker 2]
    Note1["Each task is processed by EXACTLY ONE worker instance."]
  end
  
  subgraph Publish/Subscribe Topic (Fan-Out Copies)
    PProducer[Publisher] --> Topic((Orders Topic))
    Topic --> Sub1[Email Service Subscription]
    Topic --> Sub2[Analytics Service Subscription]
    Topic --> Sub3[Loyalty Service Subscription]
    Note2["EVERY independent subscription receives its OWN COPY of the event!"]
  end

Figure 1: Contrast between point-to-point queue single worker processing and Pub/Sub multi-subscription fan-out.

Architectural Comparison Matrix

DimensionPoint-to-Point Message QueuePublish/Subscribe (Pub/Sub) Topic
Delivery Model1-to-1 (Competing Consumers share task distribution).1-to-Many (Fan-out to all active subscriptions).
Consumer IdentityWorkers are homogeneous nodes executing identical task logic.Subscribers are heterogeneous independent microservice domains.
Publisher AwarenessPublisher targets a specific named work queue (e.g. pdf-render-queue).Publisher emits a domain fact (e.g. OrderPaid) without knowing subscribers.
Adding New HandlersRequires routing new work to new queues or modifying existing workers.Zero publisher changes required; subscribe a new service to the topic.

Fan-Out Topologies: Topic-to-Queue Routing vs. Partitioned Event Logs

Modern Pub/Sub infrastructure follows two distinct architectural topologies:

1. SNS + SQS Fan-Out Topology (Cloud Message Fan-Out)

In cloud message topologies (such as AWS SNS + SQS or Google Cloud Pub/Sub), a central **Topic** forwards incoming events into multiple individual **Dedicated Queues** owned by each subscribing service:
flowchart LR
  Pub[Checkout Publisher] --> Topic((SNS Topic: orders.paid))
  
  Topic --> Q1[(SQS Queue: email-service)]
  Topic --> Q2[(SQS Queue: analytics-service)]
  Topic --> Q3[(SQS Queue: loyalty-service)]
  
  Q1 --> W1[Email Workers]
  Q2 --> W2[Analytics Workers]
  Q3 --> W3[Loyalty Workers]

Figure 2: AWS SNS + SQS Fan-Out pattern routing a single topic publish into dedicated service queues.


Partitioning and Message Ordering (Apache Kafka / Event Hubs)

In distributed log-based Pub/Sub brokers (such as Apache Kafka or Azure Event Hubs), topics are partitioned across multiple physical server disks to scale throughput:

flowchart TD
  Pub[Publisher] --> Router{Hash Partition Key: user_id}
  
  Router -->|user_id: 101| P0[(Partition 0 Log)]
  Router -->|user_id: 102| P1[(Partition 1 Log)]
  Router -->|user_id: 103| P2[(Partition 2 Log)]
  
  subgraph Consumer Group: Analytics Service
    C1[Consumer Worker 1] --> P0
    C2[Consumer Worker 2] --> P1
    C3[Consumer Worker 3] --> P2
  end

Figure 3: Partitioned Pub/Sub topic preserving per-key message ordering across a Consumer Group.

The Partition Ordering Guarantee

"Message ordering is strictly guaranteed WITHIN a single partition, but NOT across different partitions."

By setting the partition key to user_id or account_id, all events for that specific user land on the exact same partition in total sequential order (OrderCreated $\rightarrow$ OrderPaid $\rightarrow$ OrderShipped), preserving causal state ordering.

Schema Governance & Schema Registries (Confluent Avro / Protobuf)

In large-scale Pub/Sub architectures with dozens of microservices, publishers continuously evolve event schemas. If a publisher renames an attribute or changes a data type without warning, downstream consumer workers throw runtime parsing exceptions and crash. Systems deploy a centralized **Schema Registry**:

Zero-Copy Log Streaming Mechanics (Kafka OS Kernel Optimization)

Log-based Pub/Sub brokers (such as Kafka) achieve multi-gigabit throughput per second by exploiting Linux OS **Zero-Copy Data Transfer (`sendfile` system call)**:

Log Compaction for Keyed Event Streams

In topics that represent state snapshots (such as `user-profiles` or `product-prices`), storing every historical mutation log forever consumes massive storage space. Kafka provides **Log Compaction**:

Consumer Group Rebalance Storms

When a new consumer worker node joins or leaves a Kafka consumer group (e.g. during a Kubernetes deployment), Kafka triggers a **Rebalance Protocol**:

Complete Worked Example: Production Go Topic-Based Pub-Sub Broker

Let's inspect a complete Go implementation of a Topic-Based Pub-Sub Broker for the MessageLab platform (messagelab.com).

package main

import (
"context"
"fmt"
"sync"
)

type Event struct {
Topic string
Key string
Payload string
}

type PubSubBroker struct {
mu sync.RWMutex
subscribers map[string][]chan Event
}

func NewPubSubBroker() *PubSubBroker {
return &PubSubBroker{
subscribers: make(map[string][]chan Event),
}
}

func (b *PubSubBroker) Subscribe(topic string, bufferSize int) <-chan Event {
b.mu.Lock()
defer b.mu.Unlock()

ch := make(chan Event, bufferSize)
b.subscribers[topic] = append(b.subscribers[topic], ch)
fmt.Printf("[SUBSCRIBE] Registered new subscriber for topic '%s'\n", topic)
return ch
}

func (b *PubSubBroker) Publish(event Event) {
b.mu.RLock()
defer b.mu.RUnlock()

subs, exists := b.subscribers[event.Topic]
if !exists || len(subs) == 0 {
fmt.Printf("[PUBLISH] No subscribers registered for topic '%s'. Event discarded.\n", event.Topic)
return
}

fmt.Printf("[PUBLISH FAN-OUT] Broadcasting event (Key: %s) to %d subscribers on topic '%s'...\n",
event.Key, len(subs), event.Topic)

for _, sub := range subs {
select {
case sub <- event:
default:
fmt.Printf("[WARN] Subscriber buffer full on topic '%s'. Dropping event.\n", event.Topic)
}
}
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. Slow Subscriber Consumer BacklogAnalytics service takes $2\text{s}$ per event; topic partition lag explodes to 500,000 events.Analytics dashboard becomes 6 hours stale; disk usage on Kafka broker surges.High Consumer Group Lag metric on Prometheus monitoring.Scale out Consumer Group Worker Instances up to the total partition count.
2. Breaking Schema Evolution CrashPublisher adds a non-nullable JSON field; downstream Python consumer throws parsing exceptions.Downstream consumer workers crash repeatedly on every new event payload.High error rates in downstream microservice logs following deployment.Use Confluent Schema Registry (Avro/Protobuf) enforcing Backward Compatibility.
3. Out-of-Order Event ProcessingPublisher omits partition key; OrderPaid lands on Partition 0 while OrderCreated lands on Partition 1.Analytics service receives OrderPaid before OrderCreated, throwing foreign key exceptions.Missing key error logs in consumer microservices.Always supply a Deterministic Partition Key (user_id, order_id).
4. Un-Acknowledged Offset LoopConsumer crashes mid-batch; upon restart, it re-reads 1,000 events from the last committed offset.Duplicate processing of 1,000 order receipt emails to customers.High duplicate execution rate alerts on consumer worker pools.Implement Idempotent Consumers and commit offsets in small batches.

What You Should Remember

  1. Pub/Sub enables 1-to-Many Fan-Out: Decouple event producers from interested consumers; add new downstream services without altering publisher code.
  2. Message ordering is guaranteed per partition: Supply a deterministic partition key (user_id) to direct related events to the same partition.
  3. Consumer Groups scale reading in parallel: Divide partitions among consumer group worker instances; max parallel workers equals total partition count.
  4. Enforce Backward Schema Compatibility: Use Schema Registries (Avro or Protobuf) to prevent new event fields from crashing downstream consumers.
  5. Combine SNS + SQS for isolated service queues: Forward topic events into dedicated SQS queues per service to prevent slow consumers from affecting peers.

Glossary of Terms

TermDefinition
Publish/Subscribe (Pub/Sub)A messaging pattern where publishers emit events to topics without targeting specific subscribers.
Fan-OutThe architectural routing pattern delivering a single published event copy to multiple independent subscriptions.
TopicA named logical channel or log stream to which events are published.
PartitionA contiguous, append-only log file subset within a topic used to scale storage and throughput.
Consumer GroupA collection of worker instances sharing the processing of topic partitions in parallel.
Schema RegistryA centralized service enforcing backward and forward compatibility rules for event payloads.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing the event streaming backbone for a ride-sharing platform (`rides.messagelab.com`): **Questions**:
  1. Formulate the topic partitioning strategy, partition key selection, and consumer group design.
  2. Detail how your schema registry policy prevents breaking changes when the Billing team updates payment payload fields.

Interactive Self-Assessment

A partition is a single append-only disk log file; multiple partitions process in parallel across physical nodes, preserving sequential order only within individual partitions.

Kafka partitioning automatically formats persistent NVMe SSD disk drives across all broker nodes.

Kafka partitioning revokes client HTTPS TLS encryption certificates on load balancers.

Kafka partitioning cuts physical CPU hardware clock speeds in half across all consumer nodes.

Each subscribing service gets a dedicated queue, isolating slow or failing consumers from affecting peer services.

SNS + SQS Fan-Out automatically converts relational database primary key indexes into un-indexed CSV files.

SNS + SQS Fan-Out replaces public DNS nameservers with local hosts file entries.

SNS + SQS Fan-Out reboots operating system hypervisors across all worker nodes.


What to Learn Next

Track: Data, Storage and Messaging

Previous: Event-Driven Architecture — React to Facts, Don’t Chain Calls

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab