diff --git a/publish-subscribe/README.md b/publish-subscribe/README.md index 8ff29f2c6..86f6c5316 100644 --- a/publish-subscribe/README.md +++ b/publish-subscribe/README.md @@ -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 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 tag: + - Architecture + - Asynchronous - Decoupling + - Event-driven + - Messaging + - Microservices + - Publish/subscribe + - Scalability --- ## 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. -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. +Defines a one-to-many dependency between objects, enabling automatic notification of multiple subscribers when a publisher's state changes or an event occurs. ## Detailed Explanation of Publish-Subscribe Pattern with Real-World Examples -### Real-world example +Real-world example -- Messaging systems like Kafka, RabbitMQ, AWS SNS, JMS - - **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) +> 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. +In plain words -- Event driven microservices - - **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 +> 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 -- Newsletter subscriptions - - **Publisher** : Writes a new blog post and publish to subscribers - - **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. +> 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). -### Architectural Diagram -![pub-sub](./etc/pub-sub.png) +Sequence diagram + +![Publish-Subscribe sequence diagram](./etc/publish-subscribe-sequence-diagram.png) ## 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. -For example: +First, we identify events that trigger the publisher-subscriber interactions. Common examples include: -- Sending alerts based on the weather events such as earthquakes, floods and tornadoes -- Sending alerts based on the temperature -- Sending an email to different customer support emails when a support ticket is created. +* Sending alerts based on weather events, like earthquakes, floods, and tornadoes. +* Sending notifications based on temperature changes. +* 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 public record Message(Object content) { } - ``` -The Topic class will have the topic **name** based on the event +### Defining Topics -- Weather events TopicName WEATHER -- 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 +A Topic represents an event category that subscribers can register to and publishers can publish messages to. Each topic has: -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 +@Getter +@Setter +@RequiredArgsConstructor public class Topic { - private final TopicName name; - private final Set subscribers = new CopyOnWriteArraySet<>(); - //...// + private final String topicName; + private final Set 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. -- Publish method will publish the _Message_ to the corresponding _Topic_. +The Publisher maintains a collection of topics it can publish to. + +* Before publishing, a topic must be registered. +* Upon publishing, it forwards messages to subscribers of the corresponding topic. ```java 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_ -- _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 +Subscribers implement an interface that handles incoming messages. -All classes will have a _onMessage_ method which will take a Message input. - -- 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 +* Each subscriber processes messages according to specific logic. +* Subscribers can be registered to multiple topics. ```java 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 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. -The subscribers could take different time to consume the message. +Output may vary due to asynchronous subscriber processing: ``` 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 ``` +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 -- Event-Driven Systems - - Use Pub/Sub when your system relies on events (e.g., user registration, payment completion). - - Example: After a user registers, send a welcome email and log the action simultaneously. +* When an application requires loose coupling between event producers and consumers. +* In scenarios where multiple subscribers independently react to the same event. +* When developing scalable, asynchronous messaging systems, particularly within microservices architectures. -- Asynchronous Communication - - 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. +## Real-World Applications of Publish-Subscribe Pattern in Java -- Decoupling Components - - Ideal for systems where producers and consumers should not depend on each other. - - Example: A logging service listens for logs from multiple microservices. - -- Scaling Systems - - 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. +* Java Message Service (JMS) implementations (ActiveMQ, RabbitMQ) +* Apache Kafka (used extensively in Java-based microservices) +* Spring Framework's event publishing and listening mechanisms +* Google Cloud Pub/Sub in Java applications +* AWS Simple Notification Service (SNS) with Java SDK ## Benefits and Trade-offs of Publish-Subscribe Pattern -### Benefits: +Benefits: -- Decoupling - - Publishers and subscribers are independent of each other. - - Publishers don’t need to know who the subscribers are, and vice versa. - - 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. +* Loose coupling between publishers and subscribers promotes flexibility. +* Improved scalability and maintainability as new subscribers can be easily added. +* Supports asynchronous communication, enhancing system responsiveness. -### Trade-offs: +Trade-offs: -- Complexity in Debugging - - Since publishers and subscribers are decoupled, tracing event flow can be difficult. - - Requires proper logging and monitoring tools. -- 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. +* Increased complexity due to asynchronous message handling and debugging difficulties. +* Potential message delivery delays and inconsistency if the infrastructure isn't reliable. +* Risk of message flooding, requiring proper infrastructure and consumer management. ## 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). -* [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. +* [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://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 -* [Apache Kafka – Pub-Sub Model](https://kafka.apache.org/documentation/#design_pubsub) -* [Microsoft – Publish-Subscribe Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber) -* [Martin Fowler – Event-Driven Architecture](https://martinfowler.com/articles/201701-event-driven.html) +* [Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions](https://amzn.to/3WcFVui) +* [Head First Design Patterns: Building Extensible and Maintainable Object-Oriented Software](https://amzn.to/49NGldq) +* [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) diff --git a/publish-subscribe/etc/publish-subscribe-sequence-diagram.png b/publish-subscribe/etc/publish-subscribe-sequence-diagram.png new file mode 100644 index 000000000..8fb121341 Binary files /dev/null and b/publish-subscribe/etc/publish-subscribe-sequence-diagram.png differ diff --git a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/AppTest.java b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/AppTest.java index 808055307..50c780cb6 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/AppTest.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/AppTest.java @@ -28,7 +28,7 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import org.junit.jupiter.api.Test; -public class AppTest { +class AppTest { @Test void shouldExecuteApplicationWithoutException() { diff --git a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/LoggerExtension.java b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/LoggerExtension.java index 05e16e5a1..ecf015752 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/LoggerExtension.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/LoggerExtension.java @@ -28,7 +28,6 @@ import ch.qos.logback.classic.Logger; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import java.util.List; -import java.util.stream.Collectors; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; @@ -53,12 +52,10 @@ public class LoggerExtension implements BeforeEachCallback, AfterEachCallback { } public List getMessages() { - return listAppender.list.stream().map(e -> e.getMessage()).collect(Collectors.toList()); + return listAppender.list.stream().map(e -> e.getMessage()).toList(); } public List getFormattedMessages() { - return listAppender.list.stream() - .map(e -> e.getFormattedMessage()) - .collect(Collectors.toList()); + return listAppender.list.stream().map(e -> e.getFormattedMessage()).toList(); } } diff --git a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/MessageTest.java b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/MessageTest.java index a08624a3f..636e1ed66 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/MessageTest.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/MessageTest.java @@ -29,10 +29,10 @@ import static org.junit.jupiter.api.Assertions.assertInstanceOf; import org.junit.jupiter.api.Test; -public class MessageTest { +class MessageTest { @Test - public void testMessage() { + void testMessage() { final String content = "some content"; Message message = new Message(content); assertInstanceOf(String.class, message.content()); diff --git a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/TopicTest.java b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/TopicTest.java index eb2d87c8c..cbb5a9882 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/TopicTest.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/model/TopicTest.java @@ -33,7 +33,7 @@ import java.lang.reflect.Field; import java.util.Set; import org.junit.jupiter.api.Test; -public class TopicTest { +class TopicTest { private static final String TOPIC_WEATHER = "WEATHER"; diff --git a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/publisher/PublisherTest.java b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/publisher/PublisherTest.java index d3db88c42..7105db20f 100644 --- a/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/publisher/PublisherTest.java +++ b/publish-subscribe/src/test/java/com/iluwatar/publish/subscribe/publisher/PublisherTest.java @@ -36,7 +36,7 @@ import java.util.Set; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -public class PublisherTest { +class PublisherTest { @RegisterExtension public LoggerExtension loggerExtension = new LoggerExtension();