Learning about Reactive Messaging Patterns

Reading Time: 4 minutes


According to the Reactive Manifesto, a critical element in any Reactive system is that it is message-driven. But what does it mean to be message-driven?

Message-driven systems are those that communicate primarily through asynchronous and non-blocking messages. Messages enable us to build systems that are both resilient, and elastic, and therefore responsive under a variety of situations.

Message Driven Architecture

We have various ways of accomplishing synchronous and asynchronous messaging. But when we try to use synchronous messages to accomplish the task where an asynchronous message is required or vice-versa, things break down. Thus we need to make sure that we’re using the right tool for the job.

Usage of Asynchronous Messaging

Asynchronous messaging is used when the messages can be sent without waiting for a response. It is used to avoid the situation where two parts of the system are waiting on each other, and consuming resources the entire time. Using asynchronous and non-blocking messages, resources like threads, memory, etc. can be freed immediately, and contention is reduced, which in turn improves scalability. It provides a higher rate of reliability because messages can be queued for delivery in case the receiver is off-line. It also allows us to have portions of the system go off-line for sometimes, and have the remainder of the system to continue to operate as normal.

Cost of Asynchronous Messaging

Asynchronous messages make transactions more difficult because transaction remains open potentially for a long period, which makes systems very slow and brittle. Using transactions that cross micro-service boundaries should be avoided because, some services may be unavailable, or multiple databases may be involved, and long-running transactions increase contention.

Usage of Synchronous Messaging

Asynchronous messages should be the backbone of Reactive Systems. To achieve this, synchronous messages can also be used but their requirements can often be relaxed. So rather than sending a message and waiting for a response, we might just send a message and have the receiver acknowledge that they received the message.

Cost of Synchronous Messaging

Technically it’s often very convenient to build using fully synchronous messages. But there are costs associated with it. Those costs show up in the forms of inability to scale and reduced reliability.

So our goal should be to understand the difference between synchronous and asynchronous messaging and the consequences of choosing between the two.

Guaranteeing Delivery of Messages

As we build out our distributed systems, we are forced to confront the reality that delivering messages in a distributed system is complicated. We need to be careful to ensure guaranteed delivery of messages, that means, a message should be delivered to the correct recipient, the expected number of times. To provide these guarantees, we need new tools and techniques.

At Most Once Delivery

At Most Once delivery promises that no message will ever be delivered more than once. If a failure occurs, message delivery is never retried, which means that messages are never duplicated, but they could be lost. The advantage of this delivery technique is that it is very easy to implement, and does not require the storage of messages.

At Least Once Delivery

At Least Once delivery guarantees that all messages will eventually be delivered. When a failure occurs, there are two possibilities. One possibility is that the message may not have been delivered. The other possibility is that the message may have been delivered but not acknowledged. Failure always results in a retry, which means that messages may be delivered more than once, but they’re never lost. This delivery technique requires the storage of messages at the sender’s side.

Exactly Once Delivery

Exactly Once delivery is not easily achievable because, in the event of a network partition or a lost message, we can’t guarantee whether our message is received. When a failure occurs, we have to resend the message to ensure guaranteed delivery, but this could create potential duplicates. Thus to simulate Exactly Once delivery, At Least Once delivery technique is used, and deduplication of messages is performed at the receiver’s end. This delivery technique requires storage on both the sender and the receiver.

Implementation of Guaranteed Delivery Techniques

Akka uses At Most Once delivery by default. However, Akka persistence has an option for At Least Once delivery. Whereas Lagom supports both At Least Once delivery and At Most Once delivery through its message broker API.

To implement guaranteed delivery techniques, there are two distinct approaches. One approach is, each microservice can depend directly on other microservices, which means sending messages in a Point-to-Point fashion. The other approach is, each microservice can leverage Publish/Subscribe message broker to get messages from other services.

Point-to-Point Approach

In a Point-to-Point setup, each service sends messages directly to other services, which means they are directly coupled to each other’s API. The advantage of this approach is that services know about their dependencies, which allows an easy understanding of the flow of messages. This advantage also acts as a disadvantage, as services are highly coupled with each other.

Publish/Subscribe Approach

In a Publish/Subscribe setup, we have a series of microservices and a central message bus or message broker. Services publish messages to the common message bus, and then other services subscribe to those messages. This means that the publishing service does not know about the subscribing service, and vice-versa. Thus the advantage of this approach is that dependencies are much more flexible, which makes this a very decoupled approach.

Akka actors are typically implemented with Point-to-Point messaging. Lagom also supports Point-to-Point communication between services. Internal to our microservice, we have the Publish/Subscribe mechanism, and to use that, we can use Akka Persistence.