Microservice architectures, by their very nature, are all about communication. Front-end clients talk to back-end microservices. Back-end microservices talk to each other. Much like in real life, successful communication depends on everyone speaking the same language. But unlike in real life, this “language”—called, interchangeably, service contract, service interface or service API—must be precise and fully understood by all participants.
In today’s Agile-oriented software development scene, requirements change often. Since microservice development teams work in their own “bubbles,” without being aware of the architecture as a whole, propagating these changes across various service contracts tends to be a daunting task.
The first part of this series introduces service contracts and their underlying communication models. With this foundation in place, the second part will show how to release and deploy service contract changes in a controlled manner, by following several tried-and-tested recipes.
Communication Models and Service Contracts
In the simplest terms, a communication model defines the way in which services exchange messages with one another. Consider these questions:
- Does the sender of a message expect a response (request-response communication) or not (fire-and-forget communication)?
- Does the sender of a request wait for the response before continuing with its own work (synchronous communication) or does the sender continue its work now and will deal with the response later (asynchronous communication)?
- Is a sent message intended for a single recipient (one-to-one communication) or for multiple recipients (one-to-many communication)?
Depending on how you answer these questions, a different communication model arises. For this article, we have selected three of the most common ones: the Request-Response Model, the Command Model and the Publisher-Subscriber Model.
Regardless of the communication model, a service contract must always exist in order to achieve effective communication. By this, we mean that “somebody” should take the time to design, document, implement, and maintain all the interfaces owned by a service.
But which service is the owner of which interface(s) is not always clear at first glance. Is it the producer of messages? The consumer? As it turns out, it depends. In any case, the ownership of a service contract can never be shared; either the producer changes and the consumer adapts, or vice versa.
The Request-Response Model
As the name itself indicates, a service (the client) sends a request to another service (the server) and expects to receive a response.
The Request-Response Model is traditionally synchronous. The client waits for the server’s response in order to perform its own logic, within the limits of a well-defined time-out period.
The Request-Response Model is always one-to-one. Every instance of communication happens between one client and one server. However, the server can respond to requests from multiple clients at the same time.
The most common way to implement the Request-Response Model is by making use of the HTTP protocol. Not every HTTP API is RESTful, but for the sake of simplicity, let us assume that the service contract in a Request-Response implementation is a REST API.
A REST API consists of the following:
- Base URL of the server
- HTTP endpoints
- Allowed HTTP methods
- Supported query parameters
- Request headers and body
- Response headers, body and status codes
Who owns the contract?
While both the client and the server produce and consume messages, their role in the exchange is quite different. Consider the following:
- The server implements the business logic, while the clients only consume it.
- We often have more than one client, but there is always a single server.
- A server can run without its clients, but the clients cannot run if the server is down.
As the clients always depend on the server, the server must own the REST API.
The Command Model
This communication model is very straightforward. One service (the producer) sends a message (the command) to another service (the consumer or the command processor) in order to trigger a desired behavior.
The communication is of the fire-and-forget variety, i.e. the producer “fires away the message” and then “forgets” about it. No response is expected or needed, but the processor must guarantee that each received command will eventually be processed according to the contract specification. The producers shouldn’t make any other assumptions about how or when the processing will take place.
Command processing errors can occur for many reasons, so having a well-rounded error handling strategy is mandatory. Message replay and dead-letter queues are commonly used techniques.
The communication is one-to-one; every command is sent by one producer and is processed by exactly one command processor. At any given moment, several producers could be sending commands to a single command processor and the messages may arrive faster than the processor is able to handle. To prevent message loss, an inbound queue should be used; each received command can wait inside the queue until the processor is ready to handle it.
The service contract, which we will call a Command API, consists of:
- The inbound queue identifier
- The format of the processed command types
Ideally, in a microservice architecture, any given command processor should handle a single command type, according to the Single Responsibility Principle. An exception can be made if several command types are closely related, i.e. the processing logic is very similar or the format changes for the same reasons. In this case, the Command API must define a discriminator type field and a format for each type, but all commands will still be sent to the same inbound queue.
Who owns the contract?
So who is the owner of a Command API—is it the producer of commands or is it the command processor?
The situation is similar to that of the server being the owner of a REST API:
- The command processor implements the business logic, while the command producers only trigger it.
- We often have more than one command producer, but there is always a single command processor.
- A command processor can run without command producers, but the producers cannot run if the command processor is down.
As the command producers always depend on the command processor, the processor must own the Command API.
The Publisher-Subscriber Model
A single service (the publisher) emits events which are published to a topic. Other services (called subscribers) register themselves to the topic in order to receive these events. Every time an event is published, the topic automatically forwards it to all the subscribers. If there are no registered subscribers, no one will receive that particular event.
Similar to the Command Model, the communication is fire-and-forget. No response is expected; the publisher does not care what services, if any, receive the published event.
Publisher-Subscriber communication is one-to-many. An event emitted by one publisher is potentially received by many subscribers. Each type of subscriber processes the event in a different way, according to its needs.
Just like we saw with the Command Model, to avoid the loss of messages, each subscriber should employ its own subscription queue, where the received events can safely wait for their turn to be processed by that particular subscriber.
The service contract, called an Event API, consists of:
- The topic identifier
- The format of all emitted events
Similar to a Command API, an Event API should emit events of a single type or, at most, multiple event types that are closely related—i.e. the format changes for the same reasons. In the latter case, the Event API must define a discriminator type field and a format for each type, but all event types are published to the same topic.
Who owns the contract?
So who is the owner of an Event API, the publisher who produces events or the subscriber who consumes them?
There are potentially many event subscribers, but only one event publisher. Also, there is a very strong subordination relationship between the subscribers and the publisher; in most cases, the only role of the subscriber is to handle the published events.
Since the subscribers always depend on the publisher, the publisher must own the Event API.
Notice that the owner of the Publisher-Subscriber contract is the producer of messages, while the owner of the Command contract is the consumer of messages. Contract ownership is not dictated by the message flow, but rather by the semantics of the communication.
Communication Models in Action
A service contract will usually correspond to a single communication model, but this is not always the case. Depending on the architecture, some services will expose no API at all, while others will own multiple API types. Regardless of its exposed APIs, a service can consume any number of contracts owned by other services, or none at all.
We will illustrate this with an example. The diagram below shows the (simplified) architecture of an accommodation booking platform.
Let’s take a closer look at some of the services above:
- The Invoice service provides no API. It does, however, consume two service contracts: the Event API of the Payment Gateway service, to receive events like “payment finalized,” and the Command API of Email Sender, to send emails containing the payment invoice.
- The User service provides only a REST API. Frontend clients can use this API to create and manage user accounts, while other services (Messaging, User Alerts) can use it to retrieve relevant data, such as a user’s name and email. The User service itself consumes no APIs.
- The Email Sender and SMS Sender services provide only a Command API each. Other services (Invoice and User Notification) can use these APIs to trigger the sending of emails and text messages. The Sender services themselves don’t use other contracts.
- The Booking service provides both a REST and an Event API. Frontend clients can use the REST API to create and manage bookings, while other services (User Alerts and User Notification) can subscribe to the Event API in order to process booking state changes. For example, when receiving a “booking canceled” event, User Notification could send notifications (email and/or SMS) to the affected parties. When receiving the same event, User Alerts could notify certain users that a property has become available on their desired dates. At the same time, the Booking service consumes the Payment Gateway contract: it initiates payments using the REST API and it processes payment events in order to update the booking state.
Architecture diagrams, such as this one, are good for showing how data flows between services. However, when dealing with service contract changes, it’s much more important to have a clear picture of how services depend on one another. Let’s look at the dependency diagram for the same booking platform.
Notice how, in all Event APIs, data flows from the publisher to the subscribers (e.g. from Payment Gateway to Booking and Invoice), but it’s the subscribers that depend on the publisher—in this case, the direction is reversed. As a rule, the contract consumer will always depend on the contract owner, regardless of the direction in which the data passes between the two.
In part two, we will discuss handling service contract changes.