From Microservices in .NET, 2nd Edition by Christian Horsdal Gammelgaard
Find out exactly what a microservices architecture is in this article.
This book focuses on designing and implementing individual microservices, but it’s worth noting that the term microservices can also be used to describe an architectural style for an entire system consisting of many microservices. Microservices as an architectural style is a lightweight form of service-oriented architecture (SOA) where the services are tightly focused on doing one thing each and doing it well. A system with a microservices architecture is a distributed system with a (probably large) number of collaborating microservices.
The microservices architectural style has quickly been gaining in popularity for building and maintaining complex server-side software systems. And understandably so: microservices offer a number of potential benefits over both more traditional service-oriented approaches and monolithic architectures. Microservices, when done well, are malleable, scalable, and robust, and they allow for systems that do well on all four of the key metrics identified by Nicole Forsgren et al. in Accelerate and the DORA state of DevOps reports, namely:
- Deployment frequency
- Lead time for changes
- Time to restore service
- Change failure rate
This combination often proves elusive for complex software systems. Furthermore it is, as documented by Forsgren et al., a reliable predictor of software delivery performance.
In my book you will learn how to design and implement malleable, scalable and robust microservices which form systems that deliver on all four of the key metrics above.
I’ve said that a microservice is a service with a very narrowly focused capability, but what exactly does that mean? Well, there’s not a broadly accepted definition in the industry of precisely what a microservice is. We can, however, look at what generally characterizes a microservice. I’ve found there to be six core microservice characteristics:
- A microservice is responsible for a single capability.
- A microservice is individually deployable.
- A microservice consists of one or more processes.
- A microservice owns its own data store.
- A small team can maintain a few handfuls of microservices.
- A microservice is replaceable.
This list of characteristics should help you recognize a well-formed microservice when you see one, and it will also help you scope and implement your own microservices. By incorporating these characteristics, you’ll be on your way to getting the best from your microservices and producing a malleable, scalable, and robust system as a result. Throughout my book, I’ll show how these characteristics should drive the design of your microservices and how to write the code that a microservice needs to fulfill them. Now, let’s look briefly at each characteristic in turn.
Responsible for a single capability
A microservice is responsible for one and only one capability in the overall system. We can break this statement into two parts:
- A microservice has a single responsibility.
- That responsibility is for a capability.
The Single Responsibility Principle has been stated in several ways. One traditional form is, “A class should have only one reason to change.” Although this way of putting it specifically mentions a class, the principle turns out to apply beyond the context of a class in an object-oriented language. With microservices, we apply the Single Responsibility Principle at the service level.
Another, newer, way of stating the single responsibility principle, also from Robert C. Martin, is as follows: “Gather together the things that change for the same reasons. Separate those things that change for different reasons.” This way of stating the principle applies to microservices: a microservice should implement exactly one capability. That way, the microservice will have to change only when there’s a change to that capability. Furthermore, you should strive to have the microservice fully implement the capability, so that only one microservice has to change when the capability is changed.
There are two types of capabilities in a microservice system:
- A business capability is something the system does that contributes to the purpose of the system, like keeping track of users’ shopping carts or calculating prices. A good way to tease apart a system’s separate business capabilities is to use domain-driven design.
- A technical capability is one that several other microservices need to use—integration to some third-party system, for instance. Technical capabilities aren’t the main drivers for breaking down a system to microservices; they’re only identified when you find several business-capability microservices that need the same technical capability.
A microservice should be individually deployable. When you change a particular microservice, you should be able to deploy that changed microservice to the production environment without deploying (or touching) any other part of your system. The other microservices in the system should continue running and working during the deployment of the changed microservice and continue running once the new version is deployed.
Consider an e-commerce site. Whenever a change is made to the Shopping Cart microservice, you should be able to deploy just that microservice, as illustrated in figure 1. Meanwhile, the Price Calculation microservice, the Recommendation microservice, the Product Catalog microservice, and others should continue working and serving user requests.
Figure 1. Other microservices continue to run while the Shopping Cart microservice is being deployed.
Being able to deploy each microservice individually is important because in a microservice system, there are many microservices, and each one may collaborate with several others. At the same time, development work is done on some or all of the microservices in parallel. If you had to deploy all or groups of them in lockstep, managing the deployments would quickly become unwieldy, typically resulting in infrequent and big, risky deployments. This is something you should definitely avoid. Instead, you want to be able to deploy small changes to each microservice frequently, resulting in small, low-risk deployments.
To be able to deploy a single microservice while the rest of the system continues to function, the build process must be set up with the following in mind:
- Each microservice must be built into separate artifacts.
- The deployment process must also be set up to support deploying microservices individually while other microservices continue running. For instance, you might use a rolling deployment process where the microservice is deployed to one server at a time, in order to reduce downtime.
The fact that you want to deploy microservices individually affects the way they interact. Changes to a microservice’s interface usually must be backward compatible so other existing microservices can continue to collaborate with the new version the same way they did with the old. Furthermore, the way microservices interact must be robust in the sense that each microservice must expect other services to fail once in a while and must continue working as best it can. One microservice failing—for instance, due to downtime during deployment—must not result in other microservices failing, only in reduced functionality or slightly longer processing time.
Consists of one or more processes
A microservice must run in a separate process, or in separate processes, if it’s to remain as independent as possible of other microservices in the same system. The same is true if a microservice is to remain individually deployable. Breaking that down, we have two points:
- Each microservice must run in separate processes from other microservices.
- Each microservice can have more than one process.
Consider a Shopping Cart microservice again. If it ran in the same process as a Product Catalog microservice, as shown in figure 2, the Shopping Cart code might cause a side effect in the Product Catalog. That would mean a tight, undesirable coupling between the Shopping Cart microservice and the Product Catalog microservice; one might cause downtime or bugs in the other.
Figure 2. Running more than one microservice within a process leads to high coupling between the two: They cannot be deployed individually and one might cause downtime in the other.
Now consider deploying a new version of the Shopping Cart microservice. You’d either have to redeploy the Product Catalog microservice too or need some sort of dynamic code-loading capable of switching out the Shopping Cart code in the running process. The first option goes directly against microservices being individually deployable. The second option is complex and at a minimum puts the Product Catalog microservice at risk of going down due to a deployment to the Shopping Cart microservice.
Speaking of complexity, why should a microservice consist of more than one process? You are, after all, trying to make each microservice as simple as possible to handle.
Let’s consider a Recommendation microservice. It implements and runs the algorithms that drive recommendations for your e-commerce site. It also has a database that stores the data needed to provide recommendations. The algorithms run in one process, and the database runs in another. Often, a microservice needs two or more processes so it can implement everything (such as data storage and background processing) it needs in order to provide a capability to the system.
Owns its own data store
A microservice owns the data store where it stores the data it needs. This is another consequence of a microservice’s scope being a complete capability. Most business capabilities require some data storage. For instance, a Product Catalog microservice needs some information about each product to be stored. To keep Product Catalog loosely coupled with other microservices, the data store containing the product information is completely owned by the microservice. The Product Catalog microservice decides how and when the product information is stored. As illustrated in figure 3, other microservices, such as Shopping Cart, can only access product information through the interface to Product Catalog and never directly from the Product Catalog data store.
Figure 3. One microservice can’t access another’s data store.
The fact that each microservice owns its own data store makes it possible to use different database technologies for different microservices depending on the needs of each microservice. The Product Catalog microservice, for example, might use SQL Server to store product information; the Shopping Cart microservice might store each user’s shopping cart in Redis; and the Recommendations microservice might use an ElasticSearch index to provide recommendations. The database technology chosen for a microservice is part of the implementation and is hidden from the view of other microservices.
This approach allows each microservice to use whichever database is best suited for the job, which can also lead to benefits in terms of development time, performance, and scalability. The obvious downside is the need to administer, maintain, and work with more than one database, if that’s how you choose to architect your system. Databases tend to be complicated pieces of technology, and learning to use and run one reliably in production isn’t free. When choosing a database for a microservice, you need to consider this trade-off. But one benefit of a microservice owning its own data store is that you can swap out one database for another later.
Maintained by a small team
So far, I haven’t talked much about the size of a microservice, even though the micro part of the term indicates that microservices are small. I don’t think it makes sense to discuss the number of lines of code that a microservice should have, or the number of requirements, use cases, or function points it should implement. All that depends on the complexity of the capability provided by the microservice.
What does make sense, though, is considering the amount of work involved in maintaining a microservice. The following rule of thumb can guide you regarding the size of microservices: a small team of people—five, perhaps—should be able to maintain a few handfuls of microservices. Here, maintaining a microservice means dealing with all aspects of keeping it healthy and fit for purpose: developing new functionality, factoring out new microservices from ones that have grown too big, running it in production, monitoring it, testing it, fixing bugs, and everything else required. Depending on the volume of change in the microservices a few handfuls can mean anything 10 to 30 microservices or even more when the system, the tooling, and the automation are mature and effective.
For a microservice to be replaceable, it must be able to be rewritten from scratch within a reasonable time frame. In other words, the team maintaining the microservice should be able to replace the current implementation with a completely new implementation and do so within the normal pace of their work. This characteristic is another constraint on the size of a microservice: if a microservice grows too large, it will be expensive to replace; but if it’s kept small, rewriting it is realistic.
Why would a team decide to rewrite a microservice? Perhaps the code is a big jumble and no longer easily maintainable. Perhaps it doesn’t perform well enough in production. Neither is a desirable situation, but changes in requirements over time can result in a codebase that it makes sense to replace rather than maintain. If the microservice is small enough to be rewritten within a reasonable time frame, it’s OK to end up with one of these situations from time to time. The team does the rewrite based on all the knowledge obtained from writing the existing implementation and keeping any new requirements in mind.
If you want to learn more, check out the book on Manning’s liveBook platform here.