Pub/Sub Library (LLD)#
Problem statement (interviewer prompt)
Design an in-process publish/subscribe library: publishers post messages to topics; subscribers receive them, possibly with filters. Cover thread safety, async vs sync dispatch, error isolation, and bounded backlog per subscriber.
classDiagram
class Broker {
-topics : Map~String, Topic~
+publish(topicName, message)
+subscribe(topicName, subscriber, filter)
+unsubscribe(topicName, subscriberId)
}
class Topic {
-name : String
-subscribers : List~Subscription~
-dispatcher : Executor
+deliver(message)
}
class Subscription {
+id : String
+subscriber : Subscriber
+filter : Predicate~Message~
+queue : BlockingQueue~Message~
}
class Subscriber {
<<interface>>
+onMessage(m)
+onError(e)
}
class Message {
+id, topic, payload, headers, timestamp
}
Broker *-- "many" Topic
Topic *-- "many" Subscription
Subscription --> Subscriber
Async dispatch with bounded per-subscription queue gives backpressure. Filter predicate runs in the dispatcher thread to avoid waking idle subscribers.
A core LLD interview shape: many subscribers, many publishers, asynchronous coordination, failure isolation.
Class diagram#
classDiagram
class Broker {
-topics : ConcurrentMap~String, Topic~
-executor : Executor
+publish(name, msg)
+subscribe(name, sub, filter, qSize) Subscription
+unsubscribe(s : Subscription)
}
class Topic {
-name
-subs : List~Subscription~
+deliver(msg)
}
class Subscription {
+id
+subscriber : Subscriber
+filter : Predicate
-queue : BlockingQueue
-worker : Thread
+deliver(msg)
+close()
}
class Subscriber {
<<interface>>
+onMessage(msg)
+onError(e)
}
class Message {
+id, topic, payload, headers, ts
}
Broker *-- Topic
Topic *-- Subscription
Subscription --> Subscriber
Topic ..> Message
Publish path#
class Topic {
void deliver(Message m) {
for (Subscription s : subs) { // CopyOnWriteArrayList or read-lock
if (s.filter.test(m)) {
boolean accepted = s.queue.offer(m); // bounded; non-blocking
if (!accepted) {
metrics.dropped(s); // or call s.onOverflow()
}
}
}
}
}
Per-subscription worker#
class Subscription implements Runnable {
void run() {
while (!closed) {
try {
Message m = queue.take();
subscriber.onMessage(m);
} catch (Throwable t) {
subscriber.onError(t); // never let exception kill worker
}
}
}
}
Each subscriber gets its own thread (or pulls from a shared executor). One slow subscriber doesn't block others.
Backpressure#
| Option | Behaviour |
|---|---|
| Drop newest | Bounded queue with offer() |
| Drop oldest | evict + offer |
| Block publisher | Use put() instead of offer() (couples publisher to slowest sub) |
| Slow-consumer disconnect | If queue full > N seconds, evict subscription |
Filter pushdown#
Run filter in dispatcher to avoid handing the message to a worker that will discard it.
Sync vs async#
| Mode | Use |
|---|---|
| Async | Default; no caller blocked on subscriber speed |
Sync (publishSync) |
Test fixtures; ordering guarantees within a single thread |
Replay / late subscribers#
For "give me last N messages":
- Topic keeps a ring buffer of recent messages.
- New subscription with
replayLast=Ndrains the buffer first, then live.
Beyond a small buffer, use a real log (Kafka).
Error isolation#
- Subscriber exception →
onErrorcallback; subscription survives. - Worker thread
uncaughtExceptionHandlerto log. - Repeated errors trigger
breaker.open(); pause delivery.
Memory and lifecycle#
subscribereturns aSubscriptiontoken; caller MUST hold it or unsubscribe explicitly to avoid leaks.close()drains queue, joins worker.
Production analogs#
| Library | Scope |
|---|---|
| Guava EventBus | In-process, simple |
| RxJava / Reactor | In-process with backpressure |
| Akka actors | Per-actor mailbox |
| Spring ApplicationEvent | In-process, app-framework |
For distributed pub/sub see the pub-sub-pattern.
Where pub/sub LLD fits#
flowchart TB
PS((Pub/Sub<br/>LLD))
PSH["Pub/Sub pattern (HLD)<br/>distributed analog"]
BPL[Behavioral patterns<br/>Observer]
CON[Concurrency primitives<br/>queue + executor]
BP[Backpressure<br/>bounded queues]
PS -. scaled .-> PSH
BPL --> PS
CON --> PS
BP --> PS
classDef client fill:#dbeafe,stroke:#1e40af,stroke-width:1px,color:#0f172a;
classDef edge fill:#cffafe,stroke:#0e7490,stroke-width:1px,color:#0f172a;
classDef service fill:#fef3c7,stroke:#92400e,stroke-width:1px,color:#0f172a;
classDef datastore fill:#fee2e2,stroke:#991b1b,stroke-width:1px,color:#0f172a;
classDef cache fill:#fed7aa,stroke:#9a3412,stroke-width:1px,color:#0f172a;
classDef queue fill:#ede9fe,stroke:#5b21b6,stroke-width:1px,color:#0f172a;
classDef compute fill:#d1fae5,stroke:#065f46,stroke-width:1px,color:#0f172a;
classDef storage fill:#e5e7eb,stroke:#374151,stroke-width:1px,color:#0f172a;
classDef external fill:#fce7f3,stroke:#9d174d,stroke-width:1px,color:#0f172a;
classDef obs fill:#f3e8ff,stroke:#6b21a8,stroke-width:1px,color:#0f172a;
class PS service;
class PSH,BPL,CON,BP datastore;
Glossary & fundamentals#
| Tag | Concept | What it is | Page |
|---|---|---|---|
HLD |
Pub Sub Pattern | the distributed analog | pub-sub-pattern |
LLD |
Behavioral patterns | Observer | behavioral-patterns |
LLD |
Concurrency primitives | BlockingQueue, executor | concurrency-primitives |
HLD |
Backpressure | the flow-control story | backpressure |
Quick reference#
Classes#
- Broker (topic registry)
- Topic (subscription list, deliver())
- Subscription (filter, bounded queue, worker)
- Subscriber (onMessage, onError)
- Message (id, topic, payload, headers, ts)
Dispatch#
- Filter in dispatcher (avoid wasted wakeups)
offer()to bounded queue- Per-subscription worker thread
Backpressure modes#
- Drop newest (default)
- Drop oldest
- Block publisher (couples to slowest sub)
- Disconnect slow consumer
Error isolation#
- Subscriber exception → onError; subscription survives
- Worker uncaughtExceptionHandler
- Circuit breaker on repeated failures
Replay#
Topic ring buffer; new sub drains buffer then live. Beyond small N, use Kafka.
Sync vs async#
- Async default
- Sync mode for tests / ordering
Production analogs#
- Guava EventBus
- Reactor Sinks
- Akka actors
- Spring ApplicationEvent
Refs#
- Guava EventBus docs
- Reactor Sinks docs
FAQ#
How do you design an in-process pub/sub broker?#
A Broker holds Topic objects keyed by name; publishers post messages, and the broker fans them out to registered Subscriber callbacks, optionally with per-subscriber filters.
Should dispatch be synchronous or asynchronous?#
Async via per-subscriber bounded queues so a slow subscriber cannot block publishers. Sync dispatch is simpler but couples publisher latency to the slowest consumer.
How is back-pressure handled?#
Each subscriber has a bounded queue. When full, choose a policy: drop oldest, drop newest, block the publisher, or surface an overflow event for telemetry.
How are subscriber errors isolated?#
Each callback runs in a try/catch on its own worker thread or task, so an exception in one subscriber never blocks delivery to other subscribers on the same topic.
What is the difference between pub/sub and Observer?#
Observer is in-object and synchronous. Pub/Sub adds named topics, fan-out across many subjects, optional async dispatch, and decouples publishers from concrete subscriber types.
Related Topics#
- Pub Sub Pattern: the distributed-broker version
- Behavioral Patterns: Observer is the underlying pattern
- Backpressure: bounded queues are the in-process backpressure primitive