One proposed benefit of following a microservice architecture is that each service can be developed, released, and supported independently. In theory this allows development teams to work with less coordination and less overhead, leading to faster development times. In practice, this is difficult to achieve without some guidelines that make it work.
The book The Tao of Microservices provides two such guidelines — transport independence, and pattern matching — that create an environment allowing you to compose services. Service composition is the holy grail of microservices, allowing you to build progressively more complex functionality safely and efficiently. This post discusses how transport independence and pattern matching allow you to compose services, what service composition means in terms of functions, and how you can use this composition to build complex services additively.
First, some definitions from The Tao of Microservices.
Transport independence is the ability to move messages from one microservice to another without requiring microservices to know about each other or how to send messages.
Pattern matching is the ability to route messages based on the data inside the messages. This capability lets you dynamically define the network. It allows you to add and remove, on the fly, microservices that handle special cases, and to do so without affecting existing messages or microservices.
The implication of these guidelines is that services communicate strictly through message passing over well-known interfaces, with pattern matching allowing messages to be routed to the correct service at runtime. Together, this means services can be added and removed from your system dynamically, letting you to rearrange the topology of your services with each deployment.
On the surface, this sounds like a modest benefit that allows teams to deploy and run their services independently and without unnecessary coordination. Although this benefit is important, it is not benefit enough to be worth adopting a microservice architecture with all of its inherent complexity. Yet if you look a little deeper, you can start to see some additional benefits that emerge from the similarities between microservices that follow these guidelines and functions from functional programming. The rest of this post discusses these similarities and their implications on microservice system design.
In mathematics, a function is a relation between a set of inputs and a set of permissible outputs with the property that each input is related to exactly one output.
Another way to look at a function is as a black box that takes an input x, and returns an output f(x).
The black box metaphor has a few interesting features:
- it advertises a clear interface — the function parameters and their data types, and the output and its data type
- it hides internal details of the function — you only need to know the interface
- it provides transport independence — a function doesn’t care how you use it or where.
- functions are composable — function composition allows functions to be chained together to create new functions. For example, f(x) and g(y) can be composed into a new function f(g(y)).
Services built using transport independence and pattern matching are not, strictly speaking, functions, but they can act like them in some key areas. In particular, you can build them with a clearly typed interface, and you should expect to operate them as a black box without needing to understand their internals. By making service interactions transport independent and by routing messages using pattern matching, services can also be made additive, allowing you to change a system by adding new parts to it.
For example, imagine you are developing a comment service that provides creation and retrieval of blog comments. Initially, the comment service simply stores data locally on disk so you can prove out the feature with early adopters.
As you grow this system into a production service, you add a backing data store component.
Lastly, you receive a new feature request to add permission checks to comments, so you introduce a new system call.
This example provides an idealized view of how microservices can be
additive in production — new functionality is added to the system without
making changes to existing services. Transport independence and pattern
matching are the components that make these additive changes possible. The
page view service does not know or care that new functionality
has been added.
Unfortunately, services aren’t exactly like functions, and trying to compose them additively must be done carefully. In particular, services typically have side-effects like updating data in a database or sending an email. I personally believe that the most complex and difficult factor in making microservice architectures work in production is encoded in how you handle this safe composition between services.
To run a microservice architecture successfully you first need a messaging layer that supports transport independence and pattern matching, and then you need methods for composing services that may have side effects. Service composition is is typically done using careful use of retries, implementing business rules that assume failure, or through using known microservice patterns like application events, distributed sagas, or event sourcing that deal with failure. I don’t want to minimize the difficulty of composing services, or hand-wave this away, so let me reiterate. If you run microservices in production, safely composing services is the most difficult problem to solve.
Microservice System Design
What does this mean for microservice system design? First, if you want to fulfill the promise of a microservice architecture, you need a messaging layer that provides transport independence and pattern matching. Together, these qualities make it easier to additively change a system by deploying and removing services in production without affecting other services. Next, you should design your services as closely as possible to mimic individual function calls. This will make it easier to compose services together in production. Lastly, you need some way to compose services that includes handling failure. This problem is one of the core issues in distributed systems design, and understanding the impact of composing services with side-effects is a prerequisite to any successful microservice architecture.