Home › High-Level Design › Publish-Subscribe
P

Publish-Subscribe

If you cannot explain pub-sub cleanly, you will stumble on half the system design questions out there. Notifications, activity feeds, order pipelines, analytics ingestion: they all lean on this pattern. I have watched strong coders freeze when asked "okay, but what happens if a consumer is down for an hour?" Let me make sure that is not you.

High-Level DesignIntermediate11 min read

If you remember five things

Say these out loud and you sound like you have operated the system, not just drawn boxes.

  1. Decoupling is the product. The publisher does not know or care who consumes, so you can add, remove, or slow down consumers without touching the producer.
  2. The broker is a shock absorber. Spikes land in durable storage and drain into consumers at their own pace, turning a hard failure into a soft, recoverable delay.
  3. Delivery is at-least-once, not exactly-once. Consumers will see duplicates, so they must be idempotent. This is the single most tested point.
  4. Ordering is per-partition only. Key by something stable (like order_id) so all events for one entity stay ordered, and accept that cross-key order is not guaranteed.
  5. Two broker flavors, opposite guarantees. Log-based replays for offline consumers; fire-and-forget drops anything a disconnected subscriber missed. Pick by what losing a message costs.

1The core idea#

Pub-sub is how parts of a system talk without knowing about each other.

  • A publisher sends a message.
  • One or more subscribers receive it.
  • Neither side holds a direct reference to the other.
  • A broker (also called a message bus or event bus) sits in between and routes each message to whoever asked to hear about that kind of message.

The mental model I use: a bulletin board, not a phone call.

  • Phone call (request-response, the pattern most APIs use): you dial a specific number and wait for that specific person to pick up.
  • Bulletin board (pub-sub): you pin a note under a topic like "orders" and walk away.
  • Anyone who cares about "orders" reads it on their own schedule.
  • You have no idea who read it, when, or even how many readers there are.

That one property is the whole point. The publisher not knowing or caring who consumes is called decoupling, and it is what you are really buying when you reach for pub-sub.

Two words you will hear thrown around:

  • Topic: a named channel. Publishers write to it, subscribers listen to it. Think payments.completed or user.signup.
  • Fan-out: one message landing in many subscribers. Publish an order.placed event once, and the inventory service, the email service, and the analytics pipeline all get their own copy.
The sentence that earns the point

Decoupling is not a free win, it is a trade. You buy independent scaling and spike absorption, and you pay with eventual consistency and harder debugging. Name both sides and the interviewer knows you have shipped this, not read about it.

2How it works#

Here is the flow when a user places an order and three downstream services need to react.

flowchart LR A[Order Service] --> B[Broker] B --> C[Inventory Service] B --> D[Email Service] B --> E[Analytics Service] B --> F[(Message Log)]
One publish, three independent reads. Notice the Message Log hanging off the broker: that durable store is what lets a slow or crashed subscriber catch up later instead of losing the event.

The write path is short on purpose.

  • The order service publishes one order.placed message to the broker.
  • The broker stores it durably in a log, then delivers a copy to each subscriber.
  • The order service is done the moment the broker acknowledges the write.
  • It does not wait for inventory to decrement or for the email to send.
  • Checkout stays fast even if the email provider is slow. That is the decoupling paying off.

Now the part people skip over: delivery over time. Subscribers are rarely all healthy at the same instant.

sequenceDiagram participant P as Publisher participant B as Broker participant S as Subscriber P->>B: publish order placed B-->>P: ack stored B->>S: deliver message S->>S: process S->>B: ack processed Note over B: if no ack retry later
The ack is the whole game. The broker keeps the message until the subscriber confirms it processed cleanly, so a mid-processing crash means redelivery, not data loss. This is also exactly why delivery is at-least-once, not exactly-once.

Why this gives you at-least-once.

  • The broker holds the message until the subscriber acks "processed cleanly."
  • If the subscriber crashes mid-processing and never acks, the broker redelivers.
  • So the same message can arrive twice. Plan for it (more on that pain below).

