Chapter 4. Microservices Architecture
While a cloud-native architecture structures an application to incorporate cloud computing practices, a microservices architecture refines that further to modularize the application and distribute the cloud-native modules across the cloud infrastructure.
Back in the day, all applications were monolithic applications. Developers didn’t need to specify that what they were producing were monoliths, they were just applications. Then with the advent of client/server computing, developers started designing applications with Distributed Architecture, structuring a single application as coordinated parts that could run on different computers. Service-oriented architecture (SOA) evolved to structure the distributed parts as services that could invoke each other to perform functionality. This is when monolithic architecture became one of multiple possibilities: An application could be a monolith or distributed.
With the advent of cloud computing, developers started creating Cloud-Native Applications. Likewise, developers designed the services in service-oriented architecture to run on the cloud by incorporating Cloud-Native Architecture into services, evolving services into miniature applications that became known as microservices.
Introduction to Microservices Architecture
This chapter explains how to build applications that work the way cloud does: small components that can be replicated easily and distributed across infrastructure. Microservices architecture accomplishes this by combining distributed architecture with cloud-native architecture. Each microservice is a component in the distributed architecture that is not only a service but a cloud-native service, one designed to perform a capability in the application’s business domain.
These patterns assume a basic understanding of microservices, so let’s review that first. We’ll take a look at what the industry defines a microservice to be, how a microservices architecture differs from a monolith, and how it relates to cloud-native architecture. We’ll then introduce the patterns and how they fit together to architect and design a microservices application.
With this background on what microservices are, we’ll then present patterns for designing an application with a microservices architecture, starting with the root pattern for this chapter, Microservice.
Microservices
An application with a microservices architecture is one composed of microservices.
What are microservices? Amazon AWS provides this definition:
Microservices are an architectural and organizational approach to software development where software is composed of small independent services that communicate over well-defined APIs. These services are owned by small, self-contained teams.
In Building Microservices, Sam Newman defines a microservice as:
An independently deployable service that communicates with other microservices via one or more communication protocols.
What is a microservices architecture? In Microservices Patterns, Chris Richardson defines microservices architecture as:
An architectural style that structures an application as a collection of microservices that are:
Highly maintainable and testable
Loosely coupled
Independently deployable
Organized around business capabilities
Owned by a small team
From the outside, an application with a microservices architecture looks like any other server application, presumably one with a monolithic architecture. Application clients running in web browsers and mobile devices interface with the application, typically over the internet via REST APIs that should define the services that the server can perform for the clients. The clients cannot tell whether the server application is a monolith or composed of microservices. The server application could even start out as a monolith and later be refactored into microservices while preserving the services’ interfaces and without ever changing the clients.
Microservices architecture vs monolithic architecture
The difference between the microservices and monolithic architectures is in how the server application works. A monolithic application is developed by a single large team, built and deployed as a unit, scales vertically in a single process or horizontally by duplicating the entire application, and fails as a unit. A microservices application is composed of modules for individual business capabilities with their own service APIs. It differs from traditional services approaches in that each module is developed by a separate small team working independently from the others, able to build, deploy, and scale those modules independently.
An application with a microservices architecture has significant advantages over one with a monolithic architecture. A microservices application scales more efficiently and isolates failures better. Microservices help users avoid experiencing outages, both because microservices are deployed redundantly and because new versions can more easily replace old ones with zero downtime. Smaller development teams mean that each team member only has to coordinate with the other members of their team, not with everyone else in the department, so they spend less time in meetings and more time writing code. The teams can iterate more rapidly, improving agile development and continuous delivery, because they can test and deploy their microservice when it is ready without having to wait for the rest of the department to be ready to build and deploy the entire monolith.
The microservices architecture also has disadvantages which are typically outweighed by the advantages but cannot be ignored. It a word, complexity: Microservices applications, like distributed applications before them, are more complicated than monolithic ones. More microservices means more service interfaces, which require time to design and coordination between otherwise independent teams that implement and use a microservice. Each team needs to create and run its own build pipeline to deploy its microservice independently. Distributed transactions are more complex than simple ones. More components makes end-to-end testing more difficult. The operations team needs to monitor and manage lots of little microservices instead of one big monolith, aggregating their logs and measuring individual resource consumption. Traceability becomes more complex, as a request is passed from one microservice to another and performed in stages. On the other hand, each service interface is a convenient point for testing and monitoring that can actually help visualize what is working and pinpoint where problems are occurring.
Each microservice is essentially a small application. Lots of small applications are more complicated than one big application, but each small application is simpler and works better so that the whole works better as well.
Microservices and cloud-native architecture
Microservices do not have to run on a cloud platform, but regardless of their deployment platform, microservices architecture is an extension of cloud-native architecture. Just like a cloud-native application, microservices are stateless with service APIs. Microservices need to be able to deploy easily in multiple environments with limited dependencies on the environment, so they encapsulate their program as an application package with an external configuration. Stateless packages are easier to make into replicable applications. And microservices access specialized, reusable functionality as separate backend services.
Although not required, a cloud environment makes lots of small microservices much easier to manage. Multiple microservices can more easily share a pool of capacity, and the environment can more easily provision capacity for each individual microservice as needed. Capacity can also be made available more easily for a new version of a microservice to replace an old one without causing an outage. The environment provides load balancers that enable clients to access a pool of microservice replicas as though they are a single instance that is both highly reliable and highly scalable. Microservices can get their backend services from the cloud platform’s service catalog.
Architecting microservices applications
This chapter defines a collection of seven patterns that together explain how to architect and design applications with a microservices architecture. Figure 4-1 shows the patterns and their relationships.
An application with a microservices architecture is composed of multiple Microservices that each perform an independent business capability and make it available via a service API. We classify microservices into four different types:
-
Domain Microservices that implement functionality from a business domain as a complete capability with a service API and manage the data for that capability.
-
Adapter Microservices that access existing external functionality and give it a service API, thereby encapsulating the rest of the application’s dependencies on the external functionality.
-
Service Orchestrators implement complex functionality by combining the functionality of multiple simpler microservices into one that’s more complex, providing a means to perform transactions in a cloud environment.
-
Dispatchers that provide clients with a single service API to access the business functionality distributed across multiple microservices.
Microservices are language-neutral and support Polyglot Development, enabling each one to potentially be implemented in a different computer language. To maintain their independence even at the data layer, each microservice manages its own persistent data in a Self-Managed Data Store.
This introduction has covered several topics that are helpful to be familiar with to understand the patterns in this chapter. We’ve talked about what a microservice is, how it brings together distributed architecture with cloud-native architecture and forms the basis of implementing a microservices architecture. Microservices do not require a cloud platform, but microservices work the way cloud does and cloud makes microservices easier to manage.
With this background in mind, let’s discuss patterns for how to architect and design applications with a microservices architecture. We’ll start with the root pattern for this chapter, Microservice.
Microservice
You are designing a server-side, multi-user application with a Cloud-Native Architecture. The application may be deployed to run in either the cloud or traditional IT. Typically, you would architect an application as a single monolithic program.
How do you architect an application as a set of interconnected modules that can be developed independently?
The code a developer writes has two audiences: the computer that will run it and the other developers who will maintain it. When computers were new, developers focused on making the program as efficient as possible so that it would run on a machine with limited memory and CPU. When networking was new, bandwidth was limited so even powerful programs needed to limit the amount of data they sent across the network.
Over time, computers became available with greater capacity at much lower prices, a pattern known as Moore’s Law. The computer’s and network’s capacity were no longer the main limitation on computer software. Of the two audiences, the developers were the constraint limiting computer software.
As computers became more powerful, the priority became to make developers more productive. Higher-level computer languages evolved whose compiled code might be less efficient than hand-crafted assembler, but that enabled developers to program in abstractions with which they could describe functionality more easily. That helped developers write more functionality faster with less code that was easier to maintain. But an application was still a Big Ball of Mud whose functionality was difficult to maintain, where multiple developers working on the same application tended to break each other’s changes unless they coordinated very carefully. Even with higher-level languages, the application’s (lack of) architecture lowered the developers’ productivity.
To develop ever more complex applications requires ever greater amounts of code written by larger numbers of developers. Even with higher-level computer languages, to make developers more productive, multiple developers need to be able to work independently on different parts of the application with minimal coordination and for their efforts to integrate together smoothly into a single merged application that works properly.
An evolutionary step to make developers more productive and enable more complex applications is to make an application a Modular Monolith with a modular architecture. A modular architecture does little to benefit the computer it runs on because the application is still a monolith, but dividing code into modules should enable developers to maintain modules separately and therefore work independently. This works somewhat, but dependencies between modules still mean that changes by one developer will mess up another developer’s work in another module, so the developers still have to coordinate. Distributed Architecture enables running modules on separate computers, but now modules are even more complex because of remote interfaces, and distributed modules still cannot be developed independently. Dependencies between modules, even districted ones, still decrease developer productivity.
One popular modular architecture is the four-layer architecture that divides a traditional IT application into four stacked horizontal layers: view, application model, domain model, and persistence. See Figure 4-2.
Although the four-layer architecture separates the concerns into layers, the layers are still developed and deployed as a monolith. The architecture can be distributed by making the layers into tiers deployed on separate computers, making the architecture somewhat client/server. Either way, they layers or tiers are still developed by a coordinated team and deployed as a set. Each layer or tier must be designed to depend on another, and if one layer stops working, the entire application stops working. The developers of each layer must coordinate with the others, limiting developer productivity.
Another popular module architecture is the service-oriented architecture (SOA) that wrappers legacy systems as services with service APIs that can then be orchestrated to form higher-capability services. See Figure 4-3.
Each SOA service and orchestration is a module and they can be distributed. The services are supposed to align with the business and abstract business functionality such that a business analyst would recognize the service APIs, making services reusable and allowing for multiple implementations of the same service API by different service providers. In reality, each service’s API often does little to abstract the existing functionality of the existing system it wrappers. The orchestration and other service clients are written for services aligned with the business, so an increasingly complex enterprise service bus (ESB) is required between the services and their consumers to make them work together. This SOA approach increases developer productivity by making existing systems-of-record (SORs) easier to integrate into newer systems-of-engagement (SoEs), but it does little to enable developers to work independently because of the dependencies between the services and within the ESB.
Developers need an approach that treats modules as first-class objects, not just as sections in a code base or even as services that can be reused individually, but as units that can be developed, built, and executed fairly independently of each other. How can developers and small teams of developers create modules that can be developed independently?
Therefore,
Architect the application as a set of microservices. Each microservice is an independent business capability with its own data, developed by a separate team, and deployed in a separate process. The microservices work together to provide the application’s full set of business functionality.
A microservice exposes its functionality as a service API, implements its functionality statelessly, and persists its state in a data store, as shown in Figure 4-4.
Microservices implement the domain-specific logic of a complete application. There are multiple kinds of microservices, such as dispatchers, domain microservices, and adapter microservices. Figure 4-5 shows a complete application with these three types of microservices.
Each microservice is implemented with a Cloud-Native Architecture, which makes it stateless and replicable with a service API and an implementation that delegates to backend services.
Once James Lewis and Martin Fowler popularized the concept in “Microservices” (2014) and Sam Newman expanded upon it in Building Microservices (2017), the microservices approach has become a de-facto standard for developing large-scale business applications.
Microservices make modularity practical and solve the challenges of monolithic and distributed applications:
-
Each microservice implements a single domain capability
-
Microservices are composable, combining their capabilities to implement an application’s complex domain functionality
-
Microservices communicate via service APIs and events
-
Each microservice has an interface that defines the domain tasks the microservice performs, which clients use to interact with microservices and which microservices use to interact with one another
-
Each microservice can be developed by a separate team, enabling teams to work independently
-
Each team develops a microservice as a separate code base, ensuring modularity
-
Each code base can be built and deployed separately, enabling each team to work at its own pace
-
The microservices in an application can all run on a single computer or distributed across multiple computers
-
Each microservice runs in its own process which can scale and fail independently of other microservices
-
Each microservice can own and manage its own data, keeping it separate from the data used by other microservices
The microservices architecture has become so popular precisely because the combination of these points and the benefits obtained from following them is so powerful.
Figure 4-6 shows the structure of a more complete microservices application. It shows four domain microservices that implement domain logic, each with its own data store, and dispatchers for three different types of clients: a web application, a mobile app, and the API for a CLI or partner application developed independently.
At its simplest, a microservices architecture has four layers:
-
Clients – These enable users and other applications to interact with the microservices application. They are applications separate from the microservices application that typically run outside of the cloud. Typical client types include web clients that run in a web browser, mobile clients that run on mobile devices, and thick-client tools such as command line interfaces (CLIs) and other applications that interface with the microservices application.
-
Dispatchers – These are microservices that run on the server as part of the microservices application and act as interfaces between the clients and the rest of the application. Each client type–such as web clients, mobile clients, and thick clients–has its own customized dispatcher on the server.
-
Domain services – These are the heart of the microservices application, implementing functional requirements as business entities with business logic. Although shown as a single layer for simplicity, the domain layer is often a hierarchy and web of interconnected services.
-
Domain state – These are databases, storage, automation engines, and legacy systems that persist the state for domain services and help perform their functionality.
Figure 4-7 shows how the microservices architecture actually incorporates the layers from the four-layer architecture that was shown in Figure 4-2.
Rather than each layer being global to the entire application, each microservice contains its own layers.
-
Each client is an individual view.
-
Each dispatcher contains the application model for its individual client.
-
Each domain microservice contains the domain model for its functional requirements, as well as the persistence logic for accessing its domain state.
The four-layer architecture is alive and well within the microservices architecture, but like microservices themselves, the four layers are broken into smaller modules.
Microservices can be thought of as “service orientation done right”. Whereas services in an SOA focus on wrapping existing systems, each microservice focuses on modeling a complete domain capability. A microservice and its clients are designed with a common service API so that no ESB is needed to integrate them.
By incorporating a single, complete set of domain functionality within a service API, packaged to be deployed separately, microservices more than any previous architecture finally achieve the nirvana of distributed code modules that developers can create and maintain independently.
The quickest, easiest way to develop functionality is to write a whole bunch of code structured as a Big Ball of Mud. Developing and following the architecture for a Modular Monolith takes more effort, a Distributed Architecture takes even more effort, and a Microservices Architecture takes more effort still. The benefit of this effort is increased developer productivity and runtime efficiency, but it requires an initial and ongoing investment.
Develop a microservices architecture by following several best practices:
-
Split an application’s domain functions into individual Domain Microservices, each a domain capability. This designs microservices that model a business domain and enables each to be developed by a separate, small team.
-
Existing external or internal components may implement functionality needed in the application but may not implement a good microservice interface. Use an an Adapter Microservice to incorporate an existing component into a microservices architecture.
-
Application clients need the functionality in a network of microservices but need a single connection point. Add a Dispatcher that implements the API the client expects by delegating to the microservices on the backend.
-
Each development team will develop their microservice as an application package. With many languages to choose from, separate teams may want to use different languages. Polyglot Development enables each team to implement their microservice using their language or technology of choice.
-
Each stateless microservice persists its state in its own Self-Managed Data Store.
-
A Service Orchestrator combines the functionality in multiple microservices into a unified higher-level microservice.
It is often difficult to determine how to model a complex domain of functionality as a collaborating set of microservices. Microservice Design explains a process for discovering and scoping individual microservices within a domain.
Microservices can be called explicitly via their service APIs, and they can also interact in an Event-Based Architecture that choreographs the interactions between microservices rather than explicitly orchestrating the interactions.
Example
As an example of a microservices architecture, let’s consider how an airline would architect their application. Then let’s review Netflix’s transition to cloud and microservices.
Airline application
An airline’s application built with a microservices architecture has components specific to its functionality, as shown in Figure 4-8.
This application architecture is the standard microservices architecture, customized for the airline. The application includes:
-
Dispatchers – The application supports three client types: the airline’s website, its mobile app, and an API for other travel websites and apps.
-
Domain services – It models its business functionality as four domain services: booking flights, performing lookups in timetables, calculating fares, and allocating seats.
-
Domain state – Each service has its own persistence, and calculating fares uses a rules engine rather than a database.
In a similar fashion, any domain functionality can be modeled as a microservices architecture.
Netflix
One of the earliest and most vocal success stories of applying a microservices architecture has been Netflix, the video DVD and streaming company. The story of Netflix’s transformation to cloud is detailed in interviews and presentations like “Adrian Cockcroft on Architecture for the Cloud” (2013) and “Migrating to Cloud Native with Microservices” (2014) by Adrian Cockcroft, Director of Architecture for the Cloud Systems team at Netflix, “How Netflix Leverages Multiple Regions to Increase Availability: An Active-Active Case Study” (2014) and “Microservices at Netflix Scale: Principles, Tradeoffs & Lessons Learned” (2016) by Ruslan Meshenberg, Director of Platform Engineering at Netflix, and “Completing the Netflix Cloud Migration.”
When Netflix started moving to cloud, specifically Amazon Web Services (AWS), in 2009, they first moved non-customer-facing tasks like batch jobs for encoding movies and storing logging data because it gave them much greater data center capacity than they had on-premises. By early 2010, Netflix foresaw that they needed to move all of their IT operations to the cloud, including the customer-facing functions, because the business was growing so fast that Netflix would run out of on-premises capacity by the end of 2010. Even with this move, many existing systems of record (SoRs) would remain on-premises for the time being because they weren’t growing as rapidly and they would be difficult to move.
When Netflix moved their software from traditional IT to cloud, they couldn’t just lift and shift the existing systems as-is, they had to rearchitect the software. A major portion of that effort was transforming a single giant monolithic Java WAR application into microservices. The transition took seven years from 2009-2016, and as a result Netflix runs on microservices. Netflix’s development methodology evolved from a single release plan were all developers created a single monolithic application to multiple release plans for parts of the application so that they could be developed and deployed independently. This required structuring functionality as fine-grained services with REST interfaces, where each small group of developers worked on only one service. These services always ran as at least three replicas in three different availability zones so that the application kept running even when a zone went down. Each service had its own database, which meant that the database couldn’t manage transactions across multiple sets of data because the data wasn’t in the same database, which forces the application-level code to handle transactions, joins, and consistency. The billing service was the last one transitioned to cloud and microservices because it is stateful, transactional, and has to work properly.
To manage all of this, Netflix developed a suite of utilities to augment the AWS platform’s capabilities, especially its EC2 virtual machine service, and manage these running application services. Netflix started releasing these utilities in 2012 as the Netflix Open Source Software Center (Netflix OSS). The software center included utilities like Eureka for service discovery, the Zuul gateway service, and Ribbon for client-side load balancing. When Kubernetes was released for container orchestration, it incorporated many Netflix OSS capabilities such as service discovery, managed ingress, and server-side load balancing. The Netflix OSS utilities eventually became known as a service mesh and lead to infrastructure layer libraries like Istio, Consul, Kuma, Open Service Mesh (OSM), and AWS App Mesh.
Domain Microservice
(a.k.a. Decompose by Business Capability)
You are architecting a new application using Microservices or refactoring an existing application into microservices. Users have functional requirements for an application, expecting it to provide certain business functionality. Despite the hype associated with the microservices architecture, it’s not entirely clear from the basic microservices principles exactly what the microservices should do.
How should a set of microservices in an architecture provide the business functionality for an application?
Service-oriented architecture (SOA) makes traditional IT applications more modular by dividing an application into services, each of which defines a Service API between the service provider and service consumer. SOA often derived services by wrapping existing systems to give them service interfaces. This resulted in services that were often just a thin veneer over the existing system, with a service interface that did little to abstract the existing system’s implementation. The service was not designed to implement particular business capabilities or user requirements, but rather to expose whatever capability the existing system already provided. This process created SOA services that varied greatly in level of abstraction, granularity, and often didn’t even agree on the data formats being exchanged. These mismatches often required an enterprise service bus (ESB) to make service consumers and providers fit together. Even when multiple services provide the same functionality, their interfaces might differ enough that consumers require an ESB to use them interchangeably.
An application with a microservices architecture should be composed of microservices that represent individual business capabilities that align with how the business actually works, regardless of the capabilities implemented by existing systems. They should be designed to work together with compatible APIs and data models so that they don’t require an ESB for integration.
When designing services from scratch, it is not necessarily obvious what the individual business capabilities are. An application can be designed as a few course-grained microservices or many fine-grained microservices. This is easier to determine when wrapping existing systems: A good start is to design a microservice for each existing system (even if that doesn’t model the domain as an individual business capability). When designing microservices from scratch, how big is too big or too small?
It’s not enough to know what a microservice is. A team also needs to figure out how to model an application and its business domain as a set of microservices.
Therefore,
Develop each business capability as a Domain Microservice that implements a Service API for that capability and encapsulates all the business functionality that implements the capability.
A Domain Microservice encapsulates a single business capability. It implements the business functionality for that capability and, like any microservice, exposes it as a Service API. If the business functionality has state, the microservice persists it in one or more data stores. Figure 4-9 shows the structure.
Build an entire application by composing together all of its individual business capabilities, each implemented as a domain microservice. Domain microservices form the business functionality layer in a microservices architecture.
Figure 4-10 shows a complete application with the domain microservices highlighted. All of the application’s business functionality is divided into this set of domain microservices.
The scope of a domain microservice is defined by the business transactions it performs and the data they use. Here are some guidelines for deciding how broad a domain microservice should be:
-
Manages a business transaction and its data – Scope a microservice’s responsibilities broadly enough that it can perform entire business transactions and do so using only the data that it manages. When a business transaction is split across multiple microservices, the design risks that one microservice might succeed while another fails, resulting in a business transactions where the “transaction” is performed but only partially. If both microservices manage their own data, one database might get updated while another does not, resulting in inconsistent data. Avoid dependency chaining–where one microservice calls another which calls another–to perform a business transaction. If two parts of a business transaction are performed by two different microservices, especially a transaction that updates data, consider merging the microservices into a larger one. There are, however, situations where this is not possible. Eventual consistency, compensating transaction, or other techniques might need to be part of the solution in these cases.
-
Avoid tightly-coupled microservices – Scope a microservice’s responsibilities broadly enough that it can perform its functionality without having to be tightly coupled to other microservices. For example, if one microservice produces the data that another microservice processes, a client will always have to use them together. Encapsulate both functions in a single microservice that both produces the data and processes it, simplifying the service API and the client.
Dividing business functionality into a set of individual business capabilities will make it easier to maintain and reuse.
Like building blocks, Domain Microservices that perform individual business capabilities can easily be composed into complex applications.
Designing Domain Microservices around business capabilities is rather difficult when the individual business capabilities are difficult to identify. Fundamentally, designing the functionality in domain microservices is much like designing the domain objects in a domain model in object-oriented programming.
Model around Domain explains how to design domain microservices by applying domain-driven design (DDD) techniques and event storming. How a business operates can be assessed both statically and dynamically. The static view models the structure of a business domain as a set of Aggregates augmented with Domain Services. The dynamic view models the interactions within a business domain, which can be discovered through Event Storming and captured as Domain Events.
While a microservices architecture is comprised primarily of domain microservices that are designed from scratch and implement their own behavior, the architecture can also use Adapter Microservices to incorporate existing systems.
To compose a complex business capability from multiple Domain Microservices, implement it as a Service Orchestrator.
Since each Domain Microservice runs in its own process, Polyglot Development enables developers to implement each Domain Microservice in a different language or technology.
Each Domain Microservice that has state should persist it in one or more Self-Managed Data Stores.
Building a single new Domain Microservice is a great way to introduce a team to how to Start Small by starting with microservices. It is often best to do this with a new business area rather than trying to start off the bat with refactoring a monolith. There is a lot of learning that a team needs to do in order to become productive with microservice development, and starting with a simple, green-field domain microservice is often the best way to do that.
Example
As an example, let’s return to the airline application from the Microservice pattern, shown again in Figure 4-11.
This application architecture divides the airline’s business functionality into four domain microservices:
-
Book flights - Overall functionality enabling the user to purchase an airline ticket.
-
Lookup timetable - Functionality to find the flights available between the desired cities on specified dates.
-
Calculate fare - Functionality to determine how much to charge for the selected flight. It doesn’t persist state in a data store, it delegates to a rules engine.
-
Allocate seats - Functionality to either assign a seat to the passenger or enable the passenger to select a seat.
Domain microservices like these can implement all of the business functionality for an airline.
Adapter Microservice
You are architecting a new application using Microservices or refactoring an existing application into microservices. The application needs to incorporate existing sources of functionality.
How can the application take advantage of existing functionality without abandoning the microservices approach?
The ideal way to incorporate existing functionality into a microservices architecture is to reimplement the existing code as microservices; for example by Strangling the Monolith. Then the existing functionality will be reimplemented to be cloud native, to run well in the cloud, and will run better and be easier to maintain as microservices. However, this approach assumes that the current development team controls the code for the existing functionality and has the time and expertise to modernize it.
There are several reasons why an existing system may need to be reused as-is without modifying it:
-
The existing functionality may be hosted by a third party that develops and maintains it, such as a software-as-a-service (SaaS) web service.
-
The existing functionality is the only API that can access specific enterprise data sources.
-
The existing functionality is a legacy application that is too difficult to modify or replace.
-
The existing functionality is a legacy application that a future phase will eventually replace by reimplementing it as microservices, but for now it needs to be reused as-is.
The new application with the microservices architecture needs to incorporate the functionality of the existing system without modifying the existing code.
Therefore,
Add an existing system to a microservices architecture by developing an Adapter Microservice with a service API like the other microservices in the application and an implementation that delegates to the existing functionality.
As shown in Figure 4-12, an Adapter Microservice encapsulates an existing, external system of record (SoR) as a capability.
The SoR capability in the microservice is like a business capability but one limited to the behavior in the SoR. The microservice implements adapter functionality for accessing the SoR remotely. Like any microservice, an adapter microservice exposes its functionality as a Service API, yet this API is limited by the SoR’s existing interface. New clients, such as other microservices in the new application, use the adapter microservice’s service API while existing clients can continue to use the SoR’s interface directly as-is.
An adapter microservice converts the interface of an existing SoR much like an adapter object. In object-oriented programming, sometimes an existing object has one interface but an existing client expects a different interface. Rather than modify either of the existing sets of code, a developer can employ the Adapter pattern from Design Patterns, converting the interface that an existing object has into the interface that a client expects. The microservice also acts as a Proxy, providing local access within the microservice architecture to the remote SoR outside of the architecture.
An Adapter Microservice works similarly to an endpoint in an enterprise service bus (ESB) that converts one service’s interface into another. The service-oriented architecture (SOA) approach for incorporating existing functionality is to implement an ESB that converts the APIs the service consumers wish the functionality had into the interfaces the functionality actually has. The ESB can also transform the existing functionality’s data models into ones that work better for the consumer. ESBs are often implemented using products like IBM App Connect Enterprise (ACE), Mule ESB from MuleSoft, or open source solutions like Apache Camel.
Whereas a Domain Microservice is an evolution of an SOA service that models a complete business capability, the implementation of an Adapter Microservice is more like that of a traditional SOA service that attempts to wrap a service interface around an existing SoR:
-
The service implements little to no domain logic, it reuses domain logic already implemented in the SoR. If additional domain logic is needed, that should be implemented in domain microservices that use an adapter microservice to access the external SoR through a well-designed service interface.
-
The service’s functionality is based on the SoR’s functionality. The SoR’s scope of functionality and how it enables functionality to be accessed dictates how the service and the service’s API will work.
-
The service predominately contains integration logic, implemented as adapter functionality focused on how to connect to the SoR over the network and how to work with the SoR’s interface, functionality that SOA’s often delegate to an ESB. The adapter functionality may include data validation logic.
Even if the existing SoR already has a perfectly good service API, such as a well-designed SaaS web service, incorporating an adapter microservice improves the maintainability of the microservices architecture:
-
The adapter isolates the application from the external SoR’s existing API, protecting the application when the API provider changes the API or discontinues support for it.
-
The adapter can transform the external SoR’s data model into data formats that better fit the microservice architecture. For example, if the external SoR’s data format is a COBOL copybook or binary, the adapter should transform the data into a more modern format like JSON or XML.
-
One or more Adapter Microservices can perform significant API translation. The adapter can add the existing SoR’s functionality into the microservices architecture by only reusing a subset of the existing functionality, or reusing that functionality by splitting it into multiple microservices that each reuse a subset, or combining the functionality from multiple SoRs into a single microservice.
-
The existing system’s quality of service (QoS) model may not meet the QoS needs of the microservices. For example, the adapter can improve performance using caching or improve the reliability of unreliable functionality by adding retry behavior.
An Adapter Microservice can be a temporary step, reusing an existing SoR until it can be replaced. The existing SoR can be reimplemented as a domain microservice with the same service API as the adapter so that it can replace the adapter. If the existing SoR is better modeled as multiple domain microservices, coordinate them with a service orchestrator that implements the same Service API as the adapter so that the orchestrator can replace the adapter.
If the existing SoR provides callbacks to its clients, the adapter microservice can handle those callbacks, and even expose corresponding callbacks to its clients as part of its service API.
Some ESB products have evolved to support microservices, including IBM App Connect Enterprise (ACE) and Camel. This way, a microservices architecture can use an existing ESB. Furthermore, the architecture can replace the ESB by splitting apart the ESB into independent APIs and implementing each API as an Adapter Microservice.
Adapter Microservices enable a microservices architecture to incorporate existing SoRs as microservices without changing the SoRs.
However, the Adapter Microservice’s functionality is constrained by the existing SoR’s functionality. While the adapter makes the SoR more reusable, the SoR is no more scalable than it ever was, and it continues to be a single point of failure in an otherwise highly available architecture. An SoR with an interface that exposes its implementation leads to a microservice whose API may lack the abstraction of a service interface.
Having developed an adapter microservice, an application will also need Domain Microservices and Service Orchestrators that reuse the adapter’s functionality.
The SoR that an adapter microservice integrates is typically a monolith with a Modular Monolith architecture or even a Big Ball of Mud lack of architecture.
Since each adapter microservice runs in its own process, Polyglot Development enables developers to implement each adapter microservice class in a different language or technology.
Whereas an adapter microservice adapts an existing external system, a Dispatcher adapts multiple microservices into a single Public API expected by an external client.
Example
As an example, let’s return to the e-commerce application from the Cloud Application pattern, shown again in Figure 4-13.
This application is composed of five microservices. Three of them are domain microservices: Catalog, Customers, and Ordering. They implement e-commerce business functionality and persist their data in data stores.
The other two, Inventory and Payments, are adapter microservices. They wrapper existing SoRs, Warehouse System of Record and Payment Processing respectively. They don’t implement business functionality like the domain microservices do, they delegate to the SoRs for the business functionality the SoRs already implement. The SoRs may have fairly ugly interfaces that are difficult to reuse–they may even require screen scraping!–and antiquated data formats, but the adapter microservices have clean service APIs that fit easily into the rest of the microservices architecture. The adapters may improve on the SoR’s non-functional quality of service, such as adding security features like enforcing authorization and encrypting data.
The architecture may eventually replace one of the SoRs by modernizing its functionality as microservices, which would also replace the corresponding adapter microservice. The adapter can be replaced by a single domain microservice with the same API, or by a set of domain microservices coordinated by a service orchestrator with the same API.
Dispatcher
(a.k.a. Backend for Frontend, Aggregator)
You are architecting a new application using Microservices or refactoring an existing application into microservices. You notice a mismatch between the Domain Microservices that implement the business functionality and client GUIs that enable people to use the functionality.
How can a client access a microservices application through a channel-specific service interface when the business functionality is spread across an evolving set of domain-specific APIs?
When the server application is a monolith running in traditional IT, it’s easy for a client to connect. The application is a single process running statically, so the client connects through a single endpoint or set of endpoints that never changes. The application has just one client for all of the users, or different clients work the same so that they can all use the same endpoint.
Cloud-native applications, especially microservices applications, are more complex to connect to. Cloud applications are much more dynamic, with components replicated horizontally, running in IP addresses assigned dynamically. In a microservices architecture, not only is each microservice replicated independently, but multiple microservices break one set of business functionality into multiple Service APIs. Meanwhile, the easiest way for a client to interface with a server is still through a single API, not multiple microservices’ APIs.
An application’s set of microservices and their APIs change over time as the application evolves. A single microservice might be refactored into two, and two microservices that are highly dependent on each other might be merged into one. Once a microservice has been implemented, although ideally its API shouldn’t change because then its clients have to change as well, realistically an API may evolve over time as its responsibilities change.
All of this evolution in the microservices on the server cause havoc for the clients. When the microservices APIs change, the clients must be reimplemented to use the new APIs. Deploying new versions of microservices, although complex, is relatively easy compared to updating all of the devices that have the clients installed. It would be much easier if the clients didn’t have to change just because the set of microservices and their APIs changed.
Different types of clients–web browsers, mobile apps, CLIs–may need different APIs even when they offer the same business functionality. Likewise, clients for use by an enterprise’s employees may require different APIs from the clients for the enterprise’s customers because the client applications for these different types of users offer access to different functionality. Yet all of these different clients and with their differing API needs should not duplicate business functionality. These clients should still access the same set of microservices so that they all consistently offer the same business functionality.
How can various types of clients have easy, stable access to a dynamic and evolving set of microservices on the server?
Therefore,
Build a Dispatcher (a.k.a. a Backend for Frontend or BFF) that provides a unified API for clients to use the functionality in multiple microservices. Implement different dispatchers for different types of clients, each with an API customized to what that client type needs.
As shown in Figure 4-14, a dispatcher exposes a single API for an external client and implements that API by delegating to microservices in the architecture.
The Dispatcher provides a single endpoint for the client to access the application, one with an API the provides the exact functionality the client requires. The client does not need to know how that functionality is distributed across the microservices. Rather, the Dispatcher encapsulates the logic for how the functionality is distributed, delegates to those microservices to invoke the functionality, and consolidates it into the API the Dispatcher provides for the client. Each type of client requiring a different API uses a different Dispatcher.
Building Microservices introduced this pattern as Backends For Frontends:
Rather than have a general-purpose API backend, instead you have one backend per user experience—or a Backend For Frontend (BFF). Conceptually, you should think of the user-facing application as being two components—a client-side application living outside your perimeter, and a server-side component (the BFF) inside your perimeter.
Dispatchers form the client-facing layer in a microservices architecture.
Figure 4-15 shows a complete application with the dispatchers highlighted. The clients all access the same application but through different Dispatchers. The architecture includes a dispatcher implementation–such as web dispatcher, mobile dispatcher, and API dispatcher–for each client type–such as web app, mobile app, and partner/CLI app, respectively.
The dispatchers define the external APIs that clients outside the cloud use to access the microservices application hosted in the cloud. A Dispatcher’s API is a service API, typically implemented as a web service, that makes the entire microservice application look like a single API. Each client type–web browser, mobile app, CLI, etc.–should have its own Dispatcher. Each Dispatcher should have an API that’s customized for its client, so each client type gets exactly the API it needs to use the server application.
Dispatchers encapsulate a microservices architecture, which protects clients from changes in the architecture and protects the architecture from changes in the clients. As the architecture evolves–refactoring behavior, adding new microservice classes, removing obsolete ones–and the microservices’ APIs change, the Dispatchers’ implementations will evolve as well and absorb the changes, leaving the clients unaffected. Likewise, when a client needs new functionality or different data formats, its Dispatcher’s API and its implementation evolve accordingly, yet as long as the microservices still provide all of the functionality that the client needs, the microservices architecture is unaffected by changes in the client.
A Dispatcher should not contain any domain logic. Because each Dispatcher is specific to a single client type, any domain logic implemented in Dispatchers won’t be shared across all client types. The Dispatcher delegates to microservices in the architecture, based on which ones implement the functionality needed to implement the dispatcher’s API. Each microservice can be any type, such as a Domain Microservice, Adapter Microservice, or Service Orchestrator.
A Dispatcher’s internal implementation performs routing and conversion between the API the client wants to consume and the APIs the microservices provide, such as:
-
Orchestration – It can orchestrate several calls to microservices to implement a single client action.
-
Translation – It can translate the results of a microservice into a channel-specific representation that more cleanly maps to needs of the user experience of that client type.
-
Filtering – It can alter the results from the microservices to remove items or details that are not needed by a particular client type.
The scope of a Dispatcher is usually straightforward: Its API is customized for the client type it supports. Its implementation is usually pretty simple, implementing the API to route requests to microservices as necessary.
This pattern is a key part of building the range of Client Applications that can be used to access a cloud application with a microservices architecture. Client types such as Mobile Application and Single Page Application introduce unique translation or filtering requirements that often necessitate the use of a different dispatcher for each client type. For example, in a mobile application, don’t send a large set of data to the device that may have limited bandwidth, only send what it can display on its small screen. Likewise, a single page application may walk the user through a multi-screen wizard that requires orchestrating separate domain microservices on the server.
Because a Dispatcher’s API is customized for its client type, the client and its Dispatcher tend to evolve at the same rate. Because of this coupling of their functionality, the same development team should implement both the client and its Dispatcher and modify them together, as shown in Figure 4-16.
Since both the client and Dispatcher are implemented by the same development team, the team often finds it convenient to implement them both in the same language. For example, the developers building a single page application using JavaScript will want to develop their dispatcher services using Node.js on the server side. Likewise, Java developers building a native application for Android may want to develop their dispatcher services with Java.
A Dispatcher is often a convenient place to cache data for the client. When Domain Microservices produce a large data set as a result, it may not make sense to return all of that data to the client at once, since bandwidth to the client may be limited, the client’s storage may be limited, and its screen may only be able to display a small amount of data. Instead, the Dispatcher can make one call to the Domain Microservices to get the result, cache the data set, and enable the client to request it a page at a time.
A Dispatcher organizes all of the APIs in all of the microservices in an architecture into a single API that does exactly what a client needs, encapsulating the application for the client and simplifying the client’s interface with the application.
A Dispatcher can only provide functionality that the microservices provide. When a client requires additional functionality, that will need to be implemented in the microservices architecture and the changes will ripple through the microservices, the Dispatchers, and the clients.
A dispatcher is a type of Microservice but a specialized one that works differently. A dispatcher is stateless and replicable, but its simple routing implementation rarely becomes a performance bottleneck that requires replication. Its service API isn’t based on a domain capability, but more so on the requests that a client wants to make of the application. Dispatchers don’t delegate to other Dispatchers and microservices don’t delegate to dispatchers; Dispatchers are a single layer in the architecture, the architecture’s client-facing layer.
A Dispatcher adapts multiple microservices within an architecture, whereas an Adapter Microservice adapts a single system of record outside of the architecture.
A dispatcher’s API is often a Public API expected by the client.
Since each dispatcher runs in its own process, Polyglot Development enables developers to implement each dispatcher class in a different language.
Example
Let’s examine how dispatchers can help implement a banking application. Let’s suppose we are part of a bank that has two disparate sets of customers (common with retail banks today). The first set of customers want a simple web application to allow them to check their balances, do electronic transfers, and schedule payments. The second set wants all of those things, but also wants to be able to trade equities, manage their investment portfolio, and explore new investment opportunities. What’s more, the second set is more comfortable with technology, and wants a richly featured mobile application they can use on their phones or tablets.
These two types of customers will need two different user interfaces but those UI’s can share the same microservices functionality. Since the set of features that the second type of customer wants is a superset of the features needed by the first type of customer, one might assume that the two UX’s can simply use the same microservices directly. That may be true in some cases, but the odds are that the mobile app team wants to display the information on Accounts differently than the web app team does. They likely do not need to display all of the same information at the same time, nor do they necessarily need to show the same amount of information. Thus, the mobile team would need their own dispatcher to filter information from the (shared) Accounts microservice. This approach is shown in Figure 4-17.
The application needs two user interfaces, one for simple banking that runs in a web browser and another for stock trading that runs on a mobile device. These two UIs require two Dispatchers, each of which provides its UI with exactly the API and behavior it needs, and in doing so encapsulates the microservices from the client. The dispatcher for the banking UI encapsulates functionality in the bank account microservices. The Dispatcher for the stock trading UI encapsulates functionality in the equity microservices and, because the stock trading UI enables banking functionality as well, also delegates to the bank account microservices.
Polyglot Development
You are architecting a new application using Microservices or refactoring an existing application into microservices. The languages used to implement programs for traditional IT can also be used for cloud, but maybe other languages should be considered as well.
What computer language(s) should be used for implementing microservices?
Computer science has created many different computer languages, and keeps creating new ones. How should an IT department select the best language for implementing its microservices?
For decades, an enterprise IT department would develop all of the programs in the same language. A science and engineering department would implement all of its programs in FORTRAN. The data processing department used COBOL. More recently, some huge companies wrote everything in Java, whereas others exclusively used PowerBuilder, Visual Basic, or C#. Most developers specialize in one particular language, the one the department they work in uses the most.
This uniformity with computer languages was the result of several drivers:
-
Monolithic applications – When the code for an entire application runs in a single process, all of it is usually written in the same language. Even in a Modular Monolith, all of the modules are usually implemented in the same language.
-
Interoperability – Applications are easier to connect together if they’re written in the same language. If a COBOL program uses COBOL copybooks, other programs that work with it will need to be written in COBOL as well. Universal support for SQL databases give some language flexibility, but then interconnectivity technologies like sockets and CORBA limited language choice once again.
-
Platforms and frameworks – The runtime environment supported by the enterprise’s operations team could dictate language or technology. When the preferred deployment platform is Java EE application servers, all programs need to be written in Java. In a department that prefers the advantages of .NET, Java programs are useless, they need to be written in languages like C#. Android OS supports Java programs well, iOS lends itself to Swift. Web browsers are optimized to run JavaScript, which also implements user experience (UX) platforms like Angular and React.
-
IT department staffing – When all of the projects in a department write programs in the same language, staff can more easily move between projects. The department only hires new staff who have skills with that language.
Cloud computing and microservices can run programs written in any language that runs on commodity hardware. An enterprise IT department or product development team accustomed to writing all of its programs in the same language may be tempted, by design or out of habit, to also develop all of the microservices in an architecture in the same language.
With microservices, language uniformity isn’t required. The traits of a microservices architecture provide isolation between microservices, and these same traits support developing microservices in different languages, even in the same application. Cloud-Native Architecture practices enable distributed modules to be written in different languages. Not only does each microservice run in its own process, it is bundled as an Application Package with its own runtime, enabling each package to embed a different runtime. Microservices communicate through Service APIs, typically through a universal protocol like REST, enabling components written in different languages to interoperate nevertheless. Backend Services separate the stateful shared resources from the stateless microservices, so a microservice and its backend service can be implemented in different languages. Cloud computing’s dominate operating systems–Linux, as well as Microsoft Windows–support hosting runtimes for a variety of languages.
Therefore,
Microservices architecture supports Polyglot Development, where each microservice can be implemented in a different computer language. Allow each team to select the language they use to develop their microservice.
In a microservices architecture, each microservice doesn’t have to be implemented in a different language, but it can be. Figure 4-18 shows a standard example of an application with a microservices architecture, including an application client, a dispatcher, and three microservices. The microservices are implemented in three different languages: Node.js, Java, and Go.
A monolithic application runs in a single process and so must be implemented in a single language. Polyglot development takes advantage of the fact that each microservice runs in its own process and so can be implemented in a different language. Multiple microservices can still be implemented in the same language, and usually are. However, microservices that work very differently may be easier to implement and may work better by using different languages and technologies.
Polyglot development offers a key opportunity: For each microservice, figure out how to implement it and choose the best language for the task. Suit the solution to the problem. If a microservice needs to model a business domain with procedural rules and complex interactions, a good approach is to Model around Domain and implement the microservice using an object-oriented programming language such as Java. Alternatively, if a microservice needs to make classification decisions or determine best choices from incomplete data, implementing machine learning with Python may work well. If a microservice needs to manipulate complex mathematical rules or construct models of physical activities, then implementing functional programming with Scala or even JavaScript may be a good way to go.
A single application with a microservices architecture could use some or all of these approaches, plus additional approaches like scripting or event-driven architecture, as shown in Figure 4-19.
Each component in this architecture is implemented in a different language. The web client is implemented in JavaScript, the dispatcher is written in Node.js, the Account microservice is written in Java, and the Account Fraud Protection microservice is implemented using Python.
In a microservices architecture, individual microservices must communicate using network transports (e.g., HTTP) and payload data formats (e.g., JSON, Google Protocol Buffers) that support cross-language operations. This means that it does not matter which language each microservice is implemented in, since that detail is abstracted away by the protocol and encoding.
Polyglot Development can be used not only to implement each solution with the best language, but also to enable each development team to program with the language they prefer. If some developers are more highly skilled with one language and some another, rather than putting them all together and making them fight it out, put them in separate teams and let those develop separate microservices. Each should choose a microservice they can implement well using the language they prefer. If a solution requires a language that the developers lack experience using, put those who want to learn that language on a team and provide them with education to learn the language.
Polyglot Development makes sharing libraries more difficult because two microservices implemented in different languages cannot always share the same library. That is actually an advantage, however, because a library shared between two microservices creates a dependency between the microservices which can make them difficult to maintain and evolve independently. Imagine if the development team maintaining one microservice wants to make changes to the library and the other team doesn’t want those changes. Each microservice should develop its own libraries in its own preferred language. The microservice’s application package will bundle the microservice program with its own copies of the libraries it requires.
An enterprise should provide guidance to its development teams on which languages are supported, to prevent a development team from selecting a language that the enterprise cannot support. The enterprise must ensure it has a sufficient pool of developers to not only create microservices in a supported language, but also ones who will be able to maintain it over the long run, and that these developers have sufficient tooling, frameworks, and technical support to be successful with the enterprise’s approved technologies. Without this governance, a team may select a niche or outlier language that may become difficult for the enterprise to support long term.
The microservices architecture enables Polyglot Development, which enables each team to implement its microservice in the language that works best, regardless of the languages used to implement other microservices.
An enterprise needs to govern the selection of languages to avoid developing a microservice that lacks sufficient staff to maintain it.
Each kind of microservice can be polyglot, whether it is a Domain Microservice, Adapter Microservice, Dispatcher, or Service Orchestrator. Some may even be implemented not with a programming language but with a technology such as a process engine, rules engine, machine learning engine, etc.
A microservice can be implemented in almost any computer language, but to run well in a microservices architecture or hosted on the cloud, the language should be one that supports delivering a program in an Application Package. A language whose programs are difficult to package may be able to implement a microservice adequately, but that microservice will be difficult to deploy and manage.
Polyglot applies not only to language and technology but to persistence as well. Stateless microservices with data store it in Self-Managed Data Stores. When the data stores are Cloud Databases, Polyglot Persistence enables each development team to select the database type that works best for its microservice.
Examples
In the following sections, we’ll discuss a common multi-language architecture, as well as the different language selections that are more common for each component.
Node.js Dispatchers with Java Microservices
A common implementation strategy for a microservices application is to implement the Dispatchers in Node.js and the microservices (domain, adapter, and orchestrators) in Java, as shown in Figure 4-20.
A Dispatcher must support web clients, a task Node.js does well. A popular web application can have thousands of web clients, so the dispatcher must scale well. A web client spends most of its time waiting on its user rather than performing work, so the dispatcher shouldn’t block waiting as well. A web client communicates via HTTP request and response. When a web client does submit a request, it will be input that is fairly simple to process even if it’s for a task that’s complex to perform.
Node.js is optimized for these web client requirements. It works well with HTTP. It runs single-threaded, dispatching each HTTP request quickly to a callback and moving on to the next, so it can scale well without managing multithreading or blocking threads. A complex request would bog down the Node.js process and block all of its web clients, but a Node.js dispatcher can be designed to dispatch its request to another process to perform the task.
A Domain Microservice performs business functionality and attaches to external resources, capabilities that Java does well. Java is good at performing CPU-intensive computations and data processing. Its multithreaded process can scale efficiently to perform tasks for multiple concurrent clients. Its adapters make it easy to connect to Backend Services such as databases (JDBC), messaging systems (JMS), and any other external resource that supports a connector (JCA). Its HTTP interface (JAX-RS and JAX-WS) maps requests to concurrent threads.
When in doubt for which language to use for which parts of a microservices architecture, Node.js dispatchers with Java microservices is a good approach to consider.
Dispatchers
There is usually a one-to-one correspondence between Dispatchers and clients: web clients connect through corresponding web dispatchers, mobile clients via mobile dispatchers, etc. Each dispatcher runs in a separate process, so they can employ polyglot development and each dispatcher can be implemented in a different language.
Each Dispatcher should be implemented by a single team, and the same team should develop the client and its corresponding dispatcher together. The team can choose to implement the dispatcher in the language that makes the most sense, and each team can develop their dispatcher in a different language. Often a team chooses to implement the dispatcher in the same language as the client, so that the team can use the same skills implementing both parts, and the two parts can interoperate more easily. Different clients often require or lend themselves to different languages, which helps guide the language to use for the dispatcher:
-
Web Browser: As we discuss in Single-Page Application, SPA’s are often implemented using JavaScript, so implement the dispatcher using Node.js (since Node is the server-side version of JavaScript; the “.js” in the name Node.js stands for JavaScript).
-
Apple iOS: The client is often implemented in Swift, so implement the dispatcher using Swift as well.
-
Google Android: The client is usually implemented in Java, so also use Java to implement the dispatcher.
The Dispatcher can be implemented in a completely different language from the client, but that is often less convenient, especially since the Dispatcher and client should usually be written by the same team.
These examples notwithstanding, as noted in the previous example, Node.js is typically a good language to use for implementing dispatchers. Dispatchers need to handle HTTP web service I/O efficiently to support large numbers of concurrent clients, and Node.js scales well for network I/O.
Domain microservices
Each Domain Microservice runs in a separate process, so they can employ polyglot development and each microservice can be implemented in a different language. Domain microservices often use databases and other backend services. When they do, each can choose its own database service.
Each Domain Microservice should be implemented by a single team. The team should choose a language that works well for implementing the microservice’s solution, a database service that works well for how the data will be stored and used, and the language should work well with the database. Each team makes these decisions for their microservice independently of what other teams choose for their microservices.
The team can use the best language for their domain and the problem they are solving. For example, when implementing Domain Microservices, if the business logic requires business calculations to be multithreaded and use CPU efficiently, Java is a good choice. Java is also good for microservices which need to connect to external legacy systems, since Java includes adapter technologies to facilitate these connections. On the other hand, if it’s a heavily network-based application, Go Lang could be a good option instead.
Adapter microservices
Adapter Microservices are much like Domain Microservices, in that each runs in a separate process and so can be implemented in a separate language and each is developed by a separate team which can use the language it chooses. Even more than Domain Microservices, adapter microservices focus exclusively on connecting to external legacy systems.
Java is typically a good language to use for implementing Adapter Microservices because of the multithreaded connectors it provides. Another choice for implementation of an Adapter Microservice is to use a commercial API gateway (such as WebMethods, Apigee, API Connect, or many others), which will often provide low-code or no-code options for common protocols and SaaS systems.
Again, since each adapter microservice is implemented independently, some can be implemented in Java while others are implemented using one gateway technology and others use another gateway technology. How one is implemented does not limit how the others can be implemented.
Self-Managed Data Store
(a.k.a. Database per Service)
You are architecting a new application using Microservices or refactoring an existing application into microservices. A microservice, such as a Domain Microservice or Service Orchestrator, implements its Service API using domain data.
How does a microservice store its state?
Microservices are Stateless Applications, so they don’t store state internally. Yet most applications have state, and domain microservices implement business functionality that has data. If the microservice doesn’t store its business data internally, it has to persist that data somewhere.
A traditional IT application persists its data in external storage, typically a database. The entire application is connected to the database and has access to its data. A Modular Monolith is a single application and so typically has one database which all of the modules share. When multiple modules need to use the same data, it’s simple for them to all access it in the shared storage. The application is developed by a single team, or multiple teams working together, so it is easy to coordinate as necessary to make the data persistence work for all of the code that needs to use it.
For developers of traditional IT applications, the modules in an application sharing data in a database is a natural extension of the way multiple applications in an enterprise share a database of record (DoR). A complex enterprise includes several mission-critical applications and at least one DoR, typically a relational database, that is shared by many of those applications. DoRs become huge, containing any data that any application needs. Any one application typically uses only a small subset of the data in the DoR. Yet all of the data in the DoR is used by some application, and typically a useful set of data is used by multiple applications. The applications share the data, often integrating the applications as a Shared Database that passes data when one application stores it and another application reads it.
Any shared database, especially one as widely shared as a DoR, creates couplings and dependencies between the applications. The database schema cannot evolve because changes often necessitate updating all of the applications that use the data. When one application messes up some shared data, that in turn messes up all of the other applications that use the data. Even worse, if one application deletes a record that other applications still need, those application’s actions can become flawed because of the missing data. Enterprises sometimes develop policies not to delete data from DoRs because they can never be sure none of the various applications is using it, or maybe the data is useless now but might become useful again in the future. Databases become bloated with data that hasn’t been used for years but looks as valid as any other data in the DoR.
Not only is the data difficult to maintain, the applications become difficult to maintain as well. Each application must store its data in a certain format not because that format is natural for the application but because the DoR uses that format. That format must have been useful to some application at one time, but maybe has become a poor fit for all of the applications that still use it. Meanwhile, if an application has data to store that is not part of the DoR’s schema, the application may be out of luck and has to throw away the data.
DoRs also become performance bottlenecks. When too many applications access a shared resource, performance limitations in that shared resource slow down all of the applications using it. When one application locks a row or table, any other application that needs the data must block and wait. Many DoRs run in older database servers that can only support a very limited number of connections, limiting the applications that can use the data. Enterprises develop data access strategies with read-only copies of the DoR to enable greater access. As described in Command Query Responsibility Separation, applications must queue their data updates so that one processing application can perform the updates in batches that optimize limited connections and avoid locking conflicts. The enterprise effectively reinvents the capabilities a database is supposed to perform in the first place.
Compared to a DoR shared by many applications, storage shared by modules in a single application seems much simpler. Yet just as storage shared between applications creates a coupling between them that makes it more difficult for those applications to evolve independently, storage shared between modules in an application creates a similar dependency that limits evolving the modules.
Microservices should be independent, able to be developed by different teams and able to be deployed separately. If they share the same data storage, that creates a dependency between them. Teams developing multiple microservices must coordinate on how their independent sets of code will store and share common data. To evolve the data format, all of the microservices must be updated to use the new data format. To deploy the microservices, whichever is deployed first must allocate the storage, then the others must be sure not to create duplicate storage but rather to share the storage that has already been created by whichever microservice came first. The shared storage can become a performance bottleneck as differing microservices compete to read and update its data, and likewise is a single point of failure.
Microservices need to remain independent even when they share the same data.
Therefore,
Each microservice with state should include a self-managed data store to store that state, storage the microservice manages that is accessed exclusively by the microservice. Each different microservice has its own data store, while replicas of the same microservice share the same data store.
Two microservices do not share the same self-managed data store, they each have their own, as shown in Figure 4-21.
The two microservices will be developed independently. Separate data storage enables each team to develop its microservice’s storage independently as well. If the microservices shared the same storage, that would be a dependency that would require coordinating the development and maintenance of the microservices.
When one microservice needs to use another microservice’s data, it delegates the work to the other microservice via its service API, enabling the other microservice to perform the work using the data in its own storage. This way, no matter how many different microservices may use a set of data indirectly, the only microservice with direct access to the data is the single microservice that owns the data.
When a microservice is a Replicable Application, its replicas all share the same self-managed data store.
As shown in Figure 4-22, multiple replicas of a microservice share a single data store. Replicas are interchangeable, which is only possible if they share the same data. Each replica could try to store its data in its own data store, but then each replica would have different data. Replicas with different data behave differently from each other and therefore are not interchangeable.
If multiple microservices all depend on the same shared database, such as a database of record (DoR), they run into the same problem of shared dependencies that occurs with multiple traditional IT applications depending on the same DoR. Any microservice that updates the data incorrectly will corrupt it for all other microservices. If one microservice creates a lock on the data, it blocks all of the other microservices. If one microservice wants to change the data format, all of the other microservices also need to be updated to use the new format. The microservices may seem to run independently, but by sharing the same storage, they all share a performance bottleneck and single point of failure.
Microservices avoid these dependencies by not sharing storage. One will not fail because another has corrupted its data, nor will it be blocked by another using its data. One microservice can change the way it stores its data without impacting the other microservices. One microservice using its data heavily will not affect the performance of other microservices, and storage becoming temporarily unavailable only affects one microservice’s replicas, not all other microservices as well.
Each microservice managing its own data store helps keep the microservices independent. Each microservice is the only code using its storage, so it controls its data, the data’s lifecycle, and can evolve its format.
Data is no longer centralized in one massive database of record (DoR), it is dispersed amongst multiple specialized databases. Multiple data stores require additional management, such as to back up the data.
Each self-managed data store should be a Cloud Database, which works better than a microservice using block or file storage to store data. The database should be a Replicated Database so that it will scale the way the replicable microservice scales, and an Application Database whose functionality favors making the data simple to access rather than optimizing storage. A data store that supports the data formats that are incorporated in the microservice’s interface will make the microservice simpler to implement with less conversion of the data. Likewise, a data store that enables the microservice to access the data–find it, update it–the way the microservice’s interface works will also simplify the microservice’s implementation.
Not only can microservices have separate storage, they can each have different kinds of storage. With Polyglot Persistence, each set of data is stored in the type of storage that is best suited to that data.
Example
To illustrate a microservices architecture with multiple microservices that each has its own self-managed data store, let’s consider an airline reservation system. An airline reservations application may have a microservice that enables the passenger to select seats on their flights. The seat selection microservice doesn’t read the seating chart for each flight and try to interpret it. Instead, a flights microservice manages each flight, including its seats, and the seat selector delegates to the flight manager. Some flights might be on different airlines. Each one encapsulates rules for when to show a seat as taken, not only because it’s been assigned to another passenger, but perhaps because this passenger is told that that a premium seat is unavailable, or that a block of seats is unavailable because it’s being held so that a family can sit together. The flight microservice manages its seats, the seat selection microservice enables the passenger to choose one they’re allowed to reserve and records that selection in its storage.
The microservices architecture for this airline reservations application might divide it into several microservices, as shown in Figure 4-23.
To make a reservation, a user logs in as a particular customer, purchases a ticket, and selects a seat. The microservices architecture is composed of several microservices to manage this functionality, such as Customer Management, Ticketing, Flights, and Seat Selector.
These multiple microservices could all share one massive data store containing several sets of data. Instead, each microservice includes its own Self-managed Data Store, so the solution includes multiple data stores, each of which is specialized for the data that one microservice needs and manages:
-
Customer Management has its own Customer Data, a data store containing data about Customers. Only the Customer Management microservice needs access to this Customers data.
-
Ticketing has its own Ticketing Data, a data store containing Tickets data. Ticketing is the only microservice with access to the Tickets data.
-
The Flights microservice has its own data store for Flight Data that contains two sets of data, Flights and Seating Charts, and only it can access that data. The Flights and Seating Charts data are encapsulated together in a single data store because part of Flights’ responsibilities is to keep the Flights and Seating Charts data consistent with referential integrity, so that every flight has exactly one seating chart and each seating chart is part of an existing flight’s data.
-
The Seat Selector microservice is pure functionality, so it doesn’t have any additional data and therefore doesn’t have its own data store.
The Seat Selector microservice enables the user to assign a seat by using Flights to find an available seat and using Ticketing to assign that seat to the customer. The Seat Selector microservice never accesses any of that data directly, it does so indirectly by invoking behavior in the other Flights and Ticketing microservices that do have direct access to the data. Flights and Ticketing are each responsible for keeping the data they manage consistent, there is no way for Seat Selector to make the data inconsistent.
Service Orchestrator
You are architecting a new application using Microservices or refactoring an existing application into microservices. Each microservice has a Service API and its own Self-Managed Data Store.
How does a microservice perform a complex task, one that is performed in multiple steps?
A task in a service API can be a complex task, one that a microservice performs in multiple steps. Buying a concert ticket requires reserving the seat, processing the payment, and delivering the entry pass. Planning how to ship a product means finding the customer’s shipping address, the product’s dimensions and weight, and the warehouses’ inventories. A bank transfer requires updating both accounts.
This seems like a simple problem to solve. Each step in a complex task should be a simpler task in a microservice. The complex microservice implements its task to invoke the simpler tasks, composing together multiple simpler tasks into the single complex task. For the complex task to be successful, all of the simpler tasks must complete successfully. The trick is for the microservice to perform the complex task such that it performs either all of the steps or none of them.
This problem might be simpler if all of the tasks were implemented in a single microservice, but that’s not the way microservices work. Because each microservice implements a single business capability, diverse subtasks may be distributed across multiple microservices, so the complex microservice invokes multiple simpler microservices.
Likewise, it might be easier if the microservices all shared a single database, but that’s also not the way microservices work, either. Each microservice is stateless and persists its state in a Self-Managed Data Store such as a database. Each microservice manages its own data and the persistance of that data. For one microservice to access another’s data, the one must delegate to the other and use its tasks that have access to the data. So a complex task not only incorporates multiple microservices but also incorporates their multiple databases.
Applications in traditional IT solve this problem using transactions. Data is kept immediately consistent; updates to the data transition it from one consistent state to another by making a complete set of changes. A simple transaction runs in a single resource and ensures that a set of updates either all commit at once or none of them do. A distributed transaction runs across multiple resources and ensures that the updates in all of the resources either all commit at once or none of them do. Transactions provide insurance that a set of changes is complete and the data is always consistent, but they have overhead that hurts performance, which can seem unnecessary when the majority of transactions never roll back. When an update in a transaction does fail, rolling back the transaction can be complex and may itself fail, requiring manual intervention to clean up the resources.
Cloud is rather inhospitable for performing transactions. When necessary, the cloud stops and restarts servers, making them unreliable by traditional IT standards. Cloud workloads are mobile and elastic, and as the platform relocates applications and resources, it stops and restarts them. To compensate for this, tasks need to be interruptable units of work that can start over and retry. Depending on transactions constantly rolling back successfully makes an application less reliable, not more so. To support retry, an application updates each resource separately.
Updating each resource separately fits naturally with performing a complex task as separate simple tasks in separate microservices with separate storage. Yet a complex microservice needs to perform these simple tasks as a unit, all of them or none of them.
Therefore,
Design a microservice that performs complex tasks as a service orchestrator that coordinates tasks in multiple simpler microservices.
A Service Orchestrator is a kind of microservice that performs a complex task as a unit, implementing the complex task by composing it out of simpler tasks. See Figure 4-24.
A Service Orchestrator provides tasks in a service API that are complex, requiring multiple steps to perform. A service orchestrator may consist of both complex tasks and simple tasks, and a task may combine other complex tasks as well as simple tasks.
A Service Orchestrator typically implements business functionality, making it a type of Domain Microservice. Because the Service Orchestrator does not implement the same business capability as the simpler microservices it orchestrates, it itself is not a Composite object because it does not have the same service API as the simpler microservices. The Service API hides the complex implementation, so a client doesn’t know whether the microservice with the business capability is implemented as a service orchestrator or as a more atomic domain microservice. A microservice that is first implemented as an atomic domain microservice could become more complex and evolve into a service orchestrator with the same service API and its clients would remain unchanged. A Service Orchestrator may require its own storage, such as for data it collects from and passes to its simpler tasks, so that an orchestration that is interrupted–such as when the cloud platform relocates it to a different server–can restart where it left off.
By performing a complex task as a unit of work, a Service Orchestrator ensures that either all of the steps are performed or none of them are. To accomplish this on the cloud without transactions, a Service Orchestrator can be implemented with any of three design strategies:
-
Orchestration microservice – The Service Orchestrator implements each step in a complex task by invoking another microservice task.
-
Consolidated microservice – Combine several microservices into a Service Orchestrator that can perform all of the steps itself.
-
Business process – Implement the Service Orchestrator as a business process that implements each step by invoking a task in another finer-grained microservice.
Let’s examine each of these strategies in more detail.
A Service Orchestrator implemented as an orchestration microservice simply calls other microservices. For this to work, the complex task and all of its subtasks must be either read-only or idempotent. Read-only means that the task does not change the state of any of the systems it interacts with, nor does it otherwise create any side-effects. An idempotent task produces an effect the first time is it run but when repeated produces no additional effects. Because the tasks are read-only or idempotent, if the entire orchestration microservice fails or restarts, it can simply start over and try again. The orchestration microservice’s client gets an error instead of a valid result, so it retries the complex task. If a subtask fails or is interrupted, the orchestration microservice retries it. In this way, the entire read-only complex task-inclusing its subtasks-can be retried until it succeeds.
As shown in Figure 4-25, the orchestration microservice implements the steps in its complex task to invoke tasks in other microservices. Each of those microservices uses its own database or other backend services to implement its task.
The key is that the tasks are all read-only, that the state of the databases after the tasks is the same as before the tasks. If one of the subtasks updated its database, but then the complex task failed to complete, it could restart, but then it would perform the successful subtask again and update its database twice, corrupting the data. A read-only task can be run repeatedly if needed without corrupting the data.
For example, an e-commerce task to prepare a product shipment needs to retrieve the customer’s shipping address, the product’s dimensions and weight, and the warehouse’s inventory. All of these tasks are read-only, so preparing a shipment can be implemented as an orchestration microservice.
An orchestration microservice is a perfectly valid strategy, but has the limitation that it only works with read-only data or idempotent tasks.
A Service Orchestrator implemented as a consolidated microservice implements the complex task and all of its subtasks as one microservice with one persistent backend service such as a database. This requires either designing a single microservice from the beginning, or refactoring existing code to merge several microservices into a single microservice. When merging microservices, the refactoring merges their data stores as well.
As shown in Figure 4-26, the consolidated microservice implements multiple tasks, and they all share the same database. The service API exposes the complex task, and can also expose any of the subtasks clients need to invoke individually.
The key is that all tasks update data in a single database via a single session that doesn’t commit until the end of the complex task. The complex task not only invokes the subtasks, it also combines all of their database updates into a single commit in their shared database. This way, if the microservice fails or restarts during its complex task, including any of its subtasks, the database never commits and its session rolls back. The microservice can then restart and retry as if this were the first attempt.
For example, a banking task that transfers funds from one account to another must make sure the updates occur in both accounts. To implement this service orchestration as a consolidated microservice, the complex transfer task and its simpler deposit and withdrawal tasks must all be implemented in a single microservice and the data for all of the bank accounts must all be stored in the same database.
A consolidated microservice is a perfectly valid strategy, but has the limitation that it must be designed from the beginning as a single microservice, or that multiple existing microservices can be merged without breaking their clients. The need for this refactoring is caused by defining too many microservices, each with a single business capability that is too narrowly focused, such that multiple microservices must collaborate to perform a complex task. Instead, design the microservice around a single business capability broad enough to perform complex tasks in the domain.
A Service Orchestrator implemented as a business process implements its complex task to run in a business process management (BPM) engine. The business process performs each step in the complex task by invoking a microservice task.
As shown in Figure 4-27, the business process consists of multiple steps, each of which invokes a task in another microservice, where each microservice uses its own database or other backend services to implement its task. The subtasks aren’t required to be read-only or idempotent, each one can read and change the data in its database. The BPM engine can expose the business process as a Service API, or a microservice can front the business process to expose it as a service API, kind of an Adapter Microservice.
The key is that the business process can perform each step as its own defacto transaction. It invokes each subtask individually; each microservice with a subtask thinks the business process is just any client invoking the task. The business process can keep track of which steps have completed successfully and can retry ones that fail. If the business process fails or restarts, it doesn’t repeat the steps that have already completed, it starts again where it was before. The business process might even restart on a different computer and perform the rest of its steps there. A business process can even reverse steps that have already completed by performing compensating transactions–a task that does the opposite of another task, usually implemented in the same microservice as a pair where the compensating task performs the reverse of the task. The business process works much like an orchestration microservice that ensures each step is only performed successfully once, so the steps can be read-write.
For example, the e-commerce application probably has separate microservices for capabilities like customer management, product management, warehouse inventory, shipping logistics, and payment processing. A complex task such as checkout (to purchase the items in a shopping cart) requires coordinating across simpler tasks in multiple microservices. Combining all of those specialized microservices into one generalized consolidated microservice would be a poor microservice architecture. Instead, the service orchestrator will need to implement the checkout task as a business process.
A business process is a perfectly valid strategy, but it requires a BPM engine. The microservice is implemented in the BPM engine or to delegate to the BPM engine.
When choosing among these three strategies (Orchestration Microservice, Consolidation, and Business Process) you need to consider which is most appropriate for your situation, looking at the pros and cons of each. The issue is that that once you make this choice you will often need to follow it for other similar situations in your design. Thus, you will make a choice at least for a subsystem of several microservices, if not for an entire system. An architect should consider each for the system as a whole and choose the most appropriate. For instance, you may choose to select an orchestration microservice as your first choice, and then later in a refactoring, change the architecture to use a BPM engine.
Designing a microservice as a Service Orchestrator enables it to perform complex tasks by combining other complex and simple tasks. Three design strategies handle read-only tasks, multiple tasks in a single microservice, and multiple microservices with read-write tasks.
A Service Orchestrator’s design is more complex than traditional IT code that simply performs multiple tasks. The later coding strategy either depends on transaction management, which the cloud does not provide, or worse yet just assumes that multiple updates will always succeed. Code from a traditional IT application migrated to cloud may run successfully, but without a transaction manager will not handle failures correctly.
Compared to simple microservices, Service Orchestrators can be more complex to design. Not only do they implement complex tasks, but those tasks need to be designed with a lot more care to ensure that they complete as a unit.
The design for a Service orchestrator must be aware of the Self-Managed Data Stores involved in performing the task. As long as only a single data store is involved, or the data stores are used read-only, the orchestrator’s design can be simplified. A complex task that uses multiple read-write data stores needs to be implemented as a business process.
To perform short-running tasks, relational databases rely heavily on locking data with read, write, and update locks. Locking in relational databases can cause performance bottlenecks and deadlocks as one session locks data that other sessions also require. Many Cloud Databases strive to avoid these performance problems by incorporating designs that avoid and minimize locking.
A business process service orchestrator can be hosted in a BPM engine, or can remotely invoke a process hosted in a BPM engine. When invoking the BPM engine remotely, the service orchestrator is an Adapter Microservice that reuses the business functionality residing in the BPM engine.
A service orchestrator orchestrates multiple tasks following a pre-defined plan. Alternatively, multiple tasks can be choreographed dynamically in an Event-Driven Architecture.
Since each service orchestrator runs in its own process, Polyglot Development enables developers to implement each service orchestrator class in a different language or technology.
Examples
As examples of the Service Orchestrator pattern, let’s examine how an e-commerce website could implement displaying a product’s availability using a service orchestrator with the orchestration microservice strategy and managing a purchase using a service orchestrator with the business process strategy. In between, we’ll look at a business process orchestrator implemented using an enterprise integration router.
E-commerce: Displaying availability
When displaying information about a product for sale, a shopping website might display not only details about the product but also whether the product is currently in stock. In a microservices architecture, there are may be one microservice for managing the catalog of products that the website sells and another microservice for managing the inventory currently stored in the warehouse and which of those items have already been sold to other customers. The website needs a single shopping microservice that can provide all the details needed to display a product, including whether the warehouse has any available. The e-commerce application can be architected like the solution shown in Figure 4-28.
The architecture contains the following microservices:
-
Shopping - A service orchestrator whose display product task is composed of two simpler tasks, product details and inventory.
-
Product Catalog - A domain microservice that implements the product details task.
-
Product Inventory - An adapter microservice that implements the inventory task by delegating to the Warehouse Management system of record.
Neither simpler microservice implements all of the functionality that the e-commerce application requires to display the product completely. The Shopping microservice doesn’t implement all of that functionality either, but it acts like it does, delegating to the simpler microservices and combining their functionality into the complete set that the e-commerce application requires.
The Shopping microservice is a service orchestrator. Because the product details and inventory tasks are read-only, as is the display product task that delegates to them, the Shopping microservice can be implemented using the orchestration microservice strategy. Simple code calls one task, then the other, then combines their data for display.
This basic MVP of a Shopping microservice could be expanded to provide additional information such as pricing in multiple currencies, dynamically fluctuating prices, delivery time estimates, and product ratings. The service orchestrator probably would not implement any of this functionality, instead delegating to other microservices that specialize in these functions.
Enterprise integration router
When integrating together multiple applications, the routing in the middle may function as service orchestration. When the application interfaces are more-or-less service oriented and a router combines them with its own interface that is more-or-less service oriented, then that’s a Service Orchestrator.
For example, a Composed Message Processor uses a Splitter to split a composite message into a series of individual messages, routes each individual message to its destination, and then uses an Aggregator to combine the results from the destinations into a single result for the original message. This process is some kind of orchestration, but it may or may not be service orchestration. With enterprise messaging, the messages can be very data-oriented, simply passing data between applications, which is not a good example of service orchestration. When the destinations’ interfaces are service APIs or kind of like service APIs, the message is a request that specifies what the destination should do but not how to do it, which typically means that the original composite message is also a request. Likewise, the result message for a request is typically a response. A request split into smaller requests and responses merged into an overall response fits the Request-Reply pattern, which makes the router a service orchestrator.
Figure 4-29 shows the solution for a Composed Message Processor that acts as a Service Orchestrator.
The router has a Service API with a validate task whose request takes a New Order as a parameter and whose reply returns a Validated Order. That is the service orchestrator’s API. The router orchestrates two services, Widget Service and Gadget Service, which also have service APIs. The router combines their specialized capabilities into a general capability, which is service orchestration.
Another enterprise integration routing pattern, Process Manager, is an even more sophisticated message router that performs a predefined set of steps which can vary based on intermediate results. If it uses service interfaces, this is a service orchestrator, specifically the business process strategy. The business process strategy is often implemented in a business process engine, as shown in the next example.
E-commerce: Managing a purchase
When processing a purchase, a shopping website needs to make sure to perform two main tasks: Ship the product to the customer and gather payment from the customer. Both tasks are part of a single logical business transaction, either both succeed or neither should be performed. If one fails, it needs to be retried until it succeeds. If one succeeds but the other cannot be completed successfully, the first one needs so be undone, often by executing a compensating transaction that performs the opposite steps. The compensating transaction must complete successfully.
A good approach to perform multiple tasks as a single transaction is to implement the transaction as a business process, which will make the service orchestrator one implemented using the business process strategy. Figure 4-30 shows a microservices architecture with a business process (using the notation for UML activity diagrams) for performing the checkout task in a Purchasing service.
The checkout task performs four activities: receive order, fill order, send invoice, and close order. The Purchasing service implements the fill order activity by delegating to another microservice, Order Fulfillment and its ship product task. Likewise, it implements its send invoice activity by delegating to Payment Processing’s charge credit card task.
This design makes Purchasing a service orchestrator, a microservice that composes its functionality from the simpler Order Fulfillment and Payment Processing microservices. Purchasing is implemented as a business process so that it can run in a BPM engine, which can run fill order and send invoice concurrently and ensure that both complete successfully before running close order. If the BPM engine cannot complete one of the concurrent activities successfully and the other concurrent activity has already completed successfully, the BPM engine will automatically run a compensating transaction to reverse the successful one, often by performing the successful activity in reverse. The Purchasing service is much easier to implement because it can delegate all of this process management to the BPM engine, which is specialized to perform business processes reliably for any application.
A Service Orchestrator can delegate to other service orchestrators. For example, checkout delegates to ship product and charge credit card. While charge credit card may be an atomic task–either it succeeds or it fails–the ship product task may be multistep and so Order Fulfillment may also be implemented as a service orchestrator. In which case the Purchasing service, a service orchestrator, delegates to Order Fulfillment, which is implemented as another service orchestrator.
Conclusion: Wrapping up Microservices Architecture
This chapter discussed how best to build applications that work the way cloud does: small components that can be replicated easily and distributed across infrastructure. Microservices architecture combines Cloud-Native Architecture with a Distributed Architecture, forming an architecture that makes an application work the way cloud does. The architecture structures an application to model a domain as set of Microservices, components that collaborate through well-defined Service API interfaces, where each implements a single complete business capability, manages its own data, is deployed independently, runs independently, and can be developed by a small development team working independently from others.
The main type of microservice is a Domain Microservice, what tutorials usually mean when they say “microservice.” A Domain Microservice, like a domain object in object-oriented programming, models and simulates a single business capability in an enterprise’s business domain. Each domain microservice is designed to work the way the domain does, to model and automate the business’ rules and logic. This kind of microservice offers the most design flexibility: the business analysts and programmers have complete control to subdivide the domain into what they consider individual capabilities, to scope those, assign them meaningful functionality, and invent an interface for clients to access that functionality.
Another type of microservice is an Adapter Microservice, a type that is less-commonly documented but very necessary for a microservices architecture that needs to work with existing systems. An Adapter Microservice wraps an existing system of record or other external SaaS service and includes it as another microservice in the architecture. Whereas a development team has extensive flexibility to design a Domain Microservice as a complete business capability, an Adapter Microservice’s scope of functionality is dictated by that of the existing system it wraps. To the extent the existing system’s functionality isn’t a very good business capability or its interface isn’t very service-oriented, the Adapter Microservice won’t be either. The microservice can try to transform the existing system’s capability, functions, and data structures into a well-designed microservice, but the microservice may be little more than a thin veneer around an artifact from a bygone era that nevertheless is still running in the enterprise, providing important business functionality. That functionality should be incorporated into the microservices architecture as-is, and an Adapter Microservice provides the means to do so.
A Service Orchestrator is a composite type of microservice that composes its functionality from other microservices, mostly domain microservices, as well as adapter microservices and even other service orchestrators. Each microservice has a different interface that abstracts the functionality it provides. By combining the functionality of other microservices, a service orchestrator makes bigger parts from smaller ones. A service orchestrator is in a position to manage transactions, bringing transactionality to microservices architecture and cloud-native architecture that otherwise doesn’t support transactions. Service orchestrators lend themselves to implementing business processes and are often implemented as business processes in business process management (BPM) engines.
A special kind of microservice is the Dispatcher. It forms the interface between a client outside the microservices architecture and the ever-changing multitude of microservices inside the architecture. Whereas other types of microservices are composable and appear throughout the architecture, dispatchers are only positioned on the edge of the architecture with the external clients. Microservices delegate to other microservices, but not to Dispatchers, and Dispatchers do not delegate to other dispatchers, only to microservices. Yet like other microservices, each Dispatcher can be developed independently, deployed independently, and can be replicated and distributed across multiple computers. It fails independently, and can even scale independently although dispatchers rarely need to. Whereas microservices provide business capabilities, a Dispatcher provides whatever functionality its client requires, modeling the client’s capabilities more than the domain’s, and implementing its functionality using whatever microservices it needs in the architecture. Like other microservices, each Dispatcher can be developed by a separate team, one that usually also develops the client that uses the Dispatcher.
A microservices architecture supports Polyglot Development, where each development team gets to choose the language for implementing their microservice. Monolithic applications are usually monolingual, meaning that all of the code has to be written in one language because it all runs in the monolith’s runtime. Because each microservice runs in its own process with its own Application Package runtime, separate microservices can have different runtimes and can therefore be written in different languages. Typical cloud-friendly languages for implementing microservices include Node.js, Java, Go (a.k.a. Golang), and Python. Dispatchers are often implemented in Node.js and domain microservices are often implemented in Java. Adapter microservices may be implemented in Java to use the connector architecture (JCA), but can also be implemented using enterprise service bus (ESB) technologies for adapting service interfaces onto legacy systems of record. Service orchestrators can be implemented in programming code but can sometimes be implemented better using a business process management (BPM) engine. If one language or technology is the best fit for all of the microservices in an architecture, use it; but the architecture also supports implementing each microservice in whatever language or technology works best for its requirements, regardless of what is used to implement other microservices.
Each microservice should persist its data in its own Self-Managed Data Store. Replicas of a microservice all share the same data store. Microservices support Polyglot Persistence, where separate microservices can choose different types of data stores that best fit each microservice’s requirements. Only a microservice can access the data in its data store. The rest of the application can only access a microservice’s data by invoking that microservice’s interface. The microservice has complete control to decide the format of its data and to evolve that format as necessary, and to manage the data’s consistency according to the rules of the microservice’s capability.
Microservices can run in a traditional IT environment or in a cloud environment. While cloud is not required, cloud services like lifecycle management, load balancing, and autoscaling make microservices much easier to deploy and manage.
Next, we’ll explore how to design microservices, especially Domain Microservices. It’s easy to say to separate a business domain into individual business capabilities and that each microservice should model a single business capability, but much more difficult to analyze a business domain and figure out what those individual business capabilities are so that they can be modeled. Microservice Design shows how to use the event storming technique and Domain-Driven Design concepts to model a domain and discover its individual microservices.
After that, we’ll look at Event-Driven Architecture, an alternative to centrally-controlled service orchestration that uses events to choreograph microservices in dynamically discovered interactions. After that, Cloud-Native Storage explores how to persist and manage data in a distributed and unreliable cloud environment, and Cloud Application Clients describes a range of options for making the user interfaces (UIs) that people will use to interact with cloud applications.
Get Cloud Application Architecture Patterns now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.