Problem 12: Design a Pub-Sub System
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
| Question | Story 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
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.