Browse TypeScript Design Patterns & Application Architecture

Publish/Subscribe Pattern: Use Cases and Examples

Explore practical applications of the Publish/Subscribe Pattern in TypeScript, including real-time notifications, modular applications, and microservices architectures.

6.13.3 Use Cases and Examples

The Publish/Subscribe Pattern, often abbreviated as Pub/Sub, is a messaging pattern that allows for the decoupling of components in a system. It is particularly useful in scenarios where components need to communicate asynchronously without being tightly bound to each other. This pattern is widely used in various applications, from real-time web applications to complex microservices architectures. In this section, we will explore several practical use cases and examples of the Publish/Subscribe Pattern in TypeScript, highlighting its benefits and addressing potential challenges.

Real-Time Notification Systems Using WebSockets

Real-time notification systems are a common requirement in modern web applications. Whether it’s a chat application, a stock trading platform, or a collaborative tool, users expect instantaneous updates. The Publish/Subscribe Pattern, combined with WebSockets, provides an efficient way to implement such systems.

Implementing Real-Time Notifications

WebSockets enable full-duplex communication channels over a single TCP connection, making them ideal for real-time applications. In a Pub/Sub model, the server acts as the publisher, broadcasting messages to all connected clients (subscribers).

Example: Real-Time Chat Application

Let’s explore a simple chat application using TypeScript and WebSockets. We’ll use the Pub/Sub pattern to broadcast messages to all connected clients.

 1// Import necessary modules
 2import * as WebSocket from 'ws';
 3
 4// Create a WebSocket server
 5const wss = new WebSocket.Server({ port: 8080 });
 6
 7// Set up a message handler
 8wss.on('connection', (ws) => {
 9    ws.on('message', (message) => {
10        // Broadcast the message to all connected clients
11        wss.clients.forEach((client) => {
12            if (client.readyState === WebSocket.OPEN) {
13                client.send(message);
14            }
15        });
16    });
17});
18
19console.log('WebSocket server is running on ws://localhost:8080');

In this example, every time a client sends a message, the server broadcasts it to all connected clients. This is a straightforward implementation of the Pub/Sub pattern, where the server is the publisher and the clients are the subscribers.

Benefits and Challenges

Benefits:

  • Loose Coupling: Clients are not aware of each other; they only interact with the server.
  • Scalability: New clients can easily join the system without affecting existing ones.
  • Real-Time Updates: WebSockets provide low-latency communication.

Challenges:

  • Message Delivery Guarantees: Ensuring that messages are delivered in order and without loss can be challenging.
  • Handling Overload: The server must efficiently manage a large number of connections.

Modular Applications with Event Bus

In large applications, maintaining a modular architecture is crucial for scalability and maintainability. The Publish/Subscribe Pattern can be implemented using an event bus to facilitate communication between different modules.

Building Modular Applications

An event bus is a central hub through which different parts of an application can communicate. It allows modules to publish events and subscribe to them without direct dependencies.

Example: Event Bus in a Modular Application

Consider an application with separate modules for user authentication, notifications, and logging. We can use an event bus to decouple these modules.

 1// Define an EventBus class
 2class EventBus {
 3    private listeners: { [event: string]: Function[] } = {};
 4
 5    subscribe(event: string, listener: Function) {
 6        if (!this.listeners[event]) {
 7            this.listeners[event] = [];
 8        }
 9        this.listeners[event].push(listener);
10    }
11
12    publish(event: string, data: any) {
13        if (this.listeners[event]) {
14            this.listeners[event].forEach(listener => listener(data));
15        }
16    }
17}
18
19// Create an instance of EventBus
20const eventBus = new EventBus();
21
22// Subscribe to events
23eventBus.subscribe('userLoggedIn', (user) => {
24    console.log(`User logged in: ${user.name}`);
25});
26
27eventBus.subscribe('notification', (message) => {
28    console.log(`Notification: ${message}`);
29});
30
31// Publish events
32eventBus.publish('userLoggedIn', { name: 'Alice' });
33eventBus.publish('notification', 'You have a new message');

In this example, the EventBus class manages event subscriptions and publications. Modules can subscribe to specific events and react accordingly when those events are published.

Benefits and Challenges

Benefits:

  • Decoupled Architecture: Modules can be developed and tested independently.
  • Flexibility: New modules can be added without modifying existing ones.

Challenges:

  • Event Management: Keeping track of all events and their listeners can become complex.
  • Debugging: Tracing the flow of events can be difficult in large systems.

Microservices Architecture with Message Queues

Microservices architectures benefit greatly from the Publish/Subscribe Pattern, especially when services need to communicate asynchronously. Message queues, such as RabbitMQ or Apache Kafka, are often used to implement this pattern.

Designing Microservices with Pub/Sub

In a microservices architecture, services can publish events to a message queue, and other services can subscribe to those events. This allows for asynchronous communication and loose coupling between services.

Example: Order Processing System

