Hirestack
Hirestack
Chapter 16

Problem 12: Design a Pub-Sub System

📖 2 min read · 6 sections · May 2026
Asked at: Amazon LinkedIn Confluent

The setup

"Design a publish-subscribe message broker. Publishers write to topics; subscribers receive messages. Multiple subscribers per topic; deliver to each. Support delivery guarantees (at-least-once)."

Clarifying questions

QuestionStory answer
In-process or distributed?In-process for v1; mention distributed as extension.
Delivery guarantee?At-least-once. Subscribers must dedupe if at-most-once needed.
Ordering — per topic, per partition?Per topic, in publish order.
Persistence — survive restart?Optional; messages persisted to disk if configured.

Core entities

class Topic {
  String name;
  List subscribers;
  Queue messageBuffer;  // for in-flight ordering
}

interface Subscriber {
  void onMessage(Message msg);
}

class Subscription {
  Subscriber subscriber;
  Topic topic;
  long lastProcessedOffset;
}

class Message {
  long id;
  String topicName;
  byte[] payload;
  Instant timestamp;
}

class Broker {
  Map topics;
  void publish(String topicName, byte[] payload);
  Subscription subscribe(String topicName, Subscriber subscriber);
}

Design Decision 1: synchronous vs async delivery

When a publisher publishes, do we synchronously notify all subscribers (blocking the publisher) or async (return quickly, deliver in background)?

Async is the right answer for scale. Each subscriber gets its own delivery thread/queue; slow subscribers don't block fast ones or the publisher.

class Broker {
  private final Map topics = new ConcurrentHashMap<>();
  private final ExecutorService deliveryExecutor = Executors.newFixedThreadPool(20);
  
  public void publish(String topicName, byte[] payload) {
    Topic topic = topics.get(topicName);
    if (topic == null) return;
    Message msg = new Message(generateId(), topicName, payload, Instant.now());
    // Async delivery to each subscriber
    for (Subscription sub : topic.subscribers) {
      deliveryExecutor.submit(() -> deliverWithRetry(sub, msg));
    }
  }
  
  private void deliverWithRetry(Subscription sub, Message msg) {
    int attempts = 0;
    while (attempts < MAX_RETRIES) {
      try {
        sub.subscriber.onMessage(msg);
        sub.lastProcessedOffset = msg.id;
        return;
      } catch (Exception e) {
        attempts++;
        Thread.sleep(backoff(attempts));
      }
    }
    // Dead letter
    deadLetterQueue.add(msg);
  }
}

Cross-examination: ordering with concurrency

Interviewer: Your async delivery uses a thread pool. Messages for the same subscriber can be delivered out of order. How to maintain ordering?
Per-subscriber single-threaded delivery. Each subscription has its own thread or single-thread executor. Messages are queued and processed in order. Across subscriptions, parallel.
class Subscription {
  ExecutorService deliveryExecutor = Executors.newSingleThreadExecutor();
  // Each subscription has its own thread
}
Trade: more threads system-wide. For 10K subscribers, 10K threads is too many. Use a smaller pool with subscriber affinity (consistent hashing on subscriber ID → fixed thread).

Takeaway

Pub-sub is the Observer pattern at production scale. The complications (ordering, delivery guarantees, retries, dead-letter, backpressure) are what separate a toy from a real system. Real brokers like Kafka and RabbitMQ implement all of these with significant engineering investment.