Skip to content

Commit c535bb7

Browse files
Sanura Hettiarachchisanurah
authored andcommitted
feat: add publisher-subscriber design pattern (iluwatar#2898)
1 parent 371439a commit c535bb7

File tree

19 files changed

+779
-0
lines changed

19 files changed

+779
-0
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@
227227
<module>bloc</module>
228228
<module>map-reduce</module>
229229
<module>service-stub</module>
230+
<module>publish-subscribe</module>
230231
</modules>
231232
<repositories>
232233
<repository>

publish-subscribe/README.md

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
---
2+
title: "Publisher-Subscriber Pattern in Java: Decoupling the solution with asynchronous communication"
3+
shortTitle: Proxy
4+
description: "Explore the Proxy design pattern in Java with detailed examples. Learn how it provides controlled access, facilitates lazy initialization, and ensures security. Ideal for developers looking to implement advanced Java techniques."
5+
category: Structural
6+
language: en
7+
tag:
8+
- Decoupling
9+
- Encapsulation
10+
- Gang Of Four
11+
- Lazy initialization
12+
- Proxy
13+
- Security
14+
- Wrapping
15+
---
16+
17+
## Intent of the Publisher-Subscriber Design Pattern
18+
19+
The publisher-subscriber design pattern is widely used in software architecture to transmit data between various components in a system.
20+
It is a behavioral design pattern aimed at achieving loosely coupled communication between objects.
21+
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,
22+
without needing to know who or what the subscribers are.
23+
24+
## Detailed Explanation of Publisher-Subscriber Pattern with Real-World Examples
25+
26+
- Messaging systems like Kafka, RabbitMQ, AWS SNS, JMS
27+
- **Kafka** : publishes messages to topics and subscribers consumes them in real time for analytics, logs or other purposes.
28+
- **RabbitMQ** : Uses exchanges as publisher and queues as subscribers to route messages
29+
- **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)
30+
31+
32+
- Event driven microservices
33+
- **Publisher** : Point of Sale(PoS) system records the sale of an item and publish the event
34+
- **Subscribers** : Inventory management service updates stock, Billing service sends e-bill to customer
35+
36+
37+
- Newsletter subscriptions
38+
- **Publisher** : Writes a new blog post and publish to subscribers
39+
- **Subscribers** : All the subscribers to the newsletter receive the email
40+
41+
## Programmatic Example of Publisher-Subscriber Pattern in Java
42+
43+
First we need to identify the Event on which we need the pub-sub methods to trigger.
44+
For example:
45+
46+
- Sending alerts based on the weather events such as earthquakes, floods and tornadoes
47+
- Sending an email to different customer support accounts when a support ticket is created.
48+
49+
The Message class below will hold the content of the message we need to pass between the publisher and the subscribers.
50+
51+
```java
52+
public record Message(Object content) {
53+
}
54+
55+
```
56+
57+
The Topic class will have the topic **name** based on the event
58+
59+
- Weather events TopicName WEATHER
60+
- Support ticket created TopicName CUSTOMER_SUPPORT
61+
Also the Topic contains a list of subscribers that will listen to that topic
62+
63+
We can add or remove subscribers from the subscription to the topic
64+
65+
```java
66+
public class Topic {
67+
68+
private final TopicName name;
69+
private final Set<Subscriber> subscribers = new CopyOnWriteArraySet<>();
70+
//...//
71+
}
72+
```
73+
74+
Then we can create the publisher. The publisher class has a set of topics.
75+
76+
- Each new topic has to be registered in the publisher.
77+
- Publish method will publish the _Message_ to the corresponding _Topic_.
78+
79+
```java
80+
public class PublisherImpl implements Publisher {
81+
82+
private static final Logger logger = LoggerFactory.getLogger(PublisherImpl.class);
83+
private final Set<Topic> topics = new HashSet<>();
84+
85+
@Override
86+
public void registerTopic(Topic topic) {
87+
topics.add(topic);
88+
}
89+
90+
@Override
91+
public void publish(Topic topic, Message message) {
92+
if (!topics.contains(topic)) {
93+
logger.error("This topic is not registered: {}", topic.getName());
94+
return;
95+
}
96+
topic.publish(message);
97+
}
98+
}
99+
```
100+
101+
Finally, we can Subscribers to the Topics we want to listen to.
102+
103+
- For WEATHER topic we will create _WeatherSubscriber_
104+
- For CUSTOMER_SUPPORT topic we will create _CustomerSupportSubscribe_
105+
106+
Both classes will have a _onMessage_ method which will take a Message input.
107+
108+
- On message method will verify the content of the message is as expected
109+
- After content is verified it will perform the operation based on the message
110+
- _WeatherSubscriber_ will send a weather alert based on the _Message_
111+
- _CustomerSupportSubscribe_will send an email based on the _Message_
112+
113+
```java
114+
public interface Subscriber {
115+
void onMessage(Message message);
116+
}
117+
```
118+
119+
And here is the invocation of the publisher and subscribers.
120+
121+
```java
122+
public static void main(String[] args) {
123+
124+
final String weatherSub1Name = "weatherSub1";
125+
final String weatherSub2Name = "weatherSub2";
126+
final String supportSub1Name = "supportSub1";
127+
final String supportSub2Name = "supportSub2";
128+
129+
Topic weatherTopic = new Topic(TopicName.WEATHER);
130+
Topic supportTopic = new Topic(TopicName.CUSTOMER_SUPPORT);
131+
132+
Publisher publisher = new PublisherImpl();
133+
publisher.registerTopic(weatherTopic);
134+
publisher.registerTopic(supportTopic);
135+
136+
Subscriber weatherSub1 = new WeatherSubscriber(weatherSub1Name);
137+
Subscriber weatherSub2 = new WeatherSubscriber(weatherSub2Name);
138+
weatherTopic.addSubscriber(weatherSub1);
139+
weatherTopic.addSubscriber(weatherSub2);
140+
141+
Subscriber supportSub1 = new CustomerSupportSubscriber(supportSub1Name);
142+
Subscriber supportSub2 = new CustomerSupportSubscriber(supportSub2Name);
143+
supportTopic.addSubscriber(supportSub1);
144+
supportTopic.addSubscriber(supportSub2);
145+
146+
publisher.publish(weatherTopic, new Message(WeatherContent.earthquake));
147+
publisher.publish(supportTopic, new Message(CustomerSupportContent.DE));
148+
}
149+
```
150+
151+
Program output:
152+
153+
```
154+
11:46:44.310 [main] INFO com.iluwatar.publish.subscribe.subscriber.WeatherSubscriber - Subscriber: weatherSub1 issued message: earthquake tsunami warning
155+
11:46:44.311 [main] INFO com.iluwatar.publish.subscribe.subscriber.WeatherSubscriber - Subscriber: weatherSub2 issued message: earthquake tsunami warning
156+
11:46:44.311 [main] INFO com.iluwatar.publish.subscribe.subscriber.CustomerSupportSubscriber - Subscriber: supportSub1 sent the email to: [email protected]
157+
11:46:44.311 [main] INFO com.iluwatar.publish.subscribe.subscriber.CustomerSupportSubscriber - Subscriber: supportSub2 sent the email to: [email protected]
158+
```
159+
160+
## When to Use the Publisher-Subscriber Pattern
161+
162+
- Event-Driven Systems
163+
- Use Pub/Sub when your system relies on events (e.g., user registration, payment completion).
164+
- Example: After a user registers, send a welcome email and log the action simultaneously.
165+
166+
- Asynchronous Communication
167+
- When tasks can be performed without waiting for immediate responses.
168+
- Example: In an e-commerce app, notify the warehouse and the user after a successful order.
169+
170+
- Decoupling Components
171+
- Ideal for systems where producers and consumers should not depend on each other.
172+
- Example: A logging service listens for logs from multiple microservices.
173+
174+
- Scaling Systems
175+
- Useful when you need to scale services without changing the core application logic.
176+
- Example: Broadcasting messages to thousands of clients (chat applications, IoT).
177+
178+
- Broadcasting Notifications
179+
- When a message should be delivered to multiple receivers.
180+
- Example: Sending promotional offers to multiple user devices.
181+
182+
- Microservices Communication
183+
- Allow independent services to communicate without direct coupling.
184+
- Example: An order service publishes an event, and both the billing and shipping services process it.
185+
186+
## When to avoid the Publisher-Subscriber Pattern
187+
188+
- Simple applications where direct calls suffice.
189+
- Strong consistency requirements (e.g., banking transactions).
190+
- Low-latency synchronous communication needed.
191+
192+
## Benefits and Trade-offs of Publisher-Subscriber Pattern
193+
194+
### Benefits:
195+
196+
- Decoupling
197+
- Publishers and subscribers are independent of each other.
198+
- Publishers don’t need to know who the subscribers are, and vice versa.
199+
- Changes in one component don’t affect the other.
200+
- Scalability
201+
- New subscribers can be added without modifying publishers.
202+
- Supports distributed systems where multiple services consume the same events.
203+
- Dynamic Subscription
204+
- Subscribers can subscribe/unsubscribe at runtime.
205+
- Enables flexible event-driven architectures.
206+
- Asynchronous Communication
207+
- Publishers and subscribers operate independently, improving performance.
208+
- Useful for background processing (e.g., notifications, logging).
209+
- Broadcast Communication
210+
- A single event can be consumed by multiple subscribers.
211+
- Useful for fan-out scenarios (e.g., notifications, analytics).
212+
- Resilience & Fault Tolerance
213+
- If a subscriber fails, others can still process messages.
214+
- Message brokers (e.g., Kafka, RabbitMQ) can retry or persist undelivered messages.
215+
216+
### Trade-offs:
217+
218+
- Complexity in Debugging
219+
- Since publishers and subscribers are decoupled, tracing event flow can be difficult.
220+
- Requires proper logging and monitoring tools.
221+
- Message Ordering & Consistency
222+
- Ensuring message order across subscribers can be challenging (e.g., Kafka vs. RabbitMQ).
223+
- Some systems may process events out of order.
224+
- Potential Latency
225+
- Asynchronous processing introduces delays compared to direct calls.
226+
- Not ideal for real-time synchronous requirements.
227+
228+
## Related Java Design Patterns
229+
230+
* [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).
231+
* [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.
232+
233+
## References and Credits
234+
235+
* [Apache Kafka – Pub-Sub Model](https://kafka.apache.org/documentation/#design_pubsub)
236+
* [Microsoft – Publish-Subscribe Pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/publisher-subscriber)
237+
* [Martin Fowler – Event-Driven Architecture](https://martinfowler.com/articles/201701-event-driven.html)

publish-subscribe/pom.xml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>com.iluwatar</groupId>
8+
<artifactId>java-design-patterns</artifactId>
9+
<version>1.26.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>publish-subscribe</artifactId>
13+
14+
<dependencies>
15+
<dependency>
16+
<groupId>org.junit.jupiter</groupId>
17+
<artifactId>junit-jupiter-engine</artifactId>
18+
<scope>test</scope>
19+
</dependency>
20+
<dependency>
21+
<groupId>org.junit.jupiter</groupId>
22+
<artifactId>junit-jupiter-api</artifactId>
23+
<scope>test</scope>
24+
</dependency>
25+
</dependencies>
26+
27+
</project>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.iluwatar.publish.subscribe;
2+
3+
import com.iluwatar.publish.subscribe.model.CustomerSupportContent;
4+
import com.iluwatar.publish.subscribe.model.Message;
5+
import com.iluwatar.publish.subscribe.model.Topic;
6+
import com.iluwatar.publish.subscribe.model.TopicName;
7+
import com.iluwatar.publish.subscribe.model.WeatherContent;
8+
import com.iluwatar.publish.subscribe.publisher.Publisher;
9+
import com.iluwatar.publish.subscribe.publisher.PublisherImpl;
10+
import com.iluwatar.publish.subscribe.subscriber.CustomerSupportSubscriber;
11+
import com.iluwatar.publish.subscribe.subscriber.Subscriber;
12+
import com.iluwatar.publish.subscribe.subscriber.WeatherSubscriber;
13+
14+
public class App {
15+
public static void main(String[] args) {
16+
17+
final String weatherSub1Name = "weatherSub1";
18+
final String weatherSub2Name = "weatherSub2";
19+
final String supportSub1Name = "supportSub1";
20+
final String supportSub2Name = "supportSub2";
21+
22+
Topic weatherTopic = new Topic(TopicName.WEATHER);
23+
Topic supportTopic = new Topic(TopicName.CUSTOMER_SUPPORT);
24+
25+
Publisher publisher = new PublisherImpl();
26+
publisher.registerTopic(weatherTopic);
27+
publisher.registerTopic(supportTopic);
28+
29+
Subscriber weatherSub1 = new WeatherSubscriber(weatherSub1Name);
30+
Subscriber weatherSub2 = new WeatherSubscriber(weatherSub2Name);
31+
weatherTopic.addSubscriber(weatherSub1);
32+
weatherTopic.addSubscriber(weatherSub2);
33+
34+
Subscriber supportSub1 = new CustomerSupportSubscriber(supportSub1Name);
35+
Subscriber supportSub2 = new CustomerSupportSubscriber(supportSub2Name);
36+
supportTopic.addSubscriber(supportSub1);
37+
supportTopic.addSubscriber(supportSub2);
38+
39+
publisher.publish(weatherTopic, new Message(WeatherContent.earthquake));
40+
publisher.publish(supportTopic, new Message(CustomerSupportContent.DE));
41+
}
42+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.iluwatar.publish.subscribe.model;
2+
3+
public enum CustomerSupportContent {
4+
5+
6+
7+
8+
private final String email;
9+
10+
CustomerSupportContent(String email) {
11+
this.email = email;
12+
}
13+
14+
public String getEmail() {
15+
return email;
16+
}
17+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package com.iluwatar.publish.subscribe.model;
2+
3+
public record Message(Object content) {
4+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package com.iluwatar.publish.subscribe.model;
2+
3+
import com.iluwatar.publish.subscribe.subscriber.Subscriber;
4+
import java.util.Set;
5+
import java.util.concurrent.CopyOnWriteArraySet;
6+
7+
public class Topic {
8+
9+
private final TopicName name;
10+
private final Set<Subscriber> subscribers = new CopyOnWriteArraySet<>();
11+
12+
public Topic(TopicName name) {
13+
this.name = name;
14+
}
15+
16+
public TopicName getName() {
17+
return name;
18+
}
19+
20+
public void addSubscriber(Subscriber subscriber) {
21+
subscribers.add(subscriber);
22+
}
23+
24+
public void removeSubscriber(Subscriber subscriber) {
25+
subscribers.remove(subscriber);
26+
}
27+
28+
public void publish(Message message) {
29+
for (Subscriber subscriber : subscribers) {
30+
subscriber.onMessage(message);
31+
}
32+
}
33+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.iluwatar.publish.subscribe.model;
2+
3+
public enum TopicName {
4+
WEATHER,
5+
CUSTOMER_SUPPORT
6+
}

0 commit comments

Comments
 (0)