In this article I’d like to discuss how service-to-service communication can be simplified by abstracting the communication layer. This is part of a series of articles about how microservice architecture can be applied in a domain centric way without constantly dealing with technical aspects.
If you haven’t done it yet, I recommend checking my other articles in this series.
Request-response
This is also referred to as synchronous communication, it is used when the initiator of the process expects a result back. To take a real word example, phone calls are classified in this category, meaning that, at both ends of the communication channel a participant must exist in order to make the communication happen. Therefore participants are dependent on each other.
The above example pretty much describes the advantages and disadvantages of the pattern. From one hand the service initiating the communication gets an instant feedback, the result of the service call, and can continue executing it’s process using that result. On the other hand the caller’s execution depends on the availability and performance of the responding service.
Because synchronous communication creates coupling between services it’s generally recommended to avoid using it. If you have many synchronous calls between two microservices it’s a sign to reconsider your service boundaries.
Synchronous communication between services can be implemented by using remote procedure calls (RPC), meaning that the caller can invoke a method of another service through a communication channel (e.g. Http).
To better illustrate this, let’s take a fictive example of order validation defined in the Order service, which also has to check the availability of the selected items implemented by the Inventory service.
Abstracting the communication channel at the client side can be achieved by interception using the dynamic proxy by Casstle. It actually allows you to inject the service contract of the remote service as a dependency without having an actual implementation, and you can intercept all the method calls to it, then forwarding the calls through http to the actual service hosting the implementation.
In the next example you can see how the contract of the InventoryService hosted by the Inventory microservice is called from OrderCommandValidator implemented by the Order microservice.
In order to make this possible the Order service must know the base http address of the Inventory service and the definition of the service contract. This can be achieved by using the below project structure for every microservice:
The Inventory.Shared project holds only the information the Inventory service is intending to share with other services, such as definition of events, a part of the domain model and some configuration. Only objects that hold state but not services that implement behavior. So having a reference from the Order service to the Inventory.Shared project, gives it access to the service contract and it’s endpoint configuration.
Here is how the service proxy look like in my implementation:
In the approach I prefer the most, all the synchronous communication between microservices are done via an endpoint called OnRequestReceived, this handles all the remote procedure calls and every micro service exposes it. What the caller has to pass is the assembly qualified type name of the service contract (IInventoryService), the name of the method to be invoked (IsInventoryAvailable) and it’s serialized arguments. These conventions allows to have a generic remote invocation dispatcher at the receiving end:
This approach abstracts the http communication and the involved serialization in a way in which the business layer can consume these without further investment in infrastructure for new services that has to be called remotely, because neither the caller nor the host of the remote service knows about the communication channel involved.
Publish-subscribe
This is also referred to as asynchronous communication. Applies when the initiator of the process isn’t interested in the result of the communication, it just publishes the outcome of it’s execution. As a real word example, think about radio stations where the station isn’t impacted directly by the audience. It can continue emitting the signal without having any receiver (well.. not for long) but can’t receive any feedback through the same communication channel.
The above example emphasizes very well the disadvantage of not being capable of receiving feedback during the communication, however if the business domain can handle this, it brings a couple of major benefits.
Microservices can run without being impacted by the availability and performance of other microservices, making it autonomous even when communicating with other services.
Microservices can publish the result of their execution to multiple subscribers without a performance penalty.
Because of the above, this is the recommended way of handling service to service communication and it fits really well in event driven architecture where the event is the initiator of any process, so that the event is an excellent candidate to be the published message.
If you haven’t done it yet, I recommend checking my article about event driven architecture in microservices.
The publish subscribe pattern can be implemented in an abstract form by having a generic event publisher at the publishing end, accepting the event as a parameter.
The event is then published serialized in Json format, together with the event type needed for deserialization at the receiver end.
Receiving the event consists of materializing the event using the event type sent above and resolving all the event handlers for that event type.
So we have a way to send and receive the events, but still missing the message broker connecting these. Depending on the hosting platform the most common tool is a service-bus, which can connect the publisher through topics to the receivers via subscriptions.
Having a convention for topics and subscriptions can move the responsibility of their creation to a generic component, running at every deployment as part of a CD pipeline.
In my case the convention is the following:
If there is a domain event defined in a microservice, a topic with the name of the microservice is created, unless the topic is explicitly expressed by decorating the event with a PublishDestination attribute.
The above event definition tells the component responsible for creating the topics that a topic with the name Orders (taken from the event namespace), must be created. Then the presence of event handlers determine the subscriptions of that topic.
The above event handler determines the creation of an Inventory subscription under the Order topic, where the subscription name is taken from the event handler’s namespace and the topic name from the event’s namespace.
Having this convention allows to automatically create topics and subscriptions in correlation with the existing event and event handlers.
This abstraction allows the team responsible for implementing business requirements to no longer get distracted by the technical aspect of communication, but instead only implementing events and event handlers as part of the business domain.