Messaging is the backbone of distributed systems. It enables services to communicate asynchronously, decouple dependencies, and build resilient architectures. This article covers the two most fundamental messaging patterns: publish-subscribe and request-reply.


The Case for Messaging


Direct HTTP calls between services create tight coupling. When Service A calls Service B directly:


  • Service A must know Service B's location and API contract.
  • Service A is blocked while Service B processes the request.
  • If Service B is down, Service A fails.
  • Scaling Service B does not help Service A's performance.

  • Messaging intermediaries (message brokers like Kafka, RabbitMQ, SQS, or Pub/Sub) solve these problems by decoupling senders from receivers.


    Publish-Subscribe (Pub/Sub)


    In the pub/sub pattern, publishers send messages to a topic without knowing who the subscribers are. Subscribers receive messages from topics they have subscribed to.


    
    [Publisher] -> [Topic] -> [Subscriber A]
    
                           -> [Subscriber B]
    
                           -> [Subscriber C]
    
    

    How Pub/Sub Works


  • **Topics** are named channels that hold messages.
  • 2. **Publishers** send messages to a topic.

    3. **Subscribers** register interest in a topic and receive all messages published to it.

    4. Each subscriber receives a copy of every message (fan-out delivery).


    Use Cases


    **Event notification.** When a user signs up, publish a `UserRegistered` event. Multiple subscribers react: the email service sends a welcome email, the analytics service records the event, the CRM service creates a contact.


    **Broadcasting.** A live sports app publishes score updates. Thousands of clients receive updates in real-time.


    **Log aggregation.** Multiple services publish log entries to a central topic. A log processing service stores and indexes them.


    Example with Kafka


    
    # Publisher
    
    producer.send('order-events', {
    
        'type': 'OrderPlaced',
    
        'order_id': '123',
    
        'customer_id': '456',
    
        'total': 99.99
    
    })
    
    
    
    # Subscriber
    
    @kafka_listener('order-events')
    
    def handle_order_placed(event):
    
        if event['type'] == 'OrderPlaced':
    
            inventory_service.reserve_inventory(event['order_id'])
    
            notification_service.send_confirmation(event['customer_id'])
    
    

    Pub/Sub is ideal for one-to-many communication where the publisher does not need a response.


    Request-Reply


    The request-reply pattern is fundamentally different from pub/sub. A sender sends a request and expects a response. The two are correlated so the sender knows which response goes with which request.


    In messaging systems, request-reply requires correlation:


    
    [Requestor] -> [Request Queue] -> [Replier]
    
                                               
    
    [Requestor] <- [Reply Queue]   <- [Replier]
    
    

    Correlation ID


    The key mechanism is the correlation ID. The requestor includes a unique ID in the request message. The replier includes the same ID in the reply message. The requestor uses this ID to match replies to pending requests.


    
    # Requestor
    
    correlation_id = str(uuid.uuid4())
    
    reply_queue = f"reply-{correlation_id}"
    
    
    
    message = {
    
        'correlation_id': correlation_id,
    
        'payload': {'user_id': '123', 'amount': 100}
    
    }
    
    
    
    request_queue.send(message)
    
    
    
    # Wait for reply on the reply queue
    
    reply = reply_queue.receive(timeout=30)
    
    

    
    # Replier
    
    def handle_request(message):
    
        result = process(message['payload'])
    
        reply = {
    
            'correlation_id': message['correlation_id'],
    
            'payload': result
    
        }
    
        reply_queue.send(reply)
    
    

    Use Cases


    **Remote procedure calls.** Service A asks Service B to compute something and return the result. Unlike HTTP RPC, the messaging-based approach allows the requestor to be decoupled in time -- it can send the request and check for the reply later.


    **Long-running operations.** Submit a job, get back a job ID, check progress, and eventually receive the result. The reply might arrive minutes or hours later.


    Pub/Sub vs. Request-Reply


    | Aspect | Pub/Sub | Request-Reply |

    |--------|---------|---------------|

    | Communication | One-to-many | One-to-one |

    | Response expected? | No | Yes |

    | Coupling | Very loose | Moderate |

    | Use case | Event notification, broadcasting | Remote procedure, query |

    | Message ordering | Per partition/topic | Per conversation |

    | Error handling | Dead letter queue | Timeout + retry |


    Choosing a Message Broker


    **Apache Kafka:** Best for high-throughput event streaming, log aggregation, and event sourcing. Messages are persisted and replayable. Excellent for building event-driven architectures.


    **RabbitMQ:** Best for traditional messaging with complex routing (direct, topic, headers, fanout exchanges). Good for request-reply patterns. Lower throughput than Kafka but richer routing features.


    **Amazon SQS/SNS:** Fully managed. SQS for request-reply, SNS for pub/sub. No infrastructure to manage. Good for AWS-native applications.


    **Google Cloud Pub/Sub:** Fully managed, global. Good for Google Cloud-native applications.


    Advanced Patterns


    **Dead letter queues.** When a message cannot be processed (after retries), move it to a dead letter queue for manual inspection. Prevents poison messages from blocking the main queue.


    **Message bridging.** Forward messages between different messaging systems. For example, consume from SQS and publish to Kafka for long-term storage.


    **Publisher confirms.** In Pub/Sub, some brokers support acknowledgments from publishers to confirm the message was received. Use this for at-least-once delivery guarantees.


    Common Pitfalls


    **Message ordering.** Distributed messaging systems rarely guarantee total order. Kafka guarantees order within a partition, not across partitions. Design your system to handle out-of-order messages.


    **Idempotency.** Messages may be delivered more than once. Ensure your message handlers are idempotent. Use idempotency keys or deduplication.


    **Monitoring.** Monitor queue depth, consumer lag, and processing time. Set up alerts for growing backlog.


    Summary


    Pub/Sub and Request-Reply serve different purposes. Use Pub/Sub when you need to notify multiple consumers of an event. Use Request-Reply when a service needs a response. Many systems use both patterns together. Choose your message broker based on throughput requirements, routing complexity, and operational preferences. Always design for idempotency and handle duplicate messages gracefully.