docs: update publish-subscribe

This commit is contained in:
Ilkka Seppälä committed 2025-04-15 20:36:32 +03:00
1 parent 1a08b73af8
commit 9f7ce4e732
7 files changed
+114 -141

No files matched your search

+107 -131
View File
@@ -1,97 +1,101 @@
--- ---
title: "Publish-Subscribe Pattern in Java: Decoupling the solution with asynchronous communication" title: "Publish-Subscribe Pattern in Java: Decoupling components with asynchronous communication"
shortTitle: Publish-Subscribe shortTitle: Publish-Subscribe
description: "Explore the Publish-Subscribe design pattern in Java with detailed examples. Learn how it helps to create loosely coupled, scalable, and flexible systems by allowing components to communicate asynchronously without knowing each other directly." description: "Explore the Publish-Subscribe design pattern in Java with detailed examples. Learn how it helps to create loosely coupled, scalable, and flexible systems by allowing components to communicate asynchronously without knowing each other directly."
category: Behavioral category: Messaging
language: en language: en
tag: tag:
- Architecture
- Asynchronous
- Decoupling - Decoupling
- Event-driven
- Messaging
- Microservices
- Publish/subscribe
- Scalability
--- ---
## Intent of the Publish-Subscribe Design Pattern ## Intent of the Publish-Subscribe Design Pattern
The Publish-Subscribe design pattern is widely used in software architecture to transmit data between various components in a system. Defines a one-to-many dependency between objects, enabling automatic notification of multiple subscribers when a publisher's state changes or an event occurs.
It is a behavioral design pattern aimed at achieving loosely coupled communication between objects.
The primary intent is to allow a one-to-many dependency relationship where one object (the Publisher) notifies multiple other objects (the Subscribers)
about changes or events, without needing to know who or what the subscribers are.
## Detailed Explanation of Publish-Subscribe Pattern with Real-World Examples ## Detailed Explanation of Publish-Subscribe Pattern with Real-World Examples
### Real-world example Real-world example
- Messaging systems like Kafka, RabbitMQ, AWS SNS, JMS > An analogous real-world example of the Publish-Subscribe pattern is a news broadcasting system. A news agency (publisher) broadcasts breaking news stories without knowing who specifically receives them. Subscribers, such as television stations, news websites, or mobile news apps, independently decide which types of news they want to receive (e.g., sports, politics, weather) and are automatically notified whenever relevant events occur. This approach keeps the news agency unaware of subscribers' specifics, allowing flexible and scalable distribution of information.
- **Kafka** : publishes messages to topics and subscribers consumes them in real time for analytics, logs or other purposes.
- **RabbitMQ** : Uses exchanges as publisher and queues as subscribers to route messages
- **AWS SNS** : Simple Notification Service (SNS) received the messages from publishers with topic and the subscribers on that topic will receive the messages. (SQS, Lambda functions, emails, SMS)
In plain words
- Event driven microservices > The Publish-Subscribe design pattern allows senders (publishers) to broadcast messages to multiple receivers (subscribers) without knowing who they are, enabling loose coupling and asynchronous communication in a system.
- **Publisher** : Point of Sale(PoS) system records the sale of an item and publish the event
- **Subscribers** : Inventory management service updates stock, Billing service sends e-bill to customer
Wikipedia says
- Newsletter subscriptions > In software architecture, publish–subscribe or pub/sub is a messaging pattern where publishers categorize messages into classes that are received by subscribers.
- **Publisher** : Writes a new blog post and publish to subscribers This is contrasted to the typical messaging pattern model where publishers send messages directly to subscribers. Similarly, subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers, if any, there are. Publish–subscribe is a sibling of the message queue paradigm, and is typically one part of a larger message-oriented middleware system.
- **Subscribers** : All the subscribers to the newsletter receive the email
### In plain words
The Publish-Subscribe design pattern allows senders (publishers) to broadcast messages to multiple receivers (subscribers) without knowing who they are,
enabling loose coupling and asynchronous communication in a system
### Wikipedia says
In software architecture, publish–subscribe or pub/sub is a messaging pattern where publishers categorize messages into classes that are received by subscribers.
This is contrasted to the typical messaging pattern model where publishers send messages directly to subscribers.
Similarly, subscribers express interest in one or more classes and only receive messages that are of interest, without knowledge of which publishers, if any, there are.
Publish–subscribe is a sibling of the message queue paradigm, and is typically one part of a larger message-oriented middleware system.
Most messaging systems support both the pub/sub and message queue models in their API; e.g., Java Message Service (JMS). Most messaging systems support both the pub/sub and message queue models in their API; e.g., Java Message Service (JMS).
### Architectural Diagram Sequence diagram
![pub-sub](./etc/pub-sub.png)
![Publish-Subscribe sequence diagram](./etc/publish-subscribe-sequence-diagram.png)
## Programmatic Example of Publish-Subscribe Pattern in Java ## Programmatic Example of Publish-Subscribe Pattern in Java
First we need to identify the Event on which we need the pub-sub methods to trigger. First, we identify events that trigger the publisher-subscriber interactions. Common examples include:
For example:
- Sending alerts based on the weather events such as earthquakes, floods and tornadoes * Sending alerts based on weather events, like earthquakes, floods, and tornadoes.
- Sending alerts based on the temperature * Sending notifications based on temperature changes.
- Sending an email to different customer support emails when a support ticket is created. * Sending emails to customer support when support tickets are created.
The Message class below will hold the content of the message we need to pass between the publisher and the subscribers. ### Defining the Message
We start with a simple message class encapsulating the information sent from publishers to subscribers.
```java ```java
public record Message(Object content) { public record Message(Object content) {
} }
``` ```
The Topic class will have the topic **name** based on the event ### Defining Topics
- Weather events TopicName WEATHER A Topic represents an event category that subscribers can register to and publishers can publish messages to. Each topic has:
- Weather events TopicName TEMPERATURE
- Support ticket created TopicName CUSTOMER_SUPPORT
- Any other custom topic depending on use case
- Also, the Topic contains a list of subscribers that will listen to that topic
We can add or remove subscribers from the subscription to the topic * A unique identifier or name (e.g., WEATHER, TEMPERATURE, CUSTOMER_SUPPORT).
* A collection of subscribers listening to this topic.
Subscribers can dynamically subscribe or unsubscribe.
```java ```java
@Getter
@Setter
@RequiredArgsConstructor
public class Topic { public class Topic {
private final TopicName name; private final String topicName;
private final Set<Subscriber> subscribers = new CopyOnWriteArraySet<>(); private final Set<Subscriber> subscribers = new CopyOnWriteArraySet<>();
//...//
public void addSubscriber(Subscriber subscriber) {
subscribers.add(subscriber);
}
public void removeSubscriber(Subscriber subscriber) {
subscribers.remove(subscriber);
}
public void publish(Message message) {
for (Subscriber subscriber : subscribers) {
CompletableFuture.runAsync(() -> subscriber.onMessage(message));
}
}
} }
``` ```
Then we can create the publisher. The publisher class has a set of topics. ### Publisher Implementation
- Each new topic has to be registered in the publisher. The Publisher maintains a collection of topics it can publish to.
- Publish method will publish the _Message_ to the corresponding _Topic_.
* Before publishing, a topic must be registered.
* Upon publishing, it forwards messages to subscribers of the corresponding topic.
```java ```java
public class PublisherImpl implements Publisher { public class PublisherImpl implements Publisher {
@@ -115,20 +119,12 @@ public class PublisherImpl implements Publisher {
} }
``` ```
Finally, we can Subscribers to the Topics we want to listen to. ### Defining Subscribers
- For WEATHER topic we will create _WeatherSubscriber_ Subscribers implement an interface that handles incoming messages.
- _WeatherSubscriber_ can also subscribe to TEMPERATURE topic
- For CUSTOMER_SUPPORT topic we will create _CustomerSupportSubscribe_
- Also to demonstrate the async behavior we will create a _DelayedWeatherSubscriber_ who has a 0.2 sec processing deplay
All classes will have a _onMessage_ method which will take a Message input. * Each subscriber processes messages according to specific logic.
* Subscribers can be registered to multiple topics.
- On message method will verify the content of the message is as expected
- After content is verified it will perform the operation based on the message
- _WeatherSubscriber_ will send a weather or temperature alert based on the _Message_
- _CustomerSupportSubscribe_will send an email based on the _Message_
- _DelayedWeatherSubscriber_ will send a weather alert based on the _Message_ after a delay
```java ```java
public interface Subscriber { public interface Subscriber {
@@ -136,7 +132,21 @@ public interface Subscriber {
} }
``` ```
And here is the invocation of the publisher and subscribers. Subscriber examples:
* WeatherSubscriber: handles alerts for weather events or temperature changes.
* CustomerSupportSubscriber: handles support tickets by sending emails.
* DelayedWeatherSubscriber: simulates delayed processing for demonstrating asynchronous behavior.
### Example Usage (Invocation)
Here's how all components connect:
1. Create Publisher
2. Register Topics with Publisher
3. Create Subscribers and Subscribe to Relevant Topics
4. Publish Messages
5. Manage Subscriptions Dynamically
```java ```java
public static void main(String[] args) throws InterruptedException { public static void main(String[] args) throws InterruptedException {
@@ -205,10 +215,9 @@ public static void main(String[] args) throws InterruptedException {
} }
``` ```
Program output: ### Program output
Note that the order of output could change everytime you run the program. Output may vary due to asynchronous subscriber processing:
The subscribers could take different time to consume the message.
``` ```
14:01:45.599 [ForkJoinPool.commonPool-worker-6] INFO com.iluwatar.publish.subscribe.subscriber.CustomerSupportSubscriber -- Customer Support Subscriber: 1416331388 sent the email to: support@test.de 14:01:45.599 [ForkJoinPool.commonPool-worker-6] INFO com.iluwatar.publish.subscribe.subscriber.CustomerSupportSubscriber -- Customer Support Subscriber: 1416331388 sent the email to: support@test.de
@@ -219,81 +228,48 @@ The subscribers could take different time to consume the message.
14:01:47.600 [ForkJoinPool.commonPool-worker-3] INFO com.iluwatar.publish.subscribe.subscriber.DelayedWeatherSubscriber -- Delayed Weather Subscriber: 2085808749 issued message: earthquake 14:01:47.600 [ForkJoinPool.commonPool-worker-3] INFO com.iluwatar.publish.subscribe.subscriber.DelayedWeatherSubscriber -- Delayed Weather Subscriber: 2085808749 issued message: earthquake
``` ```
This demonstrates:
* Subscribers reacting independently to messages published to subscribed topics.
* Dynamic subscription management allows changing which subscribers listen to specific topics.
* The asynchronous and loosely coupled nature of the publish-subscribe pattern in Java applications.
## When to Use the Publish-Subscribe Pattern ## When to Use the Publish-Subscribe Pattern
- Event-Driven Systems * When an application requires loose coupling between event producers and consumers.
- Use Pub/Sub when your system relies on events (e.g., user registration, payment completion). * In scenarios where multiple subscribers independently react to the same event.
- Example: After a user registers, send a welcome email and log the action simultaneously. * When developing scalable, asynchronous messaging systems, particularly within microservices architectures.
- Asynchronous Communication ## Real-World Applications of Publish-Subscribe Pattern in Java
- When tasks can be performed without waiting for immediate responses.
- Example: In an e-commerce app, notify the warehouse and the user after a successful order.
- Decoupling Components * Java Message Service (JMS) implementations (ActiveMQ, RabbitMQ)
- Ideal for systems where producers and consumers should not depend on each other. * Apache Kafka (used extensively in Java-based microservices)
- Example: A logging service listens for logs from multiple microservices. * Spring Framework's event publishing and listening mechanisms
* Google Cloud Pub/Sub in Java applications
- Scaling Systems * AWS Simple Notification Service (SNS) with Java SDK
- Useful when you need to scale services without changing the core application logic.
- Example: Broadcasting messages to thousands of clients (chat applications, IoT).
- Broadcasting Notifications
- When a message should be delivered to multiple receivers.
- Example: Sending promotional offers to multiple user devices.
- Microservices Communication
- Allow independent services to communicate without direct coupling.
- Example: An order service publishes an event, and both the billing and shipping services process it.
## When to avoid the Publish-Subscribe Pattern
- Simple applications where direct calls suffice.
- Strong consistency requirements (e.g., banking transactions).
- Low-latency synchronous communication needed.
## Benefits and Trade-offs of Publish-Subscribe Pattern ## Benefits and Trade-offs of Publish-Subscribe Pattern
### Benefits: Benefits:
- Decoupling * Loose coupling between publishers and subscribers promotes flexibility.
- Publishers and subscribers are independent of each other. * Improved scalability and maintainability as new subscribers can be easily added.
- Publishers don’t need to know who the subscribers are, and vice versa. * Supports asynchronous communication, enhancing system responsiveness.
- Changes in one component don’t affect the other.
- Scalability
- New subscribers can be added without modifying publishers.
- Supports distributed systems where multiple services consume the same events.
- Dynamic Subscription
- Subscribers can subscribe/unsubscribe at runtime.
- Enables flexible event-driven architectures.
- Asynchronous Communication
- Publishers and subscribers operate independently, improving performance.
- Useful for background processing (e.g., notifications, logging).
- Broadcast Communication
- A single event can be consumed by multiple subscribers.
- Useful for fan-out scenarios (e.g., notifications, analytics).
- Resilience & Fault Tolerance
- If a subscriber fails, others can still process messages.
- Message brokers (e.g., Kafka, RabbitMQ) can retry or persist undelivered messages.
### Trade-offs: Trade-offs:
- Complexity in Debugging * Increased complexity due to asynchronous message handling and debugging difficulties.
- Since publishers and subscribers are decoupled, tracing event flow can be difficult. * Potential message delivery delays and inconsistency if the infrastructure isn't reliable.
- Requires proper logging and monitoring tools. * Risk of message flooding, requiring proper infrastructure and consumer management.
- Message Ordering & Consistency
- Ensuring message order across subscribers can be challenging (e.g., Kafka vs. RabbitMQ).
- Some systems may process events out of order.
- Potential Latency
- Asynchronous processing introduces delays compared to direct calls.
- Not ideal for real-time synchronous requirements.
## Related Java Design Patterns ## Related Java Design Patterns
* [Observer Pattern](https://github.com/sanurah/java-design-patterns/blob/master/observer/): Both involve a producer (subject/publisher) notifying consumers (observers/subscribers). Observer is synchronous & tightly coupled (observers know the subject). Pub-Sub is asynchronous & decoupled (via a message broker). * [Observer Pattern](https://java-design-patterns.com/patterns/observer/): Both patterns establish a publisher-subscriber relationship; however, Observer typically works within a single application boundary synchronously, whereas Publish-Subscribe is often distributed and asynchronous.
* [Mediator Pattern](https://github.com/sanurah/java-design-patterns/blob/master/mediator/): A mediator centralizes communication between components (like a message broker in Pub-Sub). Mediator focuses on reducing direct dependencies between objects. Pub-Sub focuses on broadcasting events to unknown subscribers. * [Mediator Pattern](https://java-design-patterns.com/patterns/mediator/): Mediator encapsulates interactions between objects in a centralized manner, whereas Publish-Subscribe provides decentralized, loosely-coupled interactions.
## References and Credits ## References and Credits
* [Apache Kafka – Pub-Sub Model](https://kafka.apache.org/documentation/#design_pubsub) * [Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions](https://amzn.to/3WcFVui)
* [Microsoft – Publish-Subscribe Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber) * [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq)
* [Martin Fowler – Event-Driven Architecture](https://martinfowler.com/articles/201701-event-driven.html) * [Pattern-Oriented Software Architecture Volume 2: Patterns for Concurrent and Networked Objects](https://amzn.to/3UgC24V)
* [Publisher-Subscriber Pattern (Microsoft)](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber)
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

@@ -28,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class AppTest { class AppTest {
@Test @Test
void shouldExecuteApplicationWithoutException() { void shouldExecuteApplicationWithoutException() {
@@ -28,7 +28,6 @@ import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender; import ch.qos.logback.core.read.ListAppender;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext;
@@ -53,12 +52,10 @@ public class LoggerExtension implements BeforeEachCallback, AfterEachCallback {
} }
public List<String> getMessages() { public List<String> getMessages() {
return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList()); return listAppender.list.stream().map(e -> e.getMessage()).toList();
} }
public List<String> getFormattedMessages() { public List<String> getFormattedMessages() {
return listAppender.list.stream() return listAppender.list.stream().map(e -> e.getFormattedMessage()).toList();
.map(e -> e.getFormattedMessage())
.collect(Collectors.toList());
} }
} }
@@ -29,10 +29,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class MessageTest { class MessageTest {
@Test @Test
public void testMessage() { void testMessage() {
final String content = "some content"; final String content = "some content";
Message message = new Message(content); Message message = new Message(content);
assertInstanceOf(String.class, message.content()); assertInstanceOf(String.class, message.content());
@@ -33,7 +33,7 @@ import java.lang.reflect.Field;
import java.util.Set; import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
public class TopicTest { class TopicTest {
private static final String TOPIC_WEATHER = "WEATHER"; private static final String TOPIC_WEATHER = "WEATHER";
@@ -36,7 +36,7 @@ import java.util.Set;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.api.extension.RegisterExtension;
public class PublisherTest { class PublisherTest {
@RegisterExtension public LoggerExtension loggerExtension = new LoggerExtension(); @RegisterExtension public LoggerExtension loggerExtension = new LoggerExtension();