Two broad flavors of broker, and knowing the difference is interview gold:

  • Log-based (Kafka, AWS Kinesis, Google Pub/Sub, RabbitMQ with durable queues). Messages are stored on disk and retained for hours or days. A slow or offline consumer catches up later by reading from where it left off. Kafka tracks this position as an offset.
  • Fire-and-forget (Redis Pub/Sub, raw WebSocket fan-out). The broker pushes to whoever is connected right now. An offline subscriber misses the message entirely. No replay. Fast and simple, but lossy.
From the interviewer's chair

When I say "pub-sub," the first thing I want to hear in your head is "which flavor." A candidate who picks log-based for payments and fire-and-forget for a typing indicator, and says why, has already shown me the judgment the rest of the question is testing for.

3A worked example with real numbers#

Let me make this concrete instead of hand-wavy. Say we run a mid-size e-commerce site during a flash sale.

The setup.

  • Baseline traffic: 500 orders/sec.
  • Peak: the sale drives 50,000 orders/sec for a 3-minute burst.
  • Each order.placed event is roughly 1 KB of JSON.
  • Four services subscribe: inventory, payments reconciliation, warehouse routing, and the confirmation email.

What the broker absorbs during that 3-minute burst (write side).

  • Incoming write volume: 50,000 msgs/sec x 1 KB = 50 MB/sec into the broker.
  • Over 180 seconds that is 9 million messages and about 9 GB on disk before replication.
  • Kafka default replication factor of 3 means roughly 27 GB of disk churn across the cluster for the burst.
  • A single modern broker node handles 50 MB/sec of writes without breaking a sweat, so even a 3-node cluster has huge headroom.

The fan-out side is what actually sizes the cluster.

  • Four subscriber groups each read the full stream.
  • Read volume is 4 x 50 MB/sec = 200 MB/sec out of the broker at peak.
  • That read number, not the write number, is what you plan capacity around.

Here is the key move: the email service is the slow consumer.

  • It can only send 2,000 emails/sec through our provider.
  • During the burst it receives at 50,000/sec but drains at 2,000/sec.
  • Without a broker, checkout would block on that 2,000/sec ceiling and 96 percent of orders would time out.
  • With the broker, checkout still returns in under 200 ms because it only waits on the 50 MB/sec write, and the email backlog simply grows.
Pause before the reveal: the email service falls behind by 48,000 msgs/sec for 180 seconds. After the burst, traffic drops back to 500/sec. How long until confirmation emails are caught up?
Reveal the answer
Backlog built up: 48,000/sec x 180 sec = 8.64 million queued messages. After the burst, email drains at its spare capacity of 1,500/sec (the 2,000 cap minus 500/sec steady-state). 8.64M divided by 1,500/sec is about 5,760 seconds, roughly 96 minutes. So a customer might get their confirmation email an hour and a half late, but no order is lost and checkout never slows down.
~96 min
The number that proves the pattern worksWorst-case email delay after the flash sale. The broker converted a hard failure (96 percent of orders dropped) into a soft, recoverable delay (emails arrive late). If 96 minutes is unacceptable, you now have a concrete number to negotiate with, and the fix is obvious: raise the email provider cap or add parallel consumers.

The broker's job is to turn a hard failure into a soft, recoverable delay.

4Trade-offs#

The honest pitch is decoupling and absorbing spikes. The honest cost is that you turned one synchronous call into a distributed system with its own failure modes.

What you gain:

  • Independent scaling. The analytics service can be slow and backed up without touching checkout latency.
  • Spike absorption. During the flash sale, 50,000 orders/sec land in the broker and drain into consumers at whatever rate they handle. The broker becomes a shock absorber.
  • Easy extension. Want to add a fraud-check service next quarter? It subscribes to order.placed. Zero changes to the order service.

What you give up:

  • Simple debugging. With a direct API call, a stack trace tells the whole story. With pub-sub, a message left service A and something went wrong "somewhere." You now need correlation IDs and tracing to follow one event across five services.
  • Instant consistency. The subscriber reacts milliseconds to seconds later, so your system is now eventually consistent. If a user expects to see their new post immediately but the feed service has not consumed the event yet, you have a product problem to design around.
  • Exactly-once as a fantasy. You will get duplicates. Plan for it.

Choosing between the two broker styles comes down to this table:

