For some, the ideal picture of a modern application is a collection of microservices that stand alone. The design isolates each service with a unique set of messages and operations. They have a discrete code base, an independent release schedule, and no overlapping dependencies.
As far as I know, this type of system is rare, if it exists at all. It might seem ideal from an architectural perspective, but clients might not feel that way. There’s no guarantee that an application made up of independently developed services will share a cohesive API. Regardless of how you think about Microservices vs. SOA, services should share a standard grammar and microservices communication is not always a design flaw.
The fact is, in most systems you need to share data to a certain degree. In an online store, billing and authentication services need user profile data. The order entry and portfolio services in an online trading system both need market data. Without some degree of sharing, you end up duplicating data and effort. This creates a risk of race conditions and data consistency issues.
At the same time, how do you share data without building a distributed monolithic service instead of a micro? What’s the effective and safe way to implement microservice communication? Let’s take a look at a few different mechanisms.
First, we’ll go over different sharing scenarios. Depending on how you use the data, you can share it via events, feeds, or request/response mechanisms. We’ll take a look at the implications of each scenario.
Then, we’ll cover several different mechanisms for microservice communication, along with an overview of how to use them.
What data do your services need to share? How often do you update that shared data? Is one service the primary provider of the data? Does the nature of the data suggest the creation of a dedicated facility to share it? Before deciding how to share data, it’s essential to identify the information you’ll share and how each service will use it.
You need to ensure that sharing data does not result in tightly-coupled services. It’s one thing to have microservices that communicate with each other. It’s another to build a distributed monolithic service.
Microservices are often developed by different teams, and the teams need to communicate if the services are going to share data. A system such as domain-driven design can often help with defining boundaries that make sense at a business level.
There are many different ways to slice up and categorize interactions between services. Let’s break them down into three broad categories.
Sometimes the shared data is a stream of updated or new records. For example, capital markets are often represented as a stream of prices and transactions. Another example is a feed of orders for an e-commerce business. A third example is a stream of inventory changes.
Services most often consume data feeds as a “real-time” stream of records. A stream requires clients to cache the data that they are interested in, which can be expensive. However, a significant advantage of feeds is that the client and server are very loosely coupled. They only need to share data types and message formats.
An alternative to a stream is request-response messaging. When a service needs external data, it requests it from the data provider. By requesting the data when the service needs it, the need for a cache is eliminated. But, it adds latency to transactions that need shared information. The advantage of using a smaller cache is often greater than that latency, though.
Depending on how you implement it, request-response can create a tight coupling between data clients and servers. In addition to sharing data formats, they may need to communicate directly with each other.
Domain events are similar to a data feed, but with additional application-specific semantics. They represent a change in application state. Some services process the events, depending on the context of the event and the role of the receiver. For example, a sale in an e-commerce application consists of several steps, each of which may result in an event. So, you can break a sale down into steps.
- Add the item to a cart
- Input a coupon code
- Select a shipper
- Submit a credit card transaction to a payment processor.
Services publish domain events to an event log. Clients retrieve the events via request/response semantics. So, services consume them as both a data feed and with request-response messages. Domain events deserve special note since they reflect careful planning and design of how data is shared between services.
Mapping out how the services need to share data makes selecting the sharing mechanism is easier. Now that we have an idea of how to share data, let’s take a look at three of the most common paradigms.
Messaging is a common choice for sharing data between services. Applications exchange messages, typically via a message broker. The broker routes messages to interested parties using topics and queues. Some common systems are RabbitMQ, Kafka, and ActiveMQ. Most systems offer both publish-subscribe and point-to-point message semantics. So, services can use them to implement all three of the scenarios we covered above.
A messaging system solves several design problems at once. Services can be anywhere in relation to each other, simplifying the use of containers and cloud instances. Since data providers and clients connect to a message broker and exchange data over topics, you don’t need service discovery or orchestration. Most messaging APIs also offer asynchronous interfaces, which many application developers prefer. The messaging system also acts as a natural demarcation point. Applications connect to it and need only share messaging formats.
Publish-subscribe messaging is an efficient choice for data feed sharing. Publishers broadcast feed updates once, and the messaging system delivers the data to all interested parties. Topics are an effective way to segment data, making it easy for clients to limit the updates they receive to the data they need.
Messaging systems also support point-to-point messaging, and guaranteed message delivery. So, you can use messaging to implement request-response semantics and ensuring that critical messages are always delivered.
Sharing Data Stores
Another way to share data is to share a data store. Services can share a relational database, NoSQL store, or another data storage service. One or more services publish the data to the database, and other services consume it when required.
Most databases and data stores provide data via request/response mechanisms. A client application connects to the store and queries for the data it needs when it needs it. However, many stores also provide asynchronous mechanisms such as triggers or publish-subscribe interfaces. However, applications that shared a data store tend to be more tightly coupled than when they use messaging, since they need to share more than just message formats.
Sharing data stores is a valid strategy, but only if the design takes into account the business context and not technical concerns.
Many architects and developers bristle at the idea of using REST, or any HTTP-based mechanism, for microservices communication. Web services have several disadvantages, but there are circumstances where they make sense.
RESTful services are easy to create and maintain. In circumstances where both external clients and internal services need to access the same information, “stacking” APIs makes sense. Reusing interfaces avoids duplication. Even if you don’t make the service available to external clients, you can reuse the data model, which means higher efficiency.
But REST is not without risk. While creating a RESTful service is easy, implementing a robust one can be difficult. Any service that sits at the center of a system needs to be reliable and resilient. Implementing a fault tolerant REST service requires more effort than for messaging. Since RESTful services are synchronous, they are not suitable for sharing data when low-latency is essential.
Planning is Key for Microservices Communication
Hopefully, now you have a better understanding of how services share data. Developers and architects have a wealth of options for microservices communications. The challenge is to ensure that services remain sufficiently isolated and not tightly-coupled. Careful planning and analysis of how the data sharing needs is a critical first step.
This post was written by Eric Goebelbecker. Eric has worked in the financial markets in New York City for 25 years, developing infrastructure for market data and financial information exchange (FIX) protocol networks. He loves to talk about what makes teams effective (or not so effective!)