You stand before your latest creation—a beautiful Node.js microservices architecture. Each service is a self-contained masterpiece, performing its duties with precision. But they work in isolation, like musicians in soundproof rooms. The magic happens when they need to play in harmony. When an order is placed, the inventory should update, the email service should fire, the analytics should track. How do you conduct this orchestra without turning it into a tangled mess of HTTP calls?
This is where our journey begins. Not with complexity, but with an elegant idea: what if services could simply whisper into the void, and only those who needed to listen would hear?
The Canvas: Embracing Loose Coupling
The traditional request-response model is like a telephone game where everyone must hold the line. Pub/Sub is different. It's the art of broadcasting. A publisher sends a message to a channel, completely unaware of who's listening. Subscribers tune into channels, completely unaware of who's speaking. This beautiful ignorance is our foundation.
Our medium today is Redis—the lightning-fast, in-memory data structure store that becomes our concert hall. Our instruments: the simple PUBLISH and SUBSCRIBE commands.
The First Movement: The Simple Whisper
Let's start with the basics. Two services need to communicate. One speaks, one listens.
// publisher.js - The soloist
import redis from 'redis';
const publisher = redis.createClient();
await publisher.connect();
// A moment of creation, sent into the ether
await publisher.publish(
'order:created',
JSON.stringify({ id: 421, total: 99.99, userId: 'usr_927' })
);
console.log('🎶 Message sent into the cosmos');
await publisher.quit();
// subscriber.js - The attentive listener
import redis from 'redis';
const subscriber = redis.createClient();
await subscriber.connect();
// Tuning our instrument to the right frequency
await subscriber.subscribe('order:created', (message) => {
const order = JSON.parse(message);
console.log(`📦 New order received: ${order.id}`);
// Magic happens here - without the publisher knowing
});
console.log('👂 Listening for the symphony of commerce...');
This is beautiful in its simplicity, but it's just the opening notes. As senior artisans, we see the limitations. What if our subscriber crashes? What about message persistence? This is where our composition deepens.
The Second Movement: The Reliable Chorus
The basic Pub/Sub has a fundamental truth: fire-and-forget. If a subscriber is down, the message is lost forever. For mission-critical systems, we need something more robust. Enter Redis Streams—the evolved, persistent sibling of Pub/Sub.
// reliable-publisher.js
import redis from 'redis';
const client = redis.createClient();
await client.connect();
// Adding to the eternal stream
const messageId = await client.xAdd(
'orders:stream',
'*', // Let Redis generate the ID
{
event: 'order:created',
orderId: '421',
amount: '99.99',
timestamp: new Date().toISOString()
}
);
console.log(`💾 Message ${messageId} etched into the stream`);
// reliable-consumer.js
import redis from 'redis';
const client = redis.createClient();
await client.connect();
// The consumer group - our ensemble
try {
await client.xGroupCreate('orders:stream', 'email-service', '0', {
MKSTREAM: true // Create stream if it doesn't exist
});
} catch (e) {
// Group already exists - and that's fine
}
// The eternal listen
while (true) {
try {
const streams = await client.xReadGroup(
'email-service',
'consumer-1',
{
key: 'orders:stream',
id: '>'
},
{ COUNT: 10, BLOCK: 5000 }
);
if (streams) {
for (const { name, messages } of streams) {
for (const { id, message } of messages) {
console.log(`✉️ Sending email for order: ${message.orderId}`);
// Acknowledge processing
await client.xAck('orders:stream', 'email-service', id);
console.log(`✅ Acknowledged message ${id}`);
}
}
}
} catch (error) {
console.error('🎻 A dissonant chord:', error);
}
}
This is where the true artistry emerges. Each consumer group can have multiple consumers, each message is acknowledged, and unacknowledged messages can be claimed by other consumers if one fails. It's fault-tolerant, scalable, and persistent.
The Third Movement: The Pattern Matching Symphony
But what if we want to listen to multiple channels? What if we care about orders, but only high-value ones? Redis provides the brushstrokes for pattern matching.
// pattern-subscriber.js
import redis from 'redis';
const subscriber = redis.createClient();
await subscriber.connect();
// The art of selective listening
await subscriber.pSubscribe('order:*', (message, channel) => {
const eventType = channel.split(':')[1];
const order = JSON.parse(message);
switch (eventType) {
case 'created':
console.log(`🎉 Welcome new order ${order.id}`);
break;
case 'cancelled':
console.log(`😞 Farewell to order ${order.id}`);
break;
case 'fulfilled':
console.log(`🚚 Order ${order.id} is on its way!`);
break;
}
});
console.log('🎨 Listening for patterns in the chaos...');
The Masterpiece: Composing the Event-Driven Architecture
Now, let's step back and see the full composition. We're not just sending messages; we're building an ecosystem.
// event-bus.js - Our conductor
import redis from 'redis';
class EventBus {
constructor() {
this.publisher = redis.createClient();
this.subscriber = redis.createClient();
this.handlers = new Map();
}
async connect() {
await this.publisher.connect();
await this.subscriber.connect();
// Listen for all published events
this.subscriber.pSubscribe('*', (message, channel) => {
this._routeMessage(channel, message);
});
}
async publish(channel, event) {
const payload = JSON.stringify({
...event,
metadata: {
id: this._generateId(),
timestamp: new Date().toISOString(),
source: 'order-service'
}
});
await this.publisher.publish(channel, payload);
console.log(`🎼 Event published to ${channel}`);
}
subscribe(channel, handlerName, handler) {
const key = `${channel}:${handlerName}`;
this.handlers.set(key, handler);
console.log(`🎧 Handler ${handlerName} subscribed to ${channel}`);
}
_routeMessage(channel, message) {
for (const [key, handler] of this.handlers) {
if (key.startsWith(channel)) {
handler(JSON.parse(message));
}
}
}
_generateId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
// Using our masterpiece
const eventBus = new EventBus();
await eventBus.connect();
// Different services, same symphony
eventBus.subscribe('order:created', 'email-service', (event) => {
sendWelcomeEmail(event.userId, event.orderId);
});
eventBus.subscribe('order:created', 'inventory-service', (event) => {
reserveInventory(event.items, event.orderId);
});
eventBus.subscribe('order:created', 'analytics-service', (event) => {
trackConversion(event.orderId, event.total);
});
// Somewhere in your order service...
await eventBus.publish('order:created', {
orderId: '421',
userId: 'usr_927',
total: 99.99,
items: ['product_1', 'product_2']
});
The Art Gallery: Monitoring Your Symphony
A masterpiece needs to be seen and heard. Monitor everything:
// monitoring.js
eventBus.subscribe('order:*', 'monitoring-service', (event) => {
metrics.increment(`events.${event.metadata.source}`);
console.log(`📊 Event ${event.metadata.id} from ${event.metadata.source}`);
});
// Track processing times, error rates, consumer lag
// Your observability is the frame around your artwork
The Artist's Wisdom: Patterns and Anti-Patterns
Embrace:
- Idempotency: Process the same message multiple times safely
- Dead Letter Queues: Handle poison pills gracefully
- Backpressure: Don't let fast producers overwhelm slow consumers
Avoid:
- Over-Subscription: Don't make every service listen to everything
- Complex Message Schemas: Keep your events simple and focused
- Ignoring Failures: Always handle disconnections and errors
The Journey Continues
What we've built today is more than code. It's a philosophy. Each service becomes an independent actor, capable of joining or leaving the symphony without disrupting the performance. The order service doesn't know about inventory, doesn't care about emails. It simply announces its creation to the world.
This is the art of building systems that can evolve, that can scale, that can fail gracefully. Your services are no longer tightly-coupled monoliths but a distributed ensemble, each playing its part in the grand composition.
The silence between the notes is as important as the notes themselves. In the world of Pub/Sub, it's the beautiful space where new services can join, where old ones can rest, where the system breathes.
Your symphony awaits its conductor.
Top comments (0)