Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
What’s the asynchronous request-response pattern for? We’re used to synchronous communication. You make a request to another service and get a response. When you move to asynchronous communication, you often think of messages as fire-and-forget. You send a command to a queue, and the consumer handles it asynchronously. Because of the temporal decoupling, you don’t know when the message was processed or what the result was. However, there is a solution! Let me explain how you can use the request-response pattern to reply sender once a message is processed.
Check out my YouTube channel where I post all kinds of content that accompanies my posts including this video showing everything that is in this post.
Most developers are familiar with blocking synchronous calls. If you make an HTTP call to a service, you’re making a blocking synchronous call.
When Service A makes a blocking synchronous call to Service B, it must wait to get the response (or acknowledgment) that the request is completed. There are many situations where this is entirely appropriate, such as in client or UI code. UI code reaching out to get data from a service is naturally blocking request-response.
However, service to service communication using blocking synchronous calls has complexity. As an example, if an order is placed within the Sales service, it makes a blocking synchronous call to the Billing service to charge the customer’s credit card or create an invoice.
Once that blocking call succeeds, then needs to make a blocking synchronous call to the warehouse service in order to allocate the product and create the shipping label.
This entire process is blocking and is temporally coupled. Both Billing and Warehouse services need to be available in order for the Sales service to place an order. Also since we lack a distributed transaction, we can be left with an inconsistent expected state if there is a failure. If the request to Billing was successful, but the request to the Warehouse failed, we now need to develop some resiliency to make another call back to the Billing service to refund the order. But what if that request fails? This isn’t resilient at all.
One solution to this problem is to remove the temporal coupling by using asynchronous request-response. This way each service can operate independently without requiring the other service to be available and able to process requests. To do this we can leverage asynchronous messaging and also apply the request-response pattern.
Here’s how asynchronous request-response works. First, the producer will send a message to a queue.
The consumer will process that message asynchronously.
Once the consumer has processed the message, it will create a reply message and send it to an entirely different queue.
The producer will then consume the reply message from the reply queue.
So now let’s jump back to the example of Sales, Billing, and Warehouse services. Using asynchronous messaging and the request-response pattern, we remove the temporal coupling and have each service work independently.
When an order is placed in the sales service, we’ll have an orchestrator that will send a Bill Order command to queue on our message broker.
Once Billing consumes that message, it will then send an Order Billed reply message that the Orchestrator will consume.
Once the Orchestrator consumes the Order Billed reply message, it will then create and send a Create Shipping Label command that the Warehouse service will consume.
Once the Warehouse has processed the message it will create a Shipping Label Created reply message that the Orchestrator will consume.
Once the Orchestrator has processed the reply, it changes the status of the order that was placed to Ready To Ship.
Here’s what our Orchestrator (NServiceBus Saga) looks like. It’s handling the appropriate reply messages and sending new commands (messages) to the broker as an asynchronous workflow.
Because all of this work is done using non-blocking asynchronous messaging, none of the services must available when an order is placed. They are all working independently processing messages in isolation.
The asynchronous request-response pattern allows you to tell a sender that the message has been processed and what the outcome or result was. You can leverage this to then build workflows to involve many different services all in a non-blocking way.
Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design. As well as access to source code for any working demo application that I post on my blog or YouTube. Check out the YouTube Membership or Patreon for more info.