In messaging-based distributed systems, we often focus on delivery semantics and the guarantees a system should provide. Many believe this is mostly an issue with asynchronous communication. However, I’d argue that it’s just as important to consider these aspects, even if your system doesn’t use asynchronous communication between services.

Producers

Imagine one of the simplest ways services might communicate with each other. Service A executes a transaction and then, post-commit sends a request to Service B. This approach is inherent “At-Most-Once”. Why? Because even if the transaction in Service A commits successfully, there’s no guarantee that Service B will ever receive the subsequent request—it might or it might not.

Now, if we adjust the process to send the request to Service B before Service A’s transaction completes, we venture into a territory that resembles “Exactly-Once” semantics. However, this isn’t a true “Exactly-Once” guarantee. Let’s term it “Dirty-Exactly-Once” since the receiving system might process the request even if the original transaction in Service A gets rolled back.

To enhance this, we could employ distributed transactions, multi-phase commits, or even sagas. But we should remember that it is another layer of complexity.

Lastly, let’s examine “At-Least-Once” semantics. In an attempt to make synchronous communication more resilient, we might introduce retries for transient failures, like connection disruptions. However, these retries can cause the same message to be delivered multiple times. This behavior is close to the “At-Least-Once” delivery semantics. So when we use that approach we should also ensure that our APIs are idempotent, meaning they can handle repeated requests gracefully.

Consumers

In the scope of asynchronous communication consumer is able to mark message as processed immediately after receiving it. Usually, we will say that it ensures the “At-Most-Once” semantic. Similarly, a service using a REST API might send back an immediate response after getting a request. This means it’s acknowledged the receipt but doesn’t guarantee further successful processing.

The more natural approach is to mark a message as processed at the end of the consumption transaction, in asynchronous systems it reduces the probability that the message will be lost. In a synchronous world, we also use that approach and achieve “Exactly-Once” or “At-Least-Once”, which depends on the retry mechanism on the producer side.

Conclusion

We shouldn’t overlook the complexity of synchronous systems. Similar to asynchronous they have many challenges in correct modeling of communication border cases. Correctly implemented synchronous systems are also rare.

So take the time to dig into the details of your system, whether it’s synchronous or asynchronous, to ensure it’s robust and effective.