Creating an API can entail having to make synchronous tasks available, i.e. where the caller actively waits for a response before processing can continue.
Asynchronous tasks (fire-and-forget, or making use of a call-back mechanism) are admittedly often presented as the panacea for such situations. But let’s put that aside and concentrate on synchronous tasks, starting from the basis that the need for such tasks is justified – which can be perfectly true!
Decouple, decouple, decouple…
As integration specialists, we advocate decoupling as basic best practice, taking three forms:
- Structural decoupling: introducing a hub structure;
- Time decoupling: using an asynchronous event/message bus;
- Functional decoupling: abstracting the logic of the systems being connected.
This should, in theory, enable us to introduce maximum flexibility and adaptability in the integration layer, and thereby pull off the trick of fitting a square peg into a round hole. These are the decoupling principles found in Messaging patterns.
Synchronous but asynchronous: having your cake and eating it?
Yes, but bear in mind:
- Firstly, messaging services, including Azure Service Bus, are asynchronous in nature. That means systems publish messages or events to it, and they are persistent until other systems consume them;
- Secondly, they work based on queues. Consequently, even if we know exactly which message we want, it is possible (and usually probable) that other messages will be ahead of it in the queue;
- Lastly, they are generally passive components (with very few exceptions, such as Azure EventGrid), meaning that they do not push messages out to be consumed, but the consumers themselves come to see whether new messages are available (i.e. a polling mechanism).
At first glance, these “constraints” seem incompatible with the requirements of an API, which demands acceptable response times. This is also the kind of scenario under which pragmatism is a tempting option, whereby close enough is good enough, perfect is the enemy of good, etc. Thoughts that, although necessary when applying scientific doubt, are too often the precursors to the deliberate introduction of technical debt (for further reading, see Mark Heath’s excellent article: Top 7 Reasons For Introducing Technical Debt) for reasons that are sometimes simply a matter of preconceptions.
But is perfect really the enemy of good? Is the best practice of decoupling, as mentioned above, incompatible with, or inapplicable to, synchronous APIs? Put another way: are we always forced to choose between synchronous and asynchronous?
To establish these ideas, let’s take the practical example of a customer management API exposing a customer creation task. This transaction has to:
- target different back-ends (including CRM) and run different processing depending on the country where the customer is based;
- return certain data, such as the possibility the customer already exists, its golden ID, etc.
Applying integration patterns
Within the Azure ecosystem, messaging scenarios can be addressed by more than one component, the main ones being:
Each has its specific features and favored use cases. Microsoft’s documentation on the subject is fairly clear, so you are advised to read it if you have not already done so.
Returning to the two customer management API responsibilities described above:
- The first point is an application of the Message Router pattern, and as such is a use case of Azure Service Bus topics.
- The second is an application of the Request-Reply pattern, which can be addressed using sessions within Service Bus.
Therefore, Service Bus is my choice as the backbone for this API. The technical architecture is very simple:
- one Azure Function representing the API’s task, which receives the caller’s request and returns a response to the caller;
- one or more Service Bus topics, within which customer creation events and responses will be published;
functions subscribing to events and publishing responses.
The overview diagram is shown below:
It works as follows:
- One Azure Function representing the API’s task receives the caller’s request, then:
- Publishes a customer creation event in a topic, and:
- Specifies the session ID that it is going to poll for the response in the ReplyToSessionId property, and potentially the Service Bus entity in which it is expecting to receive the response in the ReplyTo property;
- Indicates the customer’s country in a custom property in the message.
An example of the code follows:
Message requestMessage = new Message(System.Text.Encoding.UTF8.GetBytes(requestBody));
requestMessage.ReplyTo = responseEntity;
requestMessage.ReplyToSessionId = requestID;
requestMessage.SessionId = requestID;
- It polls on the response topic (which can be the same) by subscribing to the subscription for responses and polling one session in particular, by means of the instruction:
IMessageSession session = await rcvClient.AcceptMessageSessionAsync(requestID);
Message response = await session.ReceiveAsync(TimeSpan.FromSeconds(responseTimeout));
- The event is routed to the right subscription for the country code in question;
- Another function is subscribed to this subscription and therefore consumes the event;
- Once the processing has ended, it publishes the response in a topic, with the value of the ReplyToSessionId property for the creation message being specified in the SessionID;
outputMessage.SessionId = inputMessage.ReplyToSessionId;
- The first function then consumes the response, which may then potentially process it further before sending it to the caller.
And that is all there is to it! The scenario can obviously be expanded to include a sequence of events and further processing, but the basic principle remains as described above.
The price to be paid
It all looks good on paper, but what about actual performances? Response time is a key issue for an API. The latency caused by decoupling using the Service Bus must therefore remain acceptable and under control, because it can vary both over time and with the workload.
I have run a number of performance tests based on a simple Service Bus Standard namespace. My API does not need to exceed 50 requests per second, so I took a few precautions, and multiplied this workload by 10, and tested how it worked with 500 requests per second. The results are shown below:
These results show very consistent levels of performance with a fairly narrow distribution, and latency very much under control not exceeding 200 ms. The tests I was able to run on Premium show no gains as regards these aspects but, in my case, the performance obtained is entirely satisfactory anyway.
NOTE: one might conclude that as Premium delivers no performance benefits, a Standard namespace will suffice. But bear in mind that Standard is not designed for performance. It is intended only to supply Messaging as a Service functionalities. Indeed, it uses shared infrastructure, so the risk of “noisy neighbors” is real, although Microsoft is currently working on putting in place performance smoothing mechanisms (which will certainly entail some form of throttling). In other words, there is no guarantee that the performances described above will be maintained over the long term. Service Bus Premium, meanwhile, is built for performance, including by making use of radically different infrastructure.
My intention here is not to state an absolute rule: it is advisable for readers to learn about these factors and examine how they fit with their requirements. Nevertheless, my personal conclusions are:
- Service Bus is an effective way to implement decoupling, even in low-latency synchronous situations;
- It is always vital to test and confirm your hypotheses;
- Messaging patterns are as valid a choice as ever.
And on that point, I wish you happy messaging! 🙂