Introduction

Most people think a microservice architecture is good for building scalable applications. This isn’t false but we should have a closer look at which dimension this architecture style scales at its best. The dimension that comes into people’s mind first is the dimension of load. This means it should be possible to add additional resources to keep the performance the same when workload increases. In fact, this is not for what microservices are good for in the first place.

The dimension the microservice architecture scales is the functional dimension. In other words, it is easy to introduce new functions and qualities at any stage of the product. This leads to the consideration that it’s simply possible to react to imminent load problems with new requirements.

This articles explain the purposes and risks of microservice architectures. It further gives some hints on how an architecture must look like to met these purposes. At some points it also compares microservice to traditional service oriented architectures (SOA).

Purpose 1: Minimize Costs of Change

Purpose

Microservices are intended to scale according to new requirements.

Minimizing the costs for new or changed requirements is the major purpose of the microservice architecture style. This benefit comes directly from the “single responsibility principle”. The microservices pattern prescribe radical rules to enforce this principle in order to maximize its benefits.

To prevent that code is added where it doesn’t belong to and to ensure changeability, the microservice pattern recommends that:

different responsibilities are placed in different services

each service has its own code repository

each instance of a microservice is executed as a dedicated process

inter service communication is only allowed through a network connection. In addition, services must use its official API to talk to each other: it is not allowed that one service access the data of database that has been written by another service. Each service must have its own logical database schema!

the protocol used for communication must be technology agnostic (interoperable). This means a service should not assume that another service is written in a specific programming language.

choreography should be preferred over orchestration

Some of these principles might remind us on a traditional service oriented architecture (SOA). In fact, each microservice architecture is a service oriented architecture, but not the other way around. There are some discussions whether
microservices are SOA done right.

The difference that makes a service oriented architecture a microservice architecture is, you guessed it, a smaller services size and lightweight protocols. The smaller service size is a result of applying the first principle from the list above: place different responsibilities into different services.

Later in this article, we’ll have a closer look on how to decide which responsibilities goes together into one service and which should be better separated. For now, we assume that a well done microservice architecture is in place and examine how each of the rules above contributes to a lower
costs of change.

Enforced cohesion because of hard system boundaries

Cohesion in software architecture is a measure how related the responsibilities of a module are [SAiP, S. 121]. So when a module has responsibilities that are strongly related to each other, this module has a high cohesion.

Attention: Don’t fall into the trap and put related responsibilities into different services!

Because different responsibilities are placed in different code repositories, it is much harder to place new code into modules to which they don’t belong to by design. This is a common problem in traditional applications:

Developers will eventually ignore or oversee boundaries of subsystems due to time pressure or insufficient understanding of the larger code base. As a consequence of that, related logic is distributed across system boundaries and it is much harder to maintain and extend the software. Subsequently the test suite gets also more complex and this is another reason why it's harder to ensure that everything is still working after a code change.

Lower coupling because of high cohesion

"Low coupling often correlates with high cohesion, and vice versa"
[SADCW]. In a good designed microservice architecture the dependencies between services are minimized. One reason for that is the same as for the enforced cohesion:

It is hard for developers to introduce new communication paths without to talk to the developers of the other services. Even if there is only one developer involved, he will be more thoughtful in introducing new dependencies between two services then between modules within the same code base.

Lower coupling because of choreography

Another driver for lower coupling is that in a microservice architecture choreography is preferred over orchestration. When a service oriented architecture uses an orchestration pattern for communication, there are point-to-point connection between the services. Point-to-point means that one service calls the API of another service which results in a web of communication paths between all services. Integrating, changing or removing services from this web is hard since you have to be aware of each connection between the services.

On the left site an orchestration with point-to-point conncetions is shown.
On the right site a choreography pattern is shown where each service waits for events to act on.
Orchestration vs. Choreography, Source:
www.thoughtworks.com

Applying a choreography pattern means that one service doesn’t talk to another service in order to instruct an action. Instead each service is observing its environment and acts on events autonomous. In real live this looks like this:

Services are connected to a message bus and subscribe channels they are interested in. Once an event series occurs that matters of the service, the service performs the appropriate action. Now it is easy to add new services to the architecture; You simply have to connect them to the message bus. In the worst case you must ensure that the other services emit the events that the new service requires. But adding addional events or extending the payload of existing events won’t break existing logic.

Note: When it comes to create/read/update/delete entities the REST protocol should still be a consideration. The service that handles the REST call could then trigger an event that a new entity was created/updated/deleted.

Applying another rule from the list above will result in an even lower coupling: When technology agnostic protocols are used, different services can be developed in different technologies. So it is possible to pick the best suited technologies for each service/team.

In the end a lower coupling allows to replace, remove or add new services when requirements change without having the fear that some communication paths have been oversee. So, a lower coupling allows to make changes to the architecture at a later state with less effort.

Smaller and cleaner code bases because of separated services

Each service has a small code base and so it is easier for developers to extend or modify a service. Even when a developer hasn’t worked on a service for a while or when a developer is new in the project, the smaller code bases makes it possible to be productive right from the beginning.

In addition, removing dead code to clean up the code base is much simpler in smaller code bases. In traditional applications developers are worried about removing unused code because they fear site effects. Furthermore, because of the simplicity the likelihood to introduce errors is also reduced.

Interim conclusion

