Publish-Subscribe System
A message broker that lets publishers and subscribers never know about each other - only about a topic between them. It's the Observer pattern with a name change and a broker doing the introducing.
Requirements
Functional
- A publisher publishes a message to a named topic.
- A subscriber subscribes to a topic and receives every message published to it after it subscribed.
- A subscriber can unsubscribe from a topic.
- A topic can have many subscribers; a subscriber can be subscribed to many topics.
Non-functional
- Delivery is at-least-once: a subscriber may see the same message twice on a retry, but must never silently miss one because a callback threw.
- A slow or failing subscriber must not block delivery to the other subscribers of the same topic.
Design
Broker is the only thing publishers and subscribers ever talk to. A Topic holds its own
subscriber list and does its own fan-out; the broker's job is just routing a publish to the
right topic and creating topics on first use.
- 1A subscriber registers interest in a topic by name - it never talks to a Topic object directly.
- 2The broker creates the topic on first reference, for either a publisher or a subscriber.
- 3The publisher only knows a topic name and a message - not who, or how many, are listening.
- 4The broker forwards the publish to the matching topic and steps out of the way.
- 5The topic calls every subscriber in turn; one subscriber throwing does not stop the rest.
At-least-once delivery means Topic.publish treats one subscriber's exception as that
subscriber's problem - it catches, logs, and keeps delivering to the rest, rather than
letting one bad callback take down the whole fan-out.
Class diagram
implementsuses
Code
Design decisions
Subscriberis a callback interface, not a class the broker inspects. The broker never needs to know what a subscriber does with a message, only that it can be handed one. That's the whole reason this looks like Observer: the subject (Topic) is decoupled from every observer's implementation.- Topics are created lazily by the broker, not pre-registered. A publisher or
subscriber referencing a topic name that doesn't exist yet shouldn't be an error case to
special-case at every call site -
getOrCreateTopicmakes "topic doesn't exist" and "topic exists" the same code path. - Fan-out failure isolation lives inside
Topic.publish, not in the broker. Each subscriber call is wrapped individually so one throwing subscriber can't stop the loop before the rest have been notified - that's what "at-least-once, not at-most-once" means in code. - What's missing for a real system: this broker delivers in-process and synchronously; a production version would persist messages to a durable log before acknowledging the publisher, and would give each subscriber its own offset/cursor so a restarted subscriber resumes instead of missing everything published while it was down.