Reinventing the wheel

by Bartoval

Rhea: Exploring Common Messaging Designs (pt. 4)

Patterns for Reliability

Consider the complexity behind seemingly simple actions, like getting a letter delivered. It requires a system that can handle sorting, routing, potential delays, and ensuring the letter reaches the correct mailbox, even if the path is indirect.

Similarly, in the world of digital communication, particularly in distributed systems, ensuring messages reliably travel from one application to another involves applying established design principles. These are often referred to as messaging patterns. Today, we’ll look at some of these patterns and see how the Rhea AMQP client helps developers utilize them to build robust communication flows.

These patterns represent common, effective solutions to recurring challenges in building systems where different parts need to communicate asynchronously.

Managing Conversations: The Request-Response Pattern

A basic form of communication is asking a question and getting an answer. In distributed systems, this becomes more involved when many requests and responses are flying around simultaneously.

The Challenge of Matching

Imagine being in a busy café where several customers are placing orders and receiving their items. If you ask, “Is my latte ready?”, how do you ensure the response you hear (“Yes, it’s here!”) is for your order and not someone else’s?

+-------------+                        +-------------+
|             |  "Order for item X?"  |             |
|   Client    |-----------------------→|   Server    |
|             |                        |             |
|             |                        |             |
|             |  "Response for item X" |             |
|             |←-----------------------|             |
+-------------+                        +-------------+

A standard way to handle this in messaging is the Correlation Identifier pattern. When a request message is sent, it includes a unique ID. The response message then includes the same ID, allowing the original requester to match the incoming response to their outgoing request. It’s like giving each order a number and announcing the number when it’s ready.

Using Rhea and AMQP, a request might look like this:

Request message properties:
{
  message_id: "query-abc-789", // Unique ID for this specific request
  reply_to: "client-responses-queue", // Where the response should be sent
  body: { action: "get-price", item: "sandwich" }
}

The server processes the request and sends a response message:

Response message properties:
{
  correlation_id: "query-abc-789", // Copies the message_id from the request
  to: "client-responses-queue", // Uses the reply_to from the request
  body: { price: 7.50 }
}

This pattern allows multiple request-response conversations to happen concurrently over the same channels without their messages getting mixed up. It provides a way to link related messages in a stateless manner from the server’s perspective.

Directing Messages: The Selective Consumer Pattern

Consider a large café with different stations: one for hot drinks, another for cold drinks, and one for food. How do incoming orders get to the right station without confusion?

                     +-------------+
                  +->| Hot Drinks  |
                  |  +-------------+
+------------+    |
| All Orders |--->|  +-------------+
+------------+    +->| Cold Drinks |
                  |  +-------------+
                  |
                  |  +-------------+
                  +->| Food Orders |
                     +-------------+

The Selective Consumer pattern addresses this by allowing consumers to specify criteria for the messages they wish to receive from a source. Instead of receiving all messages and filtering them internally, the filtering happens at the messaging infrastructure level.

AMQP brokers and clients like Rhea support this through the use of message selectors or filters when a receiver link is established. A consumer can subscribe to a queue or topic but request only messages that match certain property values.

For example, a service processing only hot drink orders would configure its receiver with a filter:

Receiver configuration including a filter:
{
  source: {
    address: "cafe.orders", // The source of all orders
    filter: {
      "selector": "item_type = 'hot_drink'" // AMQP filter expression
    }
  }
}

This approach decouples message producers from consumers. Producers don’t need to know about the different types of consumers; they just send messages with relevant properties. Consumers then declare their interest based on these properties.

Sharing the Load: The Competing Consumers Pattern

If order volume at the coffee station suddenly increases, the café might add more baristas to help. How can multiple baristas take orders from the same queue without duplicating effort or missing orders?

                        +-------------+
                     +->| Barista A   |
                     |  +-------------+
+----------------+   |
| Order Queue    |---+  +-------------+
+----------------+   +->| Barista B   |
                     |  +-------------+
                     |
                     |  +-------------+
                     +->| Barista C   |
                        +-------------+

The Competing Consumers pattern allows multiple consumer instances to retrieve and process messages from the same queue. Each message is delivered to only one of the competing consumers. The messaging infrastructure handles distributing messages among the available consumers.

Rhea connects to AMQP brokers that support this pattern via the standard queue model. When multiple receivers attach to the same queue address, the broker will distribute the messages among them, typically in a round-robin fashion or based on consumer capacity.

This pattern is useful for scaling processing capacity. By simply starting more instances of a consumer application (each using a Rhea receiver attached to the same queue), you can increase the rate at which messages are processed without needing complex coordination logic between consumer instances.

Broadcasting Information: The Publisher-Subscriber Pattern