Because a microservice architecture is made for adding requirements at any time this architecture style is very good fit for agile development processes. It is possible to implement a minimum viable product, deliver it to the user and than extend the system over time.

"Applying a microservice architecture is not about building the perfect system instead it is about to build a framework in which a good system can emerge over time as the understanding grows." [bms, S. 15]

However, a still existing problem is that responsibilities are assigned to the wrong service by design. When it comes to design a microservice architecture many people don’t know how to divide a monolithic problem into multiple (micro)services. And in fact, when the services are to big or to small, the advantages are gone and problems arise. This then bothers developers and also the managers who decided to invest into this architecture style.

Hint: Making services too small is common pitfall. This antipattern is called “nanoservices”.

Purpose 2: Encourage Generalization, Replaceability and Reuse

Purpose

In microservices the lesson learned from SOA are applied to fulfil the promise of reuse.

Reusing services is an old idea and in service oriented architecture (SOA) this is a fundamental goal. The microservice architecture style promotes the following principle to reach the best reusability for a single service:

Build smaller services doing one thing well. This is similar to the rule from the section above: Different responsibilities should be placed in different services.

A typical show stopper in discussion about reusing an existing service is its complexity and that the existing solution does much more then actual required. Often you hear statements like “this is a simple problem we better build our own solution instead of learning how to use an existing one”. To make things worse a complex software goes along with a complex documentation. How often did you hear a colleague complaining about a documentation?

There are some rules of thumb out there how small a microservice should be:

These are good rules of thumb and one could argue that these are extreme examples. Two weeks two rewrite a service is a desirable time range but according to the pizza example I must always work in a one-man team.

To get a well designed microservice architecture we should not ask how big a service should be, instead we should ask: which responsibilities should go into the same service?

My opinionated answer to this is that responsibilities should be grouped in a way so that the amount and size of domain specific services is minimized. Domain specific services, in this context, are services that are specific to the problem domain and that it's unlikely to find a scenario for reusing them in other projects.

Conversely this means: The amount of reused or reusable services should be maximized.

In order master that challenge, I like to think of two dimensions of decomposition.

Decomposition Dimension 1: Actions on entities

Actions that requires access to the same database records should go into the API of same service. For example, creating, reading, updating and deleting an entity (e.g. a user) should be provided by the API of the same service.

Recap: It’s a no-go that more then one service accesses the same database since it breaks up cohesion (see section ‘Purpose 1: Minimize Costs of Change’). When a services A requires information of service B, service A must use the API of service B.

Furthermore, entities that have a strong relation to each other are good candidates to be managed by the same services. From the perspective of reusing a service: When you can’t think of a scenario where you use an entity without the other, it’s an indication that those entities should be better managed by the same service.

Decomposition Dimension 2: Aspects of an action

Just because an action is provided by the API of one service it doesn’t mean that every aspect of that action must be executed by that service. It is often possible to decompose a domain specific action into one or more generic actions. In such cases we should think to leave those aspects of an action to other services.

A simple example: When a new user signs up at an online shop, an email to this user should be send. It is a good idea to use an email service to send out those emails. Why? In case the layout of all emails should be changed each service that sends emails must be changed. Sending emails is such a generic problem that there are tons of services out there that can be reused.

A more complex example: When a customer of an online shops views a product the shop system should remember this in order to generate user specific advertisements. In this example, we have a service that manages products. Putting the logic to generate user specific advertisements would blow up the complexity of the product service. Instead we should make a research which off-the shelf software can perform such user specific advertisements and use this system along with the product service. Whenever product information gets requested by an user the product service will notify the advertisement system. FYI: When an user opens a page on amazon about 200 services are called to perform this action (see [WV]).

Another example: Imagine a fitness tracker wristband that records and analyzes your vitality. Because of the limited disk space and computation power, it sends your vitality data (and maybe your current position) to a microservice architecture. One service acts as an endpoint and is responsible for receiving the data records from the wristbands. To keep the first service simple, this service is just responsible to receive and dispatch the data records. To store and analyze the heart rate we need something like a “heart rate service” that is able to calculate the average heart rate of a given time period in the past (because we want to display this information to the user). Making a generalization step before implementing a heart rate service can save us a lot of work. The heart rate service is actually a time series database that can be used off the shelf.

Purpose 3: Increase Operations Efficiency

Purpose

Only scale the bottlenecks.

In sum the footprint of an microservice architecture is usually bigger then the footprint of a monolithic application. This is because in a microservice architecture each service must run as a separated process. This means each instance of a service requires its own runtime (e.g. JVM, Ruby interpreter, etc.).

But there is a break-even point when it comes to horizontal scaling. When scaling a monolithic application horizontally you must install the whole monolith multiple times. This is a kind of waste because often only a small part of the application becomes the bottleneck. With a microservice architecture you can only scale those part that actually have performance issues. For systems under constant high load the hardware resources are cheaper when a microservice architecture is in place.

Risk 1: Increase Operations Complexity

Risk

One disadvantage of the microservice architecture is obvious: It is necessary to operate much more applications than just one or two. Mature organizations use container (e.g. Docker), PaaS (e.g.
Cloud Foundry)
technologies and continuous delivery methodologies to mitigate this drawback.

Risk 2: Distributed Monolith

In case your architecture has a bad cohesion you will multiply all problems by distributing your application across different microservices (see [JS]).