Microservice architecture is not a silver bullet

Peter Gillich
10 min readNov 7, 2022

--

Nowadays most of self-respecting architects prefers microservice architecture. But it’s not a silver bullet. I’ve already worked in the era of Service-Oriented Architecture and Layered Architecture, so the Microservice Architecture is only a today famous concept for me, which will be precipitated by a new concept in the future. Moreover, Uncle Bob — who is more experienced than me — stated in his book Clean Architecture:

Further evidence that there are no more such paradigms is that they were all discovered within the ten years between 1958 and 1968. In the many decades that have followed, no new paradigms have been added.

I born too late :-(

source: Microservices vs Monolithic architecture

Introduction

Microservice architecture is a good concept but it’s not the best for everything. Sometime the monolith or another concept is better.

It’s a quirky article. It contains a lot of citations from Uncle Bob and Chris Richardson. They describe the drawbacks of microservice architecture better than me. The book Microservices Patterns describes the microservice architecture more deeply and suggests solutions for the problems, so most of citations are derived from there.

I hope citations of this books take a liking to read it.

Microservice does not equal to architecture

Decoupling the whole thing to smaller independent components is more important for Uncle Bob. These components can be called as “micro-services”, for example, let’s see a citation from him again:

Many architects call such components “services” or “micro-services,” depending upon some vague notion of line count. Indeed, an architecture based on services is often called a service-oriented architecture.

If that nomenclature set off some alarm bells in your mind, don’t worry. I’m not going to tell you that SoA is the best possible architecture, or that micro-services are the wave of the future. The point being made here is that sometimes we have to separate our components all the way to the service level.

Remember, a good architecture leaves options open. The decoupling mode is one of those options.

Sometime Uncle Bob has simplified direct opinion, but I agree with him about above citations. The main point is a good architecture, which fulfills SOLID principles (for example). Deployment and orchestration is less important, if the architecture is good. See a citation from Uncle Bob:

A good architecture will allow a system to be born as a monolith, deployed in a single file, but then to grow into a set of independently deployable units, and then all the way to independent services and/or micro-services. Later, as things change, it should allow for reversing that progression and sliding all the way back down into a monolith

A good architecture protects the majority of the source code from those changes.
It leaves the decoupling mode open as an option so that large deployments can
use one mode, whereas small deployments can use another.

Kubernetes, the most famous microservice orchestrator is closed to monolith, not distributed to dozens of small microservices…

The dark side of microservices

Microservice architecture has a lot of benefits, but sometime it’s like a shrew. Let’s see citations from book Microservices Patterns:

1.1.2 The benefits of the monolithic architecture

The monolithic architecture looks good, but…

In the early days of FTGO, when the application was relatively small, the application’s monolithic architecture had lots of benefits:

* Simple to develop — IDEs and other developer tools are focused on building a single application.

* Easy to make radical changes to the application — You can change the code and the database schema, build, and deploy.

* Straightforward to test — The developers wrote end-to-end tests that launched the application, invoked the REST API, and tested the UI with Selenium.

* Straightforward to deploy — All a developer had to do was copy the WAR file to a server that had Tomcat installed.

* Easy to scale — FTGO ran multiple instances of the application behind a load balancer.

Over time, though, development, testing, deployment, and scaling became much more difficult. Let’s look at why.

1.1.3 Living in monolithic hell

The simple monolithic way goes to hell, because the architects and developers aren’t obliged to make clean architecture…

A major problem with the [monolithic] FTGO application is that it’s too complex. It’s too large for any developer to fully understand. As a result, fixing bugs and correctly implementing new features have become difficult and time consuming. Deadlines are missed.

To make matters worse, this overwhelming complexity tends to be a downward spiral. If the code base is difficult to understand, a developer won’t make changes correctly. Each change makes the code base incrementally more complex and harder to understand. The clean, modular architecture shown earlier in figure 1.1 doesn’t reflect reality. [monolithic] FTGO is gradually becoming a monstrous, incomprehensible, big ball of mud.

[monolithic] FTGO has partially adopted agile. The engineering team is divided into squads and uses two-week sprints. Unfortunately, the journey from code complete to running in production is long and arduous. One problem with so many developers committing to the same code base is that the build is frequently in an unreleasable state. When the [monolithic] FTGO developers tried to solve this problem by using feature branches, their attempt resulted in lengthy, painful merges. Consequently, once a team completes its sprint, a long period of testing and code stabilization follows.

Another reason it takes so long to get changes into production is that testing takes a long time. Because the code base is so complex and the impact of a change isn’t well understood, developers and the Continuous Integration (CI) server must run the entire test suite. Some parts of the system even require manual testing. It also takes a while to diagnose and fix the cause of a test failure. As a result, it takes a couple of days to complete a testing cycle.

1.5.2 Drawbacks of the microservice architecture

The microservice way solves a few things, but introduces new problems…

DISTRIBUTED SYSTEMS ARE COMPLEX

Another issue with using the microservice architecture is that developers must deal with the additional complexity of creating a distributed system. Services must use an interprocess communication mechanism. This is more complex than a simple method call. Moreover, a service must be designed to handle partial failure and deal with the remote service either being unavailable or exhibiting high latency.

Implementing use cases that span multiple services requires the use of unfamiliar techniques. Each service has its own database, which makes it a challenge to implement transactions and queries that span services. As described in chapter 4, a microservices-based application must use what are known as sagas to maintain data consistency across services. Chapter 7 explains that a microservices-based application can’t retrieve data from multiple services using simple queries. Instead, it must implement queries using either API composition or CQRS views.

The microservice architecture also introduces significant operational complexity. Many more moving parts — multiple instances of different types of service — must be managed in production. To successfully deploy microservices, you need a high level of automation.

1.6.1 Microservice architecture is not a silver bullet

So, we are in trouble :-(

Microservices are not immune to the silver bullet phenomenon. Whether this architecture is appropriate for your application depends on many factors. Consequently, it’s bad advice to advise always using the microservice architecture, but it’s equally bad advice to advise never using it. As with many things, it depends.

The underlying reason for these polarized and hyped arguments about technology is that humans are primarily driven by their emotions. Jonathan Haidt, in his excellent book The Righteous Mind: Why Good People Are Divided by Politics and Religion (Vintage, 2013), uses the metaphor of an elephant and its rider to describe how the human mind works. The elephant represents the emotion part of the human brain. It makes most of the decisions. The rider represents the rational part of the brain. It can sometimes influence the elephant, but it mostly provides justifications for the elephant’s decisions

1.6.3 Overview of the Microservice architecture pattern language

We have to spend more time for deployment, test and troubleshooting. See my earlier article: The Three Thirds: the cost model of modern SW development

In comparison [to monolith], deploying a microservices-based application is much more complex. There may be tens or hundreds of services that are written in a variety of languages and frameworks. There are many more moving parts that need to be managed.

In contrast [to monolith], understanding and diagnosing problems in a microservice architecture is much more complicated. A request can bounce around between multiple services before a response is finally returned to a client. Consequently, there isn’t one log file to examine. Similarly, problems with latency are more difficult to diagnose because there are multiple suspects.

The microservice architecture makes individual services easier to test because they’re much smaller than the monolithic application. At the same time, though, it’s important to test that the different services work together while avoiding using complex, slow, and brittle end-to-end tests that test multiple services together.

3.1.3 Evolving APIs

Managing a bunch of API versions parallel is nightmare.

In a microservices-based application, changing a service’s API is a lot more difficult. A service’s clients are other services, which are often developed by other teams. The clients may even be other applications outside of the organization. You usually can’t force all clients to upgrade in lockstep with the service. Also, because modern applications are usually never down for maintenance, you’ll typically perform a rolling upgrade of your service, so both old and new versions of a service will be running simultaneously.

4 Managing transactions with sagas

Distributed transaction handling is a challenge.

Mary discovered that, as mentioned in chapter 2, the traditional approach to distributed transaction management isn’t a good choice for modern applications. Instead of an ACID transactions, an operation that spans services must use what’s known as a saga, a message-driven sequence of local transactions, to maintain data consistency. One challenge with sagas is that they are ACD (Atomicity, Consistency, Durability). They lack the isolation feature of traditional ACID transactions. As a result, an application must use what are known as countermeasures, design techniques that prevent or reduce the impact of concurrency anomalies caused by the lack of isolation.

4.1.3 Using the Saga pattern to maintain data consistency

The Saga pattern is a good tool, but it cannot solve all the problems.

On the surface, sagas seem straightforward, but there are a few challenges to using them. One challenge is the lack of isolation between sagas. Section 4.3 describes how to handle this problem. Another challenge is rolling back changes when an error occurs. Let’s take a look at how to do that.

4.2.1 Choreography-based sagas

Starting with this is more simple than Orchestration-based saga, but after a while it will become chaotic.

BENEFITS AND DRAWBACKS OF CHOREOGRAPHY — BASED SAGAS

And there are some drawbacks:

* More difficult to understand — Unlike with orchestration, there isn’t a single place in the code that defines the saga. Instead, choreography distributes the implementation of the saga among the services. Consequently, it’s sometimes difficult for a developer to understand how a given saga works.

* Cyclic dependencies between the services — The saga participants subscribe to each other’s events, which often creates cyclic dependencies. For example, if you carefully examine figure 4.4, you’ll see that there are cyclic dependencies, such as Order Service Accounting Service Order Service . Although this isn’t necessarily a problem, cyclic dependencies are considered a design smell.

* Risk of tight coupling — Each saga participant needs to subscribe to all events that affect them. For example, Accounting Service must subscribe to all events that cause the consumer’s credit card to be charged or refunded. As a result, there’s a risk that it would need to be updated in lockstep with the order lifecycle implemented by Order Service

4.3 Handling the lack of isolation

It’s hard to isolate distributed transactions.

The challenge with using sagas is that they lack the isolation property of ACID transactions. That’s because the updates made by each of a saga’s local transactions are immediately visible to other sagas once that transaction commits. This behavior can cause two problems. First, other sagas can change the data accessed by the saga while it’s executing. And other sagas can read its data before the saga has completed its updates, and consequently can be exposed to inconsistent data.

This lack of isolation potentially causes what the database literature calls anomalies. An anomaly is when a transaction reads or writes data in a way that it wouldn’t if transactions were executed one at time. When an anomaly occurs, the outcome of executing sagas concurrently is different than if they were executed serially.

4.3.1 Overview of anomalies

The typical isolation problems:

The lack of isolation can cause the following three anomalies:

* Lost updates — One saga overwrites without reading changes made by another saga.

* Dirty reads — A transaction or a saga reads the updates made by a saga that has not yet completed those updates.

* Fuzzy/nonrepeatable reads — Two different steps of a saga read the same data and get different results because another saga has made updates.

All three anomalies can occur, but the first two are the most common and the most challenging.

5. Designing business logic in a microservice architecture

Implementing the business logic is not an easy ride.

Developing complex business logic is even more challenging in a microservice architecture where the business logic is spread over multiple services. You need to address two key challenges. First, a typical domain model is a tangled web of interconnected classes. Although this isn’t a problem in a monolithic application, in a microservice architecture, where classes are scattered around different services, you need to eliminate object references that would otherwise span service boundaries. The second challenge is designing business logic that works within the transaction management constraints of a microservice architecture. Your business logic can use ACID transactions within services, but as described in chapter 4, it must use the Saga pattern to maintain data consistency across services.

The solution is: Aggregate pattern from DDD

Fortunately, we can address these issues by using the Aggregate pattern from DDD. The Aggregate pattern structures a service’s business logic as a collection of aggregates. An aggregate is a cluster of objects that can be treated as a unit. There are two reasons why aggregates are useful when developing business logic in a microservice architecture:

* Aggregates avoid any possibility of object references spanning service boundaries, because an inter-aggregate reference is a primary key value rather than an object reference.

* Because a transaction can only create or update a single aggregate, aggregates fit the constraints of the microservices transaction model.

Final words

This article obtained the rational limit, but at least highlights several microservices problems. Instead continuing citations, reading the Microservices Patterns is better.

--

--