Dimension Log-based (Kafka, Pub/Sub) Fire-and-forget (Redis Pub/Sub)
Offline subscriber Catches up from stored offset Misses everything
Retention Hours to days (or forever) None, in-memory only
Ordering Guaranteed within a partition Best effort
Throughput Very high (millions/sec) High but no durability
Latency Low single-digit ms Lowest, microseconds
Operational cost Heavier (clusters, disks) Trivial
Good for Pipelines, event sourcing, critical events Live dashboards, presence, chat typing indicators

My default rule:

  • If losing a message would make someone angry (payments, orders, audit logs), use a log-based broker.
  • If the data is cheap and ephemeral (someone is typing, live viewer count), fire-and-forget is fine and much cheaper to run.

5Where it is used#

Some concrete places I have either built or seen up close:

  • Order processing. An e-commerce checkout publishes order.placed. Inventory, payments reconciliation, warehouse routing, and the confirmation email all subscribe. The checkout API returns in under 200 ms because it only waits on the broker write, not on all four downstream jobs.
  • Activity feeds. When you follow someone and they post, that post event fans out so each follower's feed can be updated. Twitter-style feed systems live and die on fan-out design.
  • Metrics and logging. Services publish events to a topic, and a stream processor reads them to build dashboards, trigger alerts, and load a data warehouse. Analytics being 30 seconds behind is totally acceptable here, which makes pub-sub a natural fit.
  • Cache invalidation. A service updates a product's price, publishes product.updated, and every node holding a cached copy subscribes and evicts its stale entry.
  • Cross-service notifications. A payment clears, and that single event wakes up loyalty points, receipt generation, and a push notification, none of which the payment service should know about.

Reach for pub-sub when:

  • One event has many parties that care about it.
  • The producer and consumer run at different speeds.
  • You want to add consumers later without touching the producer.

Do not reach for it when:

  • It is a single caller needing a single immediate answer (a user clicking "get my balance"). Just make the API call.
  • Forcing async on a request that needs an instant reply only adds latency and complexity.

6Common pitfalls and gotchas#

Treating delivery as exactly-once.

  • It is not. A consumer that processes a message then crashes before acking will see that message again on restart.
  • Fix: make consumers idempotent. Processing the same order.placed twice must not charge the card twice or send two emails.
  • Usual mechanism: a dedup key. Store processed message IDs in a table or cache and skip ones you have already handled.

Ignoring ordering.

  • Kafka only guarantees order within a single partition, not across the whole topic.
  • Route order.placed and order.cancelled for the same order to different partitions and the cancel can arrive first.
  • Fix: key messages by something stable (like order_id) so all events for one order land in the same partition and stay ordered.

No dead-letter plan.

  • Some message will always fail: malformed, or a bug trips on it.
  • If your consumer just retries forever, it blocks the whole partition and nothing moves. This is a poison message.
  • Fix: a dead-letter queue. After N failed attempts, shove the message aside for a human to inspect and keep the pipeline flowing.

Unbounded consumer lag.

  • If producers outrun consumers, the broker backlog grows.
  • In the flash sale, the email service built an 8.64 million message backlog on purpose, which was fine because it drained in 96 minutes and retention was 7 days.
  • The danger is lag that never recovers. On a log-based system you hit retention limits and silently drop old messages, or run out of disk.
  • Fix: always monitor consumer lag (how far behind the latest offset each consumer is). A rising lag graph that never flattens is your early warning that a consumer is permanently underprovisioned.

Fan-out amplification.

  • One celebrity with 10 million followers posts once, and you have generated 10 million feed writes.
  • Naive fan-out-on-write melts under this.
  • Fix: real systems blend strategies. Fan-out on write for normal users, fan-out on read (pull) for the handful of huge accounts.

Schema drift.

  • The producer adds a required field, old consumers break.
  • Fix: use a schema registry, or at least version your events and only make backward-compatible changes (add optional fields, never rename or remove).
Red flag that fails you here

Saying "I will use exactly-once delivery" and moving on. That promises a guarantee the broker cannot give end to end, and it tells me you have never debugged a double-charged customer. The strong move is the opposite: assume at-least-once out loud, then make the consumer idempotent.

7How it shows up in interviews#

Interviewers use pub-sub to test whether you understand asynchronous systems, not whether you can name Kafka. Here is what they probe, with the weak answer and the answer that actually lands.

