Pub/Sub Event System
Design an in-process pub/sub system: publishers post messages to topics, and subscribers receive messages from topics they subscribe to. Support multiple subscribers per topic, retention, and delivery guarantees.
Clarifying questions
- In-process only, or do we need to span machines (Kafka-like)?
- At-most-once, at-least-once, or exactly-once delivery?
- Push to subscribers or pull (consumer offsets)?
- Retention by time, by size, or until-acked?
- Ordering guarantee per topic, per key, or none?
Core requirements
- Create and delete topics dynamically.
- Publish messages to a topic with a payload and metadata.
- Subscribe and unsubscribe handlers per topic.
- Retain messages per policy so late subscribers can read history (if configured).
- Deliver messages with the agreed semantics and track per-subscriber progress.
- Handle slow subscribers without blocking publishers.
Canonical class breakdown
Broker — Top-level registry of Topics and Subscriptions; the entry point for publish and subscribe.
Topic — Named channel that owns its message log and the set of Subscriptions reading from it.
Message — Immutable record with payload, headers, offset, and timestamp.
Subscription — Per-subscriber state on a Topic: handler reference, current offset, ack state.
Subscriber (interface) — Consumer callback contract; onMessage returns success/failure to drive ack/retry.
MessageLog — Per-Topic append-only buffer with retention policy enforcement and offset-based reads.
RetentionPolicy (interface) — Decides when a message can be evicted — by time, size, or all-subscribers-acked.
DeliveryStrategy (interface) — Encapsulates push vs pull and at-most-once vs at-least-once semantics.
Java sketchjava
// Java skeleton
class Message {
private final long offset;
private final byte[] payload;
private final Map<String, String> headers;
private final Instant timestamp;
}
interface Subscriber {
boolean onMessage(Message m); // success drives ack; failure drives retry
}
interface RetentionPolicy {
boolean shouldEvict(Message m, Topic topic, Instant now);
}
interface DeliveryStrategy {
void deliver(Subscription sub, Message m); // encodes push/pull, at-most/least-once
}
class MessageLog {
private final Deque<Message> buffer; // append-only
private long nextOffset;
private final RetentionPolicy retention;
public long append(byte[] payload, Map<String, String> headers) { ... }
public List<Message> readFrom(long offset, int max) { ... }
public void evictExpired() { ... }
}
class Subscription {
private final String subscriptionId;
private final Topic topic;
private final Subscriber subscriber;
private long currentOffset;
private final BlockingQueue<Message> backlog; // bounded
public void ack(long offset) { ... }
public void enqueue(Message m) { /* overflow policy if full */ }
}
class Topic {
private final String name;
private final MessageLog log;
private final Map<String, Subscription> subscriptions;
private final DeliveryStrategy delivery;
public long publish(byte[] payload, Map<String, String> headers) { ... }
public Subscription subscribe(String subId, Subscriber s) { ... }
public void unsubscribe(String subId) { ... }
}
class Broker {
private final Map<String, Topic> topics;
public Topic createTopic(String name, RetentionPolicy r, DeliveryStrategy d) { ... }
public void deleteTopic(String name) { ... }
public long publish(String topicName, byte[] payload, Map<String, String> headers) { ... }
public Subscription subscribe(String topicName, String subId, Subscriber s) { ... }
}
Key decisions to defend
- Per-subscriber offset (not per-message ack list) — this is what makes pub/sub scale and matches every real system.
- MessageLog is append-only; retention is the eviction policy, not a delete operation publishers see.
- RetentionPolicy and DeliveryStrategy are both Strategy interfaces because every team wants different guarantees.
- Slow subscribers cannot block publishers — bound the per-subscription queue and define an overflow policy explicitly.
- Topics are first-class objects, not just string keys, so per-topic configuration (retention, ordering) has a home.
Likely follow-ups
- · Scale beyond one machine — how do you partition a topic?
- · Add exactly-once — what does the subscriber contract have to look like?
- · A subscriber crashes — how does it resume without losing or duplicating?
Dive deeper
Pub/sub forces you to think about decoupling producers from consumers in both time and space. The fundamental insight is per-subscriber offsets over a shared append-only log — every modern system (Kafka, Pulsar, Kinesis) converges here. Candidates who model it as 'fan out to a list of callbacks' miss retention, replay, and slow-subscriber backpressure entirely. Interviewers also push on delivery semantics because that's where the real design tradeoffs are.