Loading content...
Imagine you're building a stock trading platform. When a trade executes, dozens of systems need to know: the real-time dashboard needs to update, the portfolio service needs to recalculate positions, the risk engine needs to reassess exposure, the audit log needs to record the transaction, the notification service needs to alert users, and the analytics pipeline needs to capture the event for reporting.
With point-to-point messaging, your trade execution service would need to know about every one of these consumers. It would need to send separate messages to each, maintain separate connections, and become increasingly fragile as consumers are added or removed. The trade service becomes a bottleneck that knows too much about the rest of the system.
The publish-subscribe pattern (often called pub-sub) fundamentally inverts this architecture. Instead of the producer pushing to specific consumers, the producer broadcasts to a topic, and interested consumers subscribe to receive the events. The producer doesn't know—or care—how many consumers exist, who they are, or what they do with the messages.
By the end of this page, you will understand the fundamental mechanics of one-to-many messaging, how pub-sub creates true decoupling between producers and consumers, and why this pattern is essential for building scalable, event-driven distributed systems.
To truly appreciate pub-sub, we must first understand the limitations it addresses. In traditional point-to-point messaging (like message queues), a message is produced by one sender and consumed by exactly one receiver. This creates a fundamental constraint: tight coupling between producers and consumers.
Let's trace the evolution of messaging patterns through a real-world lens:
| Pattern | Message Flow | Coupling | Scalability Challenge |
|---|---|---|---|
| Direct API Calls | Producer → Consumer | Tight: Producer knows consumer's address and API | Producer blocked waiting for response; consumer must be available |
| Message Queue | Producer → Queue → Consumer | Medium: Producer knows queue; queue routes to single consumer | Single consumer bottleneck; adding consumers creates competition |
| Publish-Subscribe | Producer → Topic → Many Consumers | Loose: Producer only knows topic; any number of consumers subscribe | Independent scaling; consumers added/removed transparently |
The Critical Insight
The fundamental shift in pub-sub is inverting the dependency direction. In point-to-point, the producer depends on the consumer (it needs to know where to send messages). In pub-sub, the consumer depends on the producer's events, but the producer has no runtime dependency on consumers.
Consider what happens when you need to add a new consumer in each paradigm:
This asymmetry is why pub-sub enables teams to move independently. The team maintaining the trade execution service doesn't need to coordinate with every downstream team. They publish events; anyone who cares subscribes.
Think of traditional messaging like sending personal letters—you need to know each recipient's address and send separate letters. Pub-sub is like publishing a newspaper: you print once, and anyone who subscribes receives a copy. The publisher doesn't need to track subscribers; the subscription mechanism handles it.
Every pub-sub system shares common architectural elements, though implementations vary in their specifics. Understanding these components and their interactions is essential for designing effective event-driven architectures.
Key Observation: Notice how the Trade Service publishes to the trades topic without any knowledge that four different services are subscribed. If the Risk Engine goes down or a new Machine Learning service subscribes tomorrow, the Trade Service remains unchanged. This is true decoupling at the infrastructure level.
Understanding the complete lifecycle of a message is crucial for designing reliable pub-sub systems. Let's trace a message from production to consumption, examining what happens at each stage.
A critical property of pub-sub is that acknowledgments are per-subscription. If the Portfolio Service acknowledges a trade event but the Risk Engine is still processing, the broker tracks them independently. One subscriber's speed doesn't block another's—this is essential for allowing heterogeneous consumers with different processing speeds.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
// Publisher: Trade Execution Serviceinterface TradeExecutedEvent { eventId: string; eventType: 'TRADE_EXECUTED'; timestamp: string; correlationId: string; payload: { tradeId: string; symbol: string; side: 'BUY' | 'SELL'; quantity: number; price: number; accountId: string; executedAt: string; };} async function publishTradeEvent(trade: Trade): Promise<void> { const event: TradeExecutedEvent = { eventId: generateEventId(), eventType: 'TRADE_EXECUTED', timestamp: new Date().toISOString(), correlationId: trade.orderId, // Links to original order payload: { tradeId: trade.id, symbol: trade.symbol, side: trade.side, quantity: trade.quantity, price: trade.executedPrice, accountId: trade.accountId, executedAt: trade.executedAt, }, }; // Publish to topic - fire and forget from publisher's perspective // The broker handles fan-out to all subscribers await messageBroker.publish('trades', event, { messageKey: trade.accountId, // Partition key for ordering headers: { 'content-type': 'application/json', 'schema-version': 'v1.2', }, }); // Publisher is done - doesn't wait for any subscriber logger.info(`Trade event published: ${trade.id}`);} // Subscriber: Portfolio Serviceasync function handleTradeEvent(message: Message): Promise<void> { const event = JSON.parse(message.payload) as TradeExecutedEvent; try { // Update portfolio position await portfolioService.updatePosition({ accountId: event.payload.accountId, symbol: event.payload.symbol, quantityDelta: event.payload.side === 'BUY' ? event.payload.quantity : -event.payload.quantity, avgPriceDelta: event.payload.price, }); // Acknowledge successful processing await message.ack(); } catch (error) { // Negative acknowledgment - message will be redelivered await message.nack({ requeue: true }); logger.error(`Failed to process trade ${event.payload.tradeId}`, error); }}The defining characteristic of pub-sub is the fan-out guarantee: every subscriber to a topic receives every message published to that topic. This sounds simple, but its implications are profound.
What the Fan-Out Guarantee Means:
Contrast with Message Queues:
In a traditional message queue, multiple consumers compete for messages. If three instances of a service consume from a queue, each message goes to exactly one instance. This is load-balancing, not broadcasting.
Real systems often need BOTH patterns. Apache Kafka introduced 'consumer groups' to solve this: messages fan-out to all consumer groups (pub-sub behavior), but within each group, messages are distributed to one consumer (queue behavior). This lets you have multiple services (each a group) receiving all messages, while each service scales horizontally with competing instances.
Pub-sub provides decoupling along multiple dimensions simultaneously. Understanding each dimension helps you appreciate the architectural flexibility this pattern provides.
| Dimension | Without Pub-Sub | With Pub-Sub | Engineering Benefit |
|---|---|---|---|
| Space (Location) | Producer needs consumer addresses | Producer knows only topic | Consumers can move, scale, or be replaced without producer changes |
| Time (Synchrony) | Producer waits for consumer response | Producer completes immediately | Producer performance independent of consumer speed; no blocking |
| Identity (Knowledge) | Producer maintains list of consumers | Producer unaware of subscribers | Teams deploy independently; no coordination overhead |
| Cardinality (Count) | Producer sends N messages for N consumers | Producer sends 1 message; broker fans out | O(1) producer work regardless of subscriber count |
| Reliability (Failure) | Producer handles each consumer's failures | Broker handles delivery; retries per subscription | Producer logic simplified; failure isolation per consumer |
Space Decoupling is the most visible: producers don't need to know consumer addresses. But time decoupling is equally important—producers complete their work immediately without waiting for downstream processing.
Consider the contrast in a trade execution:
This 190x improvement in response time fundamentally changes what's possible for user-facing systems.
Decoupling dimensions map to organizational benefits. Space decoupling means teams don't coordinate deploys. Time decoupling means SLAs are independent. Identity decoupling means backlog priorities aren't blocked. This is why pub-sub is foundational to microservices—it enables Conway's Law to work in your favor.
Pub-sub systems implement message delivery using two fundamentally different approaches: push and pull. Each has distinct characteristics that affect performance, reliability, and operational complexity.
Push Model: The broker initiates delivery, forwarding messages to subscribers as they arrive.
How it works:
Advantages:
Disadvantages:
Examples: Google Pub/Sub (push mode), AWS SNS → Lambda, WebSockets
1234567891011121314151617181920212223
// Push model: Broker calls subscriber's endpoint// Subscriber exposes an HTTP endpoint import express from 'express';const app = express(); app.post('/webhooks/trades', async (req, res) => { const event = req.body; try { await processTradeEvent(event); res.status(200).send('OK'); // Acknowledge } catch (error) { res.status(500).send('Retry'); // Broker will retry }}); // Or with a push subscription in client code:const subscription = pubsub.subscription('trades-sub', { pushConfig: { pushEndpoint: 'https://my-service/webhooks/trades', },});A critical design decision in pub-sub systems is whether messages are ephemeral (delivered and discarded) or persistent (stored for replay). This distinction has profound implications for system design.
The Log-Based Revolution
Apache Kafka popularized the log-based pub-sub model, treating topics as append-only logs rather than message queues. This seemingly simple change enables powerful capabilities:
The trade-off is storage. Kafka logs can grow large, requiring careful retention policies and tiered storage strategies.
Persistent systems require retention decisions: delete after X days? Delete after X bytes? Keep forever with compaction (keeping only the latest value per key)? The right policy depends on your use case—audit logs need years of history; real-time dashboards need hours.
Pub-sub enables remarkable scalability, but understanding how it scales helps you design systems that leverage it effectively. Let's examine the scalability characteristics along different dimensions.
| Dimension | Typical Limit | Scaling Strategy | Warning Sign |
|---|---|---|---|
| Message throughput | 1M+ msgs/sec per cluster | Add brokers, increase partitions | Broker CPU saturation, replication lag |
| Subscriber fan-out | 1000+ subscribers per topic | Use hierarchical topics, shard by region | Broker memory pressure, slow fan-out |
| Message size | 1 MB practical limit | Reference pattern: store blob, pass URI | Network congestion, slow serialization |
| Partition count | 100k across cluster (Kafka) | Fewer partitions, more topics if needed | Long leader elections, ZK/controller strain |
| Consumer groups | 1000+ groups per cluster | Optimize offset storage, use compact topics | Slow group rebalancing, offset lag |
For large payloads (images, documents, videos), don't send the content through pub-sub. Store it in object storage (S3, GCS), then publish a small event with the object reference. Subscribers fetch the blob directly. This keeps the message bus lean and fast.
We've established the foundation of publish-subscribe messaging and its one-to-many delivery model. Let's consolidate the essential concepts:
What's Next:
Now that we understand the fundamental mechanics of one-to-many messaging, we'll dive deeper into topics and subscriptions—the organizational primitives that structure pub-sub systems. We'll explore how to design topic hierarchies, manage subscription lifecycles, and configure delivery semantics for different use cases.
You now understand the foundational concepts of one-to-many messaging in pub-sub systems. This paradigm shift from point-to-point to broadcast messaging is the cornerstone of scalable, event-driven architectures. Next, we'll explore how topics and subscriptions provide the organizational structure for this messaging pattern.