"Why not just call the service directly?" The question is checking if you know why you are adding a broker.

  • Weak: "Kafka is scalable." (Names a tool, explains nothing, and would have checkout block on a 2,000/sec email cap.)
  • Strong: "The checkout path should not block on the email service, which caps at 2,000 emails/sec. At a 50,000/sec peak, a direct call would time out 96 percent of orders. Decoupling keeps user-facing latency under 200 ms and lets me add consumers later. The cost is eventual consistency and harder debugging, which I handle with tracing and idempotent consumers."

"A consumer was down for an hour. What happens?" This separates people who have run these systems from people who have read about them.

  • Weak: "The messages are lost."
  • Strong: "With a log-based broker, nothing is lost. The consumer resumes from its last committed offset and catches up. If it fell an hour behind at 2,000/sec, that is 7.2 million queued messages, and it drains them using spare capacity once it is back. I would watch consumer lag during recovery and make sure retention is longer than my worst expected downtime, say 7 days."

"How do you prevent processing a message twice?" They want to hear idempotency and at-least-once.

  • Weak: "I will use exactly-once delivery." (Promises a guarantee the broker cannot give end to end.)
  • Strong: "I assume at-least-once, so I make the consumer idempotent. I check a unique message ID against a dedup store before any side effect, or I design the operation to be naturally idempotent, like setting a status to PAID rather than incrementing a counter."

"How do you guarantee ordering?"

  • Weak: "Kafka keeps everything in order." (Wrong, it is per-partition only.)
  • Strong: "I partition by a stable key like order_id so all events for one order stay ordered. I accept that cross-key ordering is not guaranteed, and I question whether I even need global ordering, because I usually do not."

The single best move in these rounds is to state the guarantees out loud: "This gives me at-least-once delivery and per-partition ordering, so my consumers will be idempotent and I will key by order_id." That one sentence, paired with a real number like the 96-minute email catch-up, signals you have actually operated these systems, not just drawn boxes on a whiteboard.

Follow-ups the interviewer will ask (and your answers)
"Why not just call the service directly?"
Because the slow consumer would set the pace for everyone. The email provider caps at 2,000/sec, so a direct call at a 50,000/sec peak times out 96 percent of orders. The broker keeps checkout under 200 ms and lets me add consumers later. I pay for it with eventual consistency and harder debugging, which I cover with tracing and idempotent consumers.
"A consumer was down for an hour. What happens?"
On a log-based broker, nothing is lost. It resumes from its last committed offset and catches up on the backlog using spare capacity. I make sure retention (say 7 days) is longer than my worst expected downtime, and I watch consumer lag during recovery.
"How do you stop a message being processed twice?"
I assume at-least-once, so the consumer is idempotent. I check a unique message ID against a dedup store before any side effect, or I design the operation to be naturally idempotent, like setting status to PAID instead of incrementing a counter.
"How do you guarantee ordering?"
I key by a stable value like order_id so every event for one order lands in the same partition and stays ordered. Cross-key ordering is not guaranteed, and I usually do not need it, so I do not pay for global ordering.
"A message keeps failing and blocks the partition. What do you do?"
That is a poison message. After N retries I route it to a dead-letter queue so a human can inspect it while the pipeline keeps flowing. Retrying forever in place would stall every message behind it in that partition.

What you can now explain in the room

  • Why decoupling (the publisher not knowing its consumers) is the actual value of pub-sub, and what it costs.
  • Why the broker acts as a shock absorber, with the flash-sale math from 96 percent dropped orders to a 96-minute email delay.
  • Why delivery is at-least-once, not exactly-once, and how idempotent consumers handle duplicates.
  • Why ordering is per-partition only, and how keying by order_id keeps one entity's events in order.
  • When to pick a log-based broker versus fire-and-forget, driven by what losing a message costs.
  • How to catch trouble early: monitor consumer lag, plan a dead-letter queue, and version your event schema.

Next up, try the System Design Question walkthrough on building a notification service, where pub-sub fan-out and dead-letter handling are the whole game.

Get a prep plan built around your Publish-Subscribe interview

Tell us the role and your interview date. We will send back a dated, day by day plan. Free.

Thanks. Check your inbox, your Publish-Subscribe prep plan is on the way.