SweatyImposterPub/Sub Event System

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

BrokerTop-level registry of Topics and Subscriptions; the entry point for publish and subscribe.
TopicNamed channel that owns its message log and the set of Subscriptions reading from it.
MessageImmutable record with payload, headers, offset, and timestamp.
SubscriptionPer-subscriber state on a Topic: handler reference, current offset, ack state.
Subscriber (interface)Consumer callback contract; onMessage returns success/failure to drive ack/retry.
MessageLogPer-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