The Art of Microservices Integration Using Service Choreography

This is the first post in a three part series looking at the topic of microservice integration. In this first installment, I’ll be focusing mainly on the theory side of event-driven service choreography. In the second post, I’ll cover the implementation traits required to satisfy the theory discussed in this first post, and, in final post, I’ll be assessing the support available for those traits in well known implementation technologies. So, let’s get on with part one!

Looking back

One of the biggest shortcomings of traditional SOA is/was the tendency to break up a highly-coupled monolith into a series of smaller services with the same level of coupling that was previously internal to the monolith. The likely result being a distributed monolith with all the same problems you had before, but now with an additional operational burden — you end up in a worse position than if you’d just stuck with the monolith!

Looking forward

So, enter the world of microservices architecture, and the promise of isolation, autonomy, single-responsibility, encapsulated persistence etc. But, what exactly allows one to achieve such isolation and autonomy?

Service orchestration vs service choreography

In a classic distributed monolith scenario as described above, the prevalent integration technique is likely to involve service orchestration. This is where backend services typically have a high-level of synchronous coupling — i.e. a service is reliant on other services to be operational and working, within a single request/response cycle, in order for it to carry out its own responsibilities. Such real-time dependencies prevent a service from acting autonomously — any failures in its dependencies will inevitably cascade, preventing the service from fulfilling its own responsibility. Here’s a visualisation of service orchestration in action:

Clarifying asynchronous integration in service choreography

I feel there’s some confusion where people refer to asynchronous communication, especially in the field of microservices integration. It’s worth some time to clarify what’s meant in the context of service choreography.

  • Classic MQ Request/Reply — It’s possible using classic MQ technology (e.g. JMS, AMQP) to achieve asynchronous request/reply behaviour. You could pop a message on a queue, and wait for a response on some temporary reply queue. There’s certainly some added decoupling in that the caller needn’t know exactly who will reply, but, like with non-blocking I/O, if this is being done as part of a service handling an incoming request, then, despite the communication with the MQ itself being asynchronous in nature, the service is still not acting autonomously. If a consumer responsible for replying is down, and the call must then timeout, it’s ultimately no different to an HTTP endpoint being unavailable or failing.

End-to-end autonomy

Where I’ve covered isolation and autonomy in this post, I’ve been referring to runtime autonomy. It’s worth noting that a strong motivation for isolating services is the autonomy they additionally afford throughout the entire engineering process. The event-driven integration nature of service choreography sits very naturally with the desire to assign clear ownership to specific teams, enabling them to develop, build, test and release independently. Whilst there are techniques to support these things alongside service orchestration, I find it much easier to reason about isolation and independence when service dependencies are largely confined to the background.

Added complexity?

Service choreography, without doubt, introduces a level of technical complexity beyond orchestration. Rather than the apparent simplicity of making calls to dependencies in real-time, you need to both produce events yourself and consume events from others; it requires a fundamental shift in the way you build software. It exposes you to such challenges as guaranteeing at-least-once-delivery/processing of events, handling out of order events, ensuring idempotency in event consumers, factoring in eventual consistency in the UI etc.

Touching on boundaries

Whilst service choreography enables autonomy, the elephant in the room is that a dependency is still a dependency, whether it be event-driven or not. If a service is required to consume from a large number of event streams, it’s still adding overhead in terms of managing the dependencies over time.

Coming next

That wraps up part one of this three part series. I’ll be following up soon with part two within which I’ll cover the implementation traits required to satisfy the theory discussed in this first post. Stay tuned!

Software engineering nut. Cyclist. Musician. Dog lover

Software engineering nut. Cyclist. Musician. Dog lover