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.

All OOD prompts