In most cases, we will our applications to process a sequence of messages in the order that they arrive. This is especially the case if and when we need to scale out our message processors to handle any increased load in messages.
This isn’t always an easy task, because as our application scales out, instances can often pull messages from the queue independently, similar to how this happens when implementing the Competing Consumers pattern.
For example, you may have an e-commerce application that processes customer orders. Each order goes through several steps, such as validating the order, processing payment from the customer and shipping the order. Each step must be processed in this specific order, and the services performing each step needs to keep track of which order it is processing. It must be able to do this without blocking the processing of other orders that are made.
This is where the Sequential Convoy pattern comes into play. With this pattern, we can process a set of related messages in a defined order, without blocking the processing of other groups of messages that our application needs to process.
How does the Sequential Convoy pattern work?
Using our e-commerce application example from before, we use the Sequential Convoy pattern to push messages related to our categories within the queuing system, and pull only from one category, one message at a time.
In our e-commerce application, orders made in our application would be our categories, and messages representing actions made on our orders would be pulled from the queue within that group.
In Azure Service Bus, we can use Sessions and give our messages a SessionId to correlate our messages. For example, order 1 is made, and the order gets validated, payment is processed, and then shipped to the customer. Each message needs to be processed in order in a First-In-First-Out (FIFO) manner, but only at the order level.
When we send each message, we can define a SessionId property to the message to indicate that these messages are correlated to a specific order. Using Azure Functions or Azure Logic Apps, we consume messages correlated with the same SessionId to process each message in the order that it’s received.
Orders are processed in a transaction which will never span multiple orders. Each consumer can processes orders in parallel, but in a FIFO manner within a order.
Let’s take a look at how this might look in Azure:
We have a ledge processor that fans out messages by de-batching the content of each message in the first queue. The ledger processor Function takes care of processing the ledger one transaction at a time. It then sets the SessionId of the message to match the order ID, so the broker knows that these messages need to be processed within the same batch. It then sends each transaction to our transactions queue with the SessionId set to the order ID.
Each consumer Function will listen to the Transaction queue where each matching order ID message will be processed in the queue using peek-lock mode.
What should we keep in mind when using the Sequential Convoy pattern?
Scaling can be an issue here, since the ledger queue will be the primary bottleneck. Use a property of your incoming message to scale out on. In the e-commerce example, this could be the order ID. Your required throughput should also be taken into account.
As you scale, how you add categories to the convoy will also be a factor to consider. You may need to consider adding ledger processors to accommodate for new categories that you add to the system.
Finally, consider the message broker that you’ll be using. If it allows for one-at-a-time processing of messages within a category, great! If not, you may need to consider alternatives. Regardless of message broker, you may receive messages that are out of order due to network latency. You may have to implement a flag within your last message to indicate that it is indeed the last message to process. Sequence numbers may also need to be considered if you have multiple messages as part of a transaction.
When should use the Sequential Convoy pattern and when shouldn’t we?
If you have messages that arrive in an order and MUST be processed within that order, or your incoming messages can be categorized in a way that it becomes a unit of scale for the system, then the Sequential Convoy pattern is an effective pattern for handling those messages.
This pattern is not suitable for scenarios where you have extremely high throughput scenarios (such as processing millions of messages per second or minute). This is because the FIFO requirements of Sequential Convoys limits the scaling of your system.
In this article, we discussed the Sequential Convoy pattern, how it works, what we should keep in mind when using this pattern, and when we should and shouln’t use it.
If you want to read more about this pattern, check out the following resources:
- Azure Architecture doc on the Sequential Convoy pattern
- Send related messages in order by using a sequential convoy in Azure Logic Apps with Azure Service Bus
- Message Sessions in Service Bus
- Peek-Lock Message (Non-Destructive Read)
If you have any questions, feel free to reach out to me on X/Twitter @willvelida
Until next time, Happy coding! 🤓🖥️