Skip to content

Pub/Sub Pattern#

Problem statement (interviewer prompt)

Design a messaging substrate that decouples producers from consumers. It should support topic-based fan-out, per-key ordering, at-least-once delivery (with optional exactly-once), bounded retention, and consumer groups for parallel processing.

flowchart LR
  P1[Publisher A]
  P2[Publisher B]
  B((Broker / Topic))
  S1[Subscriber 1]
  S2[Subscriber 2]
  S3[Subscriber 3]
  P1 --> B
  P2 --> B
  B --> S1
  B --> S2
  B --> S3

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class P1,P2,S1,S2,S3 service;
    class B queue;

Publishers don't know subscribers; the broker fan-outs messages to all (or to a chosen group).

flowchart TB
  subgraph Producers
    P1[Service A]
    P2[Service B]
    P3([CDC / Debezium])
  end

  subgraph Broker[Broker Cluster]
    direction TB
    subgraph Topic_orders[Topic: orders]
      P0[Partition 0]
      P1p[Partition 1]
      P2p[Partition 2]
      P3p[Partition 3]
    end
    META[[Controller / Metadata<br/>Kafka KRaft, Zookeeper]]
    REPL[Replication ISR<br/>RF=3]
  end

  subgraph Storage
    LOG[Append-only log /<br/>segment files]
    IDX[Index per partition]
    TIER[Tiered storage S3<br/>cold segments]
  end

  subgraph Consumers
    subgraph CG_billing[Consumer Group: billing]
      CB1[c1 partitions 0,1]
      CB2[c2 partitions 2,3]
    end
    subgraph CG_analytics[Consumer Group: analytics]
      CA1[c1 partitions 0-3]
    end
    DLQ[(Dead Letter Topic)]
  end

  subgraph Semantics
    AL[At-Least-Once]
    AM[At-Most-Once]
    EO[Exactly-Once<br/>idempotent + tx]
    ORD[Per-partition Order]
    RT[Retention by time/size]
  end

  P1 --> Topic_orders
  P2 --> Topic_orders
  P3 --> Topic_orders
  Topic_orders --> LOG
  LOG --> IDX
  LOG -. age out .-> TIER
  META -. assign leaders .-> Topic_orders
  REPL --- Topic_orders
  Topic_orders --> CG_billing
  Topic_orders --> CG_analytics
  CG_billing -. poison msg .-> DLQ

    classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
    classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
    classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
    classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
    classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
    classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
    classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
    classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
    classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
    classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
    class P1,P2,P0,P1p,P2p,P3p,REPL,LOG,IDX,CB1,CB2,CA1,AL,AM,EO,ORD,RT service;
    class DLQ datastore;
    class META queue;
    class P3 compute;
    class TIER storage;

Routing models#

  • Topic (Kafka, NATS, Pub/Sub): publisher writes once, multiple consumer groups read independently.
  • Queue / Work (SQS, RabbitMQ direct): each message goes to exactly one consumer of a group.
  • Topic + filters (NATS subjects, SNS attributes): selective subscribe.

Delivery semantics#

  • At-most-once: fire and forget.
  • At-least-once: ack after processing; duplicates possible - make consumers idempotent.
  • Exactly-once: transactional producer + idempotent consumer + read-process-write tx (Kafka EOS).

Ordering#

  • Per-partition / per-key only. To preserve order, use stable partition key (e.g., user_id).

Backpressure & DLQ#

  • Pull (Kafka) vs push (RabbitMQ); pull naturally backpressures.
  • Retries with exponential backoff; after N failures → DLQ.

Glossary & fundamentals#

Concepts referenced in this design. Each row links to its canonical page; the tag column shows whether it is a high-level (HLD) or low-level (LLD) concept.

Tag Concept What it is Page
HLD Pub/Sub & message brokers topics, consumer groups, delivery semantics pub-sub-pattern
HLD Leader/follower replication sync/semi-sync/async replication, failover replication-leader-follower
HLD Idempotency & retries safe re-execution, backoff + jitter idempotency-retries
HLD Resilience patterns timeout, retry, breaker, bulkhead, backpressure resilience-patterns
HLD Change Data Capture WAL/binlog tailing, outbox publishing change-data-capture

Quick reference#

Why use it#

  • Decouple producers from consumers.
  • Buffer bursts; consumers process at their own pace.
  • Fan-out: one event drives many side-effects (search index, cache invalidation, analytics).

Broker comparison (rough)#

Kafka RabbitMQ SQS Pub/Sub (GCP) NATS JetStream
Model log, partitioned exchange + queue queue log-like log
Order per-partition per-queue best effort (FIFO opt) per-key per-subject
Retention days-months until consumed 4 days max 7 days configurable
Throughput very high medium high very high very high
Use case event streaming task queue task queue event streaming edge messaging

Patterns#

  • Outbox pattern: write DB row + outbox event in same tx; relay to broker.
  • Saga: long-running tx via events + compensations.
  • Event sourcing: state = replay of events.

Pitfalls#

  • Schema evolution: use Avro/Protobuf + schema registry, enforce compatibility.
  • Hot partitions: bad key choice → uneven load.
  • Consumer lag: alert on it.

Refs#

  • Kafka design doc (LinkedIn), Confluent EOS post, RabbitMQ in Depth (book), AWS SQS/SNS architecture, NATS JetStream docs.

FAQ#

What is the pub/sub pattern?#

Publishers send messages to a broker topic without knowing the subscribers. Subscribers receive every message they care about, decoupling producers from consumers.

Pub/sub vs message queue?#

A queue delivers each message to exactly one consumer. Pub/sub fans the same message out to many subscribers, each on its own offset, ideal for event-driven systems.

What is a consumer group?#

A consumer group splits a topic's partitions across its members. Each partition goes to one member at a time, giving parallel processing with per-partition ordering.

How do you get exactly-once delivery?#

Combine an idempotent consumer (inbox table or dedup key) with at-least-once delivery. Brokers like Kafka also offer transactional writes for end-to-end exactly-once.

Which pub/sub system should I pick?#

Pick Kafka or Pulsar for durable, partitioned logs at scale. Pick SNS, Google Pub/Sub, or NATS for simpler hosted fan-out. Pick Redis Streams for low-latency, small footprints.

  • Event Sourcing and CQRS: event sourcing relies on pub/sub as the transport for distributing domain events to subscribers
  • Batch and Stream Processing: pub/sub topics feed real-time stream processing pipelines for immediate event handling
  • Change Data Capture: CDC pipelines publish database change events to pub/sub topics for downstream consumers

Further reading#

Curated, high-credibility sources for going deeper on this topic.