Let’s consider an order processing system where different services handle order creation, payment, and shipping. We’ll use a message queue to facilitate communication.

 1// Simulate a message queue
 2class MessageQueue {
 3    private subscribers: { [event: string]: Function[] } = {};
 4
 5    subscribe(event: string, callback: Function) {
 6        if (!this.subscribers[event]) {
 7            this.subscribers[event] = [];
 8        }
 9        this.subscribers[event].push(callback);
10    }
11
12    publish(event: string, data: any) {
13        if (this.subscribers[event]) {
14            this.subscribers[event].forEach(callback => callback(data));
15        }
16    }
17}
18
19// Create a message queue instance
20const messageQueue = new MessageQueue();
21
22// Order service publishes an event
23function orderService() {
24    const order = { id: 1, item: 'Laptop' };
25    console.log('Order created:', order);
26    messageQueue.publish('orderCreated', order);
27}
28
29// Payment service subscribes to the orderCreated event
30messageQueue.subscribe('orderCreated', (order) => {
31    console.log('Processing payment for order:', order.id);
32    // Simulate payment processing
33    setTimeout(() => {
34        console.log('Payment processed for order:', order.id);
35        messageQueue.publish('paymentProcessed', order);
36    }, 1000);
37});
38
39// Shipping service subscribes to the paymentProcessed event
40messageQueue.subscribe('paymentProcessed', (order) => {
41    console.log('Shipping order:', order.id);
42    // Simulate shipping
43    setTimeout(() => {
44        console.log('Order shipped:', order.id);
45    }, 1000);
46});
47
48// Start the order processing
49orderService();

In this example, the MessageQueue class simulates a message queue. The order service publishes an orderCreated event, which the payment service subscribes to. Once the payment is processed, the payment service publishes a paymentProcessed event, which the shipping service subscribes to.

Benefits and Challenges

Benefits:

  • Scalability: Services can be scaled independently based on load.
  • Resilience: Services can continue to operate even if some components are down.

Challenges:

  • Message Delivery Guarantees: Ensuring reliable message delivery can be complex.
  • Latency: Asynchronous communication can introduce latency.

Handling Message Delivery and Overload

While the Publish/Subscribe Pattern offers many benefits, it also presents challenges related to message delivery and overload. Let’s explore these challenges and potential solutions.

Message Delivery Guarantees

In a Pub/Sub system, ensuring that messages are delivered reliably and in order is crucial. This can be challenging, especially in distributed systems.

Solutions:

  • Acknowledgments: Require subscribers to acknowledge receipt of messages to ensure delivery.
  • Retries: Implement retry mechanisms for failed message deliveries.
  • Ordering: Use sequence numbers or timestamps to maintain message order.

Handling Message Overload

In systems with high message throughput, managing overload is essential to prevent bottlenecks.

Solutions:

  • Throttling: Limit the rate of message publication or subscription to prevent overload.
  • Load Balancing: Distribute messages across multiple subscribers to balance the load.
  • Backpressure: Implement backpressure mechanisms to slow down message production when the system is overloaded.

Encouraging the Use of Publish/Subscribe Pattern

The Publish/Subscribe Pattern is a powerful tool for designing systems that require decoupled communication between components. By enabling loose coupling and scalability, it allows developers to build flexible and maintainable architectures.

Considerations for Using Pub/Sub

When designing a system, consider the following factors to determine if the Publish/Subscribe Pattern is appropriate:

  • Decoupling Needs: If components need to communicate without direct dependencies, Pub/Sub is a good fit.
  • Scalability Requirements: If the system needs to scale easily, Pub/Sub can facilitate this by allowing components to be added or removed without affecting others.
  • Asynchronous Communication: If components need to communicate asynchronously, Pub/Sub provides a natural solution.

Try It Yourself

To gain a deeper understanding of the Publish/Subscribe Pattern, try modifying the examples provided:

  • Extend the Chat Application: Add features such as private messaging or message history.
  • Enhance the Event Bus: Implement additional events and subscribers to simulate a more complex application.
  • Expand the Order Processing System: Add new services, such as inventory management or customer notifications.

By experimenting with these examples, you’ll gain hands-on experience with the Publish/Subscribe Pattern and its applications in TypeScript.

Visualizing the Publish/Subscribe Pattern

To better understand the flow of messages in a Publish/Subscribe system, let’s visualize the architecture using a sequence diagram.

    sequenceDiagram
	    participant Publisher
	    participant MessageQueue
	    participant Subscriber1
	    participant Subscriber2
	
	    Publisher->>MessageQueue: Publish Event
	    MessageQueue->>Subscriber1: Deliver Event
	    MessageQueue->>Subscriber2: Deliver Event
	    Subscriber1->>MessageQueue: Acknowledge Event
	    Subscriber2->>MessageQueue: Acknowledge Event

Diagram Description: This sequence diagram illustrates the flow of messages in a Publish/Subscribe system. The publisher sends an event to the message queue, which then delivers the event to all subscribers. Each subscriber acknowledges receipt of the event.

Conclusion

The Publish/Subscribe Pattern is a versatile and powerful design pattern that facilitates decoupled communication in various applications. From real-time notifications to modular applications and microservices architectures, it provides a robust solution for managing asynchronous communication. By understanding its benefits and challenges, developers can effectively leverage this pattern to build scalable and maintainable systems.

Quiz Time!

Loading quiz…
Revised on Thursday, April 23, 2026