A pattern is a reusable solution to a problem that occurs in a particular context. Microservices patterns are high-level design patterns, where the solution consists of collaborating services. The module’s overview is based on chapter 1 of Richardson’s Microservices Patterns [MP].
Each pattern has a standard structure:
Patterns are not silver bullets: every pattern resolves some forces and leaves others unresolved, and introduces new issues of its own. Reading the resulting context is as important as reading the solution.
The patterns are grouped based on the kind of problem they solve:
The catalogue is divided into three layers, according to who is affected:
Patterns that solve problems that are mostly infrastructure issues outside of development — for example the service deployment patterns and most observability plumbing: log aggregation servers, metrics servers, deployment platforms.
Patterns for infrastructure issues that also impact development — the middle layer of the catalogue: service discovery, circuit breaker, API gateway, externalized configuration, and the microservice chassis that packages the cross-cutting machinery.
Patterns that solve problems faced by developers — the top layer: decomposition into services, communication styles, sagas for data consistency, CQRS and API composition for querying, contract and component testing.
The strategies to decompose a system into a set of services were discussed in module 2.2 (Chapter 9):
Both are themselves patterns in the catalogue — and the module points out that they are only the first step: the rest of the catalogue exists because decomposing an application creates a whole family of new problems.
Communication patterns cover a variety of architectural and design decisions about how your services communicate with one another and the outside world. The module groups them into five communication groups:
The first group — communication style — was already covered in Chapter 9 (interaction styles and IPC technologies: REST, gRPC, messaging, GraphQL). The remaining four groups give the questions the patterns answer: discovery (how a client of a service determines the IP address of a service instance so that it can make an HTTP request), reliability (how to keep communication reliable even though services can be unavailable — the Circuit Breaker pattern), transactional messaging (how to integrate the sending of messages and publishing of events with the database transactions that update business data), and external API (how the application’s clients communicate with the services — the API Gateway pattern).
In microservices, to ensure loose coupling, each service has its own database — which poses the problem: how to preserve data consistency across services? The traditional approach of using distributed transactions (two-phase commit) isn’t a viable option for a modern application.
Among the patterns in this group:
Both patterns were introduced in Chapter 7 (domain events and sagas as the aggregate rules’ escape hatch) and in Chapter 9 (sagas as the answer to the cross-service consistency obstacle). Here they return as named entries in the catalogue — and the lab notes on designing event-driven microservices will go much deeper.
Often in microservices, queries need to query and join data owned by multiple services — and we can’t use distributed queries against the services’ databases. Among the patterns:
Deploying a microservices-based application is complex: there may be tens or hundreds of services written in a variety of languages and frameworks, and many more moving parts to manage. The traditional — and often manual — way of deploying applications in a language-specific packaging format (for example WAR files) doesn’t scale to support a microservice architecture: a highly automated deployment infrastructure is needed. Ideally, a deployment platform should provide the developer with a simple UI (command-line or GUI) for deploying and managing their services.
Several patterns exist for deploying microservices:
In any case, a service deployment platform is needed: an automated, self-service platform for deploying and managing services.
A key part of operating an application is monitoring and understanding its runtime behaviour — observability — for diagnosing and troubleshooting problems (failed requests, high latency) and, more generally, for ensuring quality attributes. Observability is more challenging in microservices systems than in monolithic ones: a request can bounce around between multiple services before a response is finally returned to a client, so there isn’t one log file to examine, and latency problems are harder to diagnose because there are multiple suspects.
The patterns to design observable services:
The microservices architecture makes individual services easier to test because they are much smaller than the monolithic application. At the same time, it is important to test that the different services work together — while avoiding complex, slow and brittle end-to-end tests that test multiple services together. The patterns simplify testing by testing services in isolation:
In a microservices architecture there are numerous concerns that every service must implement — including the observability patterns and the discovery patterns. It must also implement the Externalized Configuration pattern, which supplies configuration parameters such as database credentials to a service at runtime.
When developing a new service, it would be far too time-consuming to reimplement these concerns from scratch: a much better approach is to apply the Microservice Chassis pattern and build services on top of a framework that handles these concerns for them.
In a microservices architecture, users are typically authenticated by the API gateway. It must then pass information about the user — such as identity and roles — to the services it invokes. A common solution is the Access Token pattern: the API gateway passes an access token, such as a JWT (JSON Web Token), to the services, which can validate the token and obtain information about the user.
Besides choosing the right architecture, a main concern of software architects is process and organisation: an architecture succeeds only if the teams and the delivery process fit it.
Large teams have communication trouble: Fred Brooks noted in The Mythical Man-Month that the communication overhead of a team of size N is O(N²). If the team gets too large it becomes inefficient — imagine trying to do a daily standup with 20 people. The solution is to refactor a large single team into a team of teams:
Small teams bring two benefits. Higher velocity: the microservices architecture enables the teams to be autonomous — each team can develop, deploy and scale its services without coordinating with others, and it is very clear who to contact when a service isn’t meeting its SLA. Higher scalability of the organisation: we grow the organisation by adding teams; if a team becomes too large, we split it and its associated service or services; because teams are loosely coupled, we can add people without impacting productivity.
Several approaches exist in the literature; the natural choices when developing an application with the microservice architecture are agile development and deployment practices (such as Scrum or Kanban) and DevOps, to practise continuous delivery/deployment. The microservice architecture directly supports them:
The goal of continuous delivery/deployment — and, more generally, of DevOps, “move fast without breaking things” — is to deliver software rapidly yet reliably. Four useful metrics assess software development:
In a traditional organisation the deployment frequency is low and the lead time is high; a DevOps organisation releases frequently, often multiple times per day, with far fewer production issues. The module’s data points: Amazon deployed changes into production every 11.6 seconds in 2014, and Netflix had a lead time of 16 minutes for one software component.
A pattern is a reusable solution to a problem that occurs in a particular context; microservices patterns are high-level design patterns whose solution consists of collaborating services. Structure: Forces (the issues to address), Resulting context (Benefits — resolved forces; Drawbacks — unresolved forces; Issues — new problems introduced), and Related patterns (successor, predecessor, specialization, generalization, grouping).
Groups: decomposition into services, communication, data consistency for transaction management, querying data, service deployment, observability, automated testing of services, cross-cutting concerns, security. Layers: Infrastructure patterns (issues outside development), application infrastructure (infrastructure issues that also impact development), application patterns (problems faced by developers).
Decomposition: by business capabilities and by subdomains (from module 2.2). Communication groups: style, discovery (finding a service instance’s IP address), reliability (Circuit Breaker), transactional messaging (integrating message sending with database transactions), external API (API Gateway).
Each service owns its database, so cross-service consistency cannot rely on distributed transactions (2PC), which are not a viable option for modern applications. Data consistency: Saga (sequence of local transactions coordinated by messaging, eventually consistent) and Event Sourcing (persistence modelled as events). Querying: API composition (invoke several APIs and aggregate) and CQRS (maintain easily queried replicas).
There may be tens or hundreds of services in a variety of languages and frameworks: manual deployment in a language-specific packaging format (e.g. WAR files) doesn’t scale, so a highly automated service deployment platform is needed. Modern approaches: deploy services as VMs or containers, or the serverless approach (upload code, the platform runs it).
A request bounces between multiple services before the response returns, so there isn’t one log file to examine and latency problems have multiple suspects. Patterns: Health check API, Log aggregation, Distributed tracing, Exception tracking, Application metrics, Audit logging.
Consumer-driven contract test — verifies a service meets the expectations of its clients. Consumer-side contract test — verifies the client of a service can communicate with the service. Service component test — tests a service in isolation. Together they replace complex, slow and brittle end-to-end tests.
Externalized Configuration supplies configuration parameters such as database credentials to a service at runtime. Microservice Chassis is the framework on which services are built, handling the cross-cutting concerns every service must implement (observability, discovery, externalized configuration) so they are not reimplemented from scratch.
Users are typically authenticated by the API gateway, which then passes information about the user (identity and roles) to the services it invokes using an access token such as a JWT; each service validates the token and obtains the user information.
Conway’s law: organizations which design systems … are constrained to produce designs which are copies of the communication structures of these organizations. The Reverse Conway Maneuver: design your organization so that its structure mirrors your microservice architecture. Team-of-teams rules: teams of 8–12 people, with a clearly defined business-oriented mission (developing and possibly operating one or more services), cross-functional so they can develop, test and deploy without frequent coordination with other teams.
Continuous delivery: getting changes of all types into production safely and quickly in a sustainable way — software is always releasable. Continuous deployment: automatically deploying releasable code into production. The four metrics: deployment frequency, lead time (check-in to deployment), mean time to recover, change failure rate.