Sometimes, a single piece of information needs to be delivered to multiple interested parties. If the café runs out of milk, all baristas need to know immediately.

The Publisher-Subscriber pattern enables one-to-many communication. A publisher sends a message on a specific topic or subject, and all subscribers who are interested in that topic receive a copy of the message.

                     +-------------+
                  +->| Barista 1   | (Subscribed to 'milk-low')
                  |  +-------------+
+------------+    |
| Manager    |--->|  +-------------+
| (Publisher)|    +->| Barista 2   | (Subscribed to 'milk-low')
+------------+    |  +-------------+
                  |
                  |  +-------------+
                  +->| Stock Clerk | (Subscribed to 'inventory-low')
                     +-------------+

Rhea supports this by connecting to AMQP brokers that implement exchanges, particularly topic exchanges. Publishers send messages to an exchange with a routing key (like a category), and subscribers bind queues to the exchange using routing patterns that match the keys they are interested in.

A barista’s station might subscribe to inventory notices specifically about dairy:

// Example simplified concept - specific binding depends on broker config
receiver.on("message", (context) => {
  /* handle milk low message */
});
connection.open_receiver({
  source: {
    address: "inventory_exchange",
    durable: false,
    capabilities: ["topic"],
  },
  filter: { selector: "subject LIKE 'inventory.dairy.%'" }, // Interested in dairy inventory topics
});

Publishers using this pattern do not need to know who is listening, only the topic of the message. Subscribers similarly do not need to know who is publishing, only the topics they want to receive. This results in loose coupling between message producers and consumers.

Handling Unprocessable Messages: The Dead Letter Pattern

What happens to an order that cannot be fulfilled – perhaps the requested item is permanently out of stock, or the order details are corrupted? Simply discarding it isn’t ideal; it might contain valuable information about a problem.

The Dead Letter pattern provides a mechanism for handling messages that cannot be delivered or processed successfully after a certain number of attempts or within a time limit. These messages are moved to a dedicated “dead letter queue.”

                                 +----------------+
                      +--------->|                |
                      |          | Dead Letter    |
                      |          | Queue          |
                      |          |                |
                      |          +----------------+
+--------------+      |
|              |      |
|  Main Queue  |------+ (Message cannot be processed)
|              |      |
+--------------+      |        +----------------+
                      |        |                |
                      +------->| Successful     |
                               | Processing     |
                               |                |
                               +----------------+

AMQP supports moving messages to a dead letter queue based on criteria like expiration or negative disposition (e.g., rejecting a message with specific flags). Rhea provides the client-side capability to send dispositions that can trigger this behavior on a compliant broker.

This pattern provides a place to examine messages that failed processing, aiding in debugging and error handling.

Prioritizing Tasks: The Priority Queue Pattern

Not all orders in a café have the same urgency. A rushed commuter might need their coffee faster than someone settled in to read.

The Priority Queue pattern allows messages to be assigned a priority level. The messaging system then delivers messages with higher priority before those with lower priority, even if the lower-priority messages arrived first.

       High Importance +----------------+
       +-------------->|                |
       |               |                |
+------+------+        |                |
|             |        |    Queue       |
| Producer    |        |                |
|             |        |                |
+------+------+        |                |
       |               |                |
       +-------------->|                |
        Low Importance +-------+--------+
                               |
                               | (Consumer pulls based on priority)
                               v
                       +---------------+
                       |   Consumer    |
                       +---------------+

AMQP messages have a standard ‘priority’ field (typically 0-255). Producers can set this value when sending a message. A compliant AMQP broker, when acting as a queue, will then deliver messages to consumers based on this priority, servicing higher priorities first.

Rhea allows setting the priority property on outgoing messages:

sender.send({
  body: "Urgent system alert!",
  priority: 9, // Higher number indicates higher priority
});

This pattern helps ensure that time-sensitive tasks are handled promptly, contributing to application responsiveness and meeting business requirements.

Tracking Changes: The Event Sourcing Pattern (Enabled by Messaging)

How can a café keep a perfect record of everything that happened during the day, not just the final sales total? They could record every single event: “customer A ordered coffee,” “payment received for order A,” “order A picked up,” “received milk delivery,” etc.

This approach, Event Sourcing, captures all changes to application state as a sequence of immutable events. Instead of updating data in place, each transaction generates an event message that is stored in an append-only log (the event store). The current state of the application is then derived by replaying these events.

+----------------+    +-------------------+    +------------------+
| Order Placed   |    | Payment Received  |    | Order Fulfilled  |
| (Event)        |--->| (Event)           |--->| (Event)           |
+----------------+    +-------------------+    +------------------+
         |                       |                       |
         v                       v                       v
+--------------------------------------------------------+
|                       Event Stream (e.g., AMQP Topic)   |
+--------------------------------------------------------+
         ^
         | (Consumers read events to build read models)
+------------------+
| Application State|
| (Derived from   |
| events)          |
+------------------+

While Event Sourcing is a system design pattern broader than just messaging, reliable messaging systems like those supported by Rhea are often used to implement it. Events can be published as messages to a durable topic, and consumers (different parts of the application or other services) can subscribe to these events to update their own state or trigger actions.

Rhea facilitates this by providing reliable delivery guarantees for messages, which is essential for ensuring that all events in the sequence are processed.

What happens if the payment processor connection at the café fails? Constantly trying to connect will just cause delays and use resources. A better approach is to stop trying for a bit, assume it’s down, and maybe try again later.

This concept relates to the Circuit Breaker pattern, which prevents an application from repeatedly trying to access a service that is known to be failing. It’s like an electrical circuit breaker: if overloaded, it trips, preventing current flow. After a timeout, it might allow a single test connection to see if the service has recovered.

While not a direct AMQP pattern implemented within the message flow itself, a messaging client like Rhea incorporates similar robustness in its connection management. When a connection to the broker fails, Rhea implements a reconnection strategy that typically involves exponential backoff. It doesn’t just try to reconnect endlessly; it waits increasing amounts of time between attempts, giving the broker a chance to recover without being overwhelmed by reconnection attempts.

This behavior helps applications using Rhea behave reliably in the face of transient or prolonged network issues or broker downtime.

Bridging Disconnections: The Store-and-Forward Pattern (Utilized by Messaging Infrastructure)

If the network connection between the café and the central ordering system goes down, can they still take orders? Yes, if they can store the orders locally and send them once the connection is restored.

The Store-and-Forward pattern describes a system where messages are stored securely at an intermediate point if the final destination is unreachable, and then forwarded automatically when connectivity is restored.

+----------+           +--------+           +----------+
|          |  Store    |        |  Forward  |          |
| Producer |---------->| Broker |---------->| Consumer |
|          |           |        |           |          |
+----------+           +--------+           +----------+
                           ^
                           |
                      Persistence
                           |
                       +-------+
                       |       |
                       | Disk  |
                       |       |
                       +-------+

Messaging brokers supporting AMQP and used by Rhea clients implement this pattern internally. Messages sent by a Rhea producer can be marked as ‘durable’. A durable message, once accepted by the broker, is typically written to disk. If the consuming application or the network connection to it is temporarily unavailable, the durable message is stored by the broker until the consumer reconnects and is ready to receive it.

Rhea allows producers to specify message durability:

sender.send({
  body: "Important order details",
  durable: true, // Request that the broker persists this message
});

This broker-side pattern, leveraged by Rhea, ensures that messages are not lost due to transient network issues or consumer downtime.

Managing Complex Workflows: The Saga Pattern (Enabled by Messaging)

A complex operation like “Process a Customer Order” involves multiple steps executed by different systems: check inventory, process payment, schedule shipping, update loyalty points. If the shipping step fails, the payment might need to be refunded and inventory restocked.

The Saga pattern is a way to manage distributed transactions that span multiple services. Instead of a single, all-or-nothing transaction (which is hard to achieve across services), a saga is a sequence of local transactions. If a step fails, compensating transactions are executed to undo the changes made by previous steps, aiming to leave the system in a consistent state.

                     +------------------+
                     | Service A        | (Local Transaction 1)
                     +--------+---------+
                              | (Publishes event)
                              v
+--------------------------------------------------------+
|                       Message Broker (Events)           |
+--------------------------------------------------------+
                              | (Consumed by next service)
                              v
                     +------------------+
                     | Service B        | (Local Transaction 2)
                     +--------+---------+
                              | ... and so on

Messaging systems are frequently used to coordinate sagas, either through choreography (each service reacts to events from others) or orchestration (a central service directs participants via messages). Rhea, by providing reliable message sending and receiving capabilities via AMQP, is a suitable tool for building the communication layer required by saga implementations.

This pattern allows developers to manage complexity in distributed workflows while avoiding tightly coupled distributed transactions.

Conclusion: Patterns for Building Reliability

The seemingly complex task of reliable communication in distributed systems becomes more manageable when viewed through the lens of established messaging patterns. Patterns like Correlation Identifier, Selective Consumer, Competing Consumers, Publisher-Subscriber, Dead Letter, Priority Queue, Event Sourcing, Store-and-Forward, and Saga provide proven approaches to common challenges.

The Rhea AMQP client provides the tools to implement and utilize these patterns by interacting with compliant messaging brokers. By understanding these patterns and how they are supported by the underlying protocol and libraries like Rhea, developers can build applications that are not only functional but also robust, scalable, and resilient in the face of the challenges inherent in distributed environments.