Microservices: Communications And Contracts

Ken Ngo
ELMO Software
Published in
10 min readJul 13, 2023
Photo by Cytonn Photography on Unsplash

In this article, we will discuss about communications between services inside microservices, their contracts and how we could enforce, or respect the contracts.

I. Microservice Communication

Lucas Jellema, in his speech “Event Bus as Backbone for Decoupled Microservice Choreography” (Ref.1), has described microservices as asynchronous microservices, in which:

Services should never call each other, not even through public API, all interactions are through events. (Lucas Jellema, Ref.1)

I really like the idea of asynchronous microservices, I think it is possible with the right design, and in a business model where instant responses are not required. But in the majority of the cases, we still deal with synchronous communication.

  • Synchronous communication happens in real-time, using an API (Ref.2, 4)
  • Asynchronous communication happens via messaging, messages are exchanged over a Message Broker or Message Bus (Ref.8)

Why do microservices favour asynchronous communication?

The answer is Partition Tolerance. Partition tolerance is one of the three factors in the CAP theorem, or Brewer’s theorem, which are Consistency, Availability and Partition Tolerance.

CAP theorem

In distributed architectures, microservices is one, Partition Tolerance is the first priority because we want our distributed system to work even if one or any services go down.

For example, if we have an e-commerce store, customers can browse the categories, choose products, and checkout but the payment service is not working, then customers would not be able to complete their purchases. In that case, we do not have a working store.

Imagine if the checkout and payments communicate asynchronously, customers will be able to complete their purchases, even though the payment will be processed when the payment service is back online.

Port and Adapter Architecture

If we apply the Port and Adapter architecture (Ref.13) to the service level. then each service will have precisely two Driving (Incoming) ports and two Driven (Outgoing) ports: API and Messaging

Port and Adapter Architecture

II. Contracts

1. Synchronous Contracts

Because synchronous communication uses APIs, so basically synchronous contracts are API contracts.

We will discuss some of the most popular styles of API and their contracts

  • REST
  • GraphQL (one-endpoint API)
  • gRPC (long-lived connection using HTTP/2)

REST API

Rest API uses API specification as the contract

API specification

paths:
/users/{userId}:
get:
summary: Get a user by ID
parameters:
- in: path
name: userId
schema:
type: integer
required: true
description: Numeric ID of the user to get:

GraphQL

GraphQL uses GraphQL schemas as the contract

GraphQL schema

type User {
id: Int
phone: String
email: String
}

gRPC

gRPC uses Protocol buffers (protobufs) as the contracts, senders and recipients must use the same protobufs. These protobufs will be used to generate the classes for both senders and recipients in their desired programming languages.

protobuf

message User {
int64 id = 1;
string phone = 2;
string email = 3;
{

The generated class

The class generated from protobuf file

2. Asynchronous Contracts

Asynchronous communication uses messages, therefore asynchronous contracts are message contracts.

We will discuss some of the popular message formats and their contracts (Ref.5):

  • protobuf
  • JSON
  • XML

Because the same principle would be applicable to JSON and XML, we will only discuss contracts for JSON format.

protobuf messages

Similar to synchronous communication that uses gRPC and protobuf, messages that use protobuf format also requires publishers and consumers to pre-defined the protobuf interfaces and use the generated classes in their desired programming language.

For example the above-generated User class, we could serialize to a protobuf string or JSON string before sending it to a Message Bus

$user = new \Protobuf\Messages\User();
$user
->setName('Ken Ngo')
->setEmail('kenngo@micronative.com');
$user->serializeToString();
$user->serializeToJsonString();

Noting that protobuf string is only 33 characters long while the JSON string is 51 characters long for the same message content.

The only drawback of using protobuf is we have to define all these interfaces in a separate repository which will be pulled to both publishers and consumers, then generate the classes in their desired programming languages.

JSON message

There are two styles of messages:

  • Command Message
  • Event Message

Command Message (Ref.6, 14)

  • Used in Point-to-point delivery pattern
  • Messages are published directly to the recipient’s queues.
  • Senders expect specific behaviour from recipients
  • Usually in an Orchestration architecture
{ 
"service": "NotificationService",
"action": "unsubscribeUser",
"id": "100"
}

A small note about Command Message is that it might require further authentication and authorization on the consumer side because the consumer is asked to perform certain actions.

Event Messages (Ref.7, 14)

  • Used in Pub-sub delivery pattern
  • Messages are published messages to Topics
  • Publishers do not know about the existence of subscribers
  • There might be multiple subscribers that are subscribing to one topic
  • Usually in a Choreography architecture
{ 
"event": "Identity.User.Created",
"payload": {
"id": "100",
"email": "anyuser@gmail.com",
"phone": "0123456789"
}
}

Event Messages provide generic information about an event, they do not target any specific subscribers and it’s up to the subscribers to do (or don’t) whatever they want with the information.

Event data

Typically, the event message should only carry data about the event that happened. When a user is created on the Identity service, an event is published with only data of the User entity

{
"event": "Identity.User.Created",
"payload": {
"id": "100",
"email": "anyuser@gmail.com"
}
}

But, what if a subscriber, for example, a Notification service, wants to access more data of the user and those data are sitting on the Identity service? Should we allow subscribers to send synchronous API to the publishers for more data?

The short answer is no, we should avoid sending synchronous APIs when processing asynchronous messages. Therefore:

  • An event message should carry data on the direct event that happened and any related data that can be accessed on the same service.
  • Data from the direct event is required and will be guaranteed to deliver
  • Related data are optional

So, the message of event Identity.User.Created might look like this

{
"event": "Identity.User.Created",
"payload": {
"id": "100",
"name": "Ken Ngo",
"email": "kenngo@micronative.com"
"settings": {
"email_notification": true,
"phone_notification": false
}
}
}

It carries the data of the new user and the settings of that user. When the Notification service picks up this message, it will search for “email” in user data and “email_notification” in settings to decide whether to add this user to the mailing list or not.

The event data should be self-sufficient (Ref.14, 15)

Event Schema

Michael Bryzek, in his speech “Design Microservice Architectures the Right Way” (Ref.9), has mentioned a very interesting concept called “event schema first” which, I think, should be in every book about microservices.

Why event schema first?

Because in the Pub-sub channel, publishers do not know about subscribers, they only publish events into the topics. Subscribers have no control over the data and format of the messages, they have to work with whatever publishers provide.

  • What if publishers change the data or format of the messages?
  • Then subscribers will not be able to process the messages anymore.

That is why we need an event schema. Event schema is the contract between publishers and subscribers. Once the contract is established, both publishers and subscribers have to follow. Publishers will be able to change the data and format of the messages as long as they do not violate the schema.

Event schema, in my opinion, is the most important, and the first thing we need to design when doing asynchronous messaging.

Below is an example of an event schema (Identity.User.Created.json)

{
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 0,
"maxLength": 256
},
"version": {
"type": [
"string",
"null"
],
"minLength": 0,
"maxLength": 256,
"default": "1.0.0"
},
"payload": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 0,
"maxLength": 256
},
"email": {
"type": "string",
"minLength": 0,
"maxLength": 256
}
},
"required": [
"name",
"email"
],
"additionalProperties": true
},
},
"required": [
"name",
"payload"
],
"additionalProperties": true
}

This is the schema of the event Identity.User.Created which is published by Identity service. The Identity service will use this schema to validate the event message before publishing. By using this schema, the Identity service tells subscribers:

  • The event message has two required properties: “name” and “payload”. There might be other properties in the message.
  • “version” is an optional string, defaulting to “1.0.0”
  • “payload” is an object which has two required properties: “name” and “email”. There might be other properties in the “payload”
  • Required properties are data that is guaranteed to be delivered.

When subscribers listen to this event, they will expect a message like this

{ 
"name":"Identity.User.Created",
"payload":{
"name":"Ken",
"email":"ken@micronative.com"
}
}

There might be more data in the messages but these are the data that the publishers guarantee. So the subscribers know that they should only rely on these data to process the message.

III. How to enforce or respect the contracts

1. Synchronous Contracts

Synchronous contracts, or API contracts, are defined and published by the request consumers (or the API providers), therefore consumers should always be able to enforce the contracts. Of course, they can choose not to respect the contracts if they want.

Because synchronous contracts have been always there, sometimes we take their existence for granted.

2. Asynchronous Contracts

Unlike API contracts, there is no hard enforcement of message contracts on the publisher side, therefore we could only expect publishers to respect the contracts published by themselves.

Protobuf Messages

If using protobuf as a message format, then publishers and consumers share the same message contracts defined by the protobuf interfaces, if they use these interfaces in publishing and consuming then the contracts are automatically respected. That is one of the reasons protobuf has become a popular choice.

JSON Messages

Can we do the same with JSON messages? That means publishers and consumers agree on the event schemas and publish these schemas on an independent repository, then both publishers and consumers pull these schemas in when publishing and consuming.

We can, but still, there is no guarantee that publishers will not violate the contracts. We could only expect publishers to voluntarily respect the contracts.

Therefore, there is no need for publishers and consumers to share the event schemas. In fact, because the events are local to publishers (Ref.14), it is up to them to define the schemas. Publishers then publish these schemas as a notification to any interested consumers. Publishers respect the contracts by validating the message against these schemas before publishing to Message Bus.

Below is an example of events and schema published by the Identity service:

{
"Identity.User.Created": {
"type": "object",
"properties": {
"event": {
"type": "string",
"default": "Identity.User.Created"
},
"payload": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"data": {
"type": "object"
}
},
"required": [
"data"
]
}
},
"required": [
"user"
],
"additionalProperties": true
}
},
"required": [
"event",
"payload"
],
"additionalProperties": true
},
"Identity.User.Updated": {
"type": "object",
"properties": {
"event": {
"type": "string",
"default": "Identity.User.Updated"
},
"payload": {
"type": "object",
"properties": {
"user_id": {
"type": "integer"
},
"user": {
"type": "object",
"properties": {
"data": {
"type": "object"
}
},
"required": [
"data"
]
}
},
"required": [
"user_id",
"user"
],
"additionalProperties": true
}
},
"required": [
"event",
"payload"
],
"additionalProperties": true
}
}

We could embed the event schema into open API spec so it would be viewed in an open API inspector

Do consumers need to respect the same contracts published by publishers?

Consumers could use the same schemas published by publishers to validate the message they receive. But they do not need to, instead, they should use their own schemas targeting their desired piece of data.

For example, the Notification service is listening to the event Identity.User.Created from the Identity service, then it will expect a message from the Identity service

{
"name":"Identity.User.Created",
"payload":{
"name":"Ken",
"email":"ken@micronative.com"
},
"created_at":"2022-09-05 03:37:54"
}

However, the Notification service might only use part of the data, for example, the Notification service only needs the user’s email to add to the mailing list. In order to do so, the Notification service will implement a service schema (Notification.User.Created.json):

{  
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 0,
"maxLength": 256
},
"version": {
"type": [
"string",
"null"
],
"minLength": 0,
"maxLength": 256,
"default": "1.0.0"
},
"payload": {
"type": "object",
"properties": {
"email": {
"type": "string",
"minLength": 0,
"maxLength": 256
}
},
"required": [
"email"
],
"additionalProperties": true
},
"published_by": {
"type": "string",
"default": "UserService"
}
},
"required": [
"name",
"payload"
],
"additionalProperties": true
}

This schema is used to make sure that the input message is in the correct format and has an “email”.

References

  1. Event Bus as Backbone for Decoupled Microservice Choreography
  2. Communication in a microservice architecture
  3. gRPC on HTTP/2 Engineering a Robust, High-performance Protocol
  4. Design interservice communication for microservices
  5. Microservice communication and integration: what are my options?
  6. Command Message
  7. Event Message
  8. Pattern: Messaging
  9. Design Microservice Architectures the Right Way
  10. Introduction to gRPC
  11. GRPC
  12. GraphQL
  13. Hexagonal Architecture, there are always two sides to every story
  14. Designing Events-First Microservices
  15. When is an event not an event?

--

--