The microservices honeymoon period is over. Uber is refactoring thousands of microservices into a more manageable solution [1]; Kelsey Hightower is predicting monoliths are the future [2]; and even Sam Newman is declaring that microservices should never be the default choice, but rather a last resort [3].
Whatโs going on here? Why have so many projects become unmaintainable, despite microservicesโ promise of simplicity and flexibility? Or are monoliths better, after all?
In this post, I want to address these questions. You will learn about common design issues that turn microservices into distributed big balls of mudโโโand, of course, how you can avoid them.
But first, letโs set the record straight on what a monolith is.
Monolith
Microservices have always been positioned as a solution for monolithic codebases. But are monoliths necessarily a problem? According to Wikipediaโs definition [4], a monolithic application is self-contained and independent from other computing applications. Independence from other applications? Isnโt that what we are chasing, often to no avail, when we design microservices? David Heinemeier Hansson [5] called out the smearing of monoliths right away. He warned about the liabilities and challenges inherent to distributed systems and used Basecamp to prove that a large system serving millions of customers can be implemented and maintained in a monolithic codebase.
Hence, microservices do not โfixโ monoliths. The real problem that microservices are supposed to solve is an inability to deliver business goals. Often, teams fail to achieve business goals because of the exponentially growingโโโor even worse unpredictableโโโcosts of making a change. In other words, the system is unable to keep up with the needs of the business. The uncontrollable cost of change is not a property of a monolith but rather of a big ball of mud [6]:
A Big Ball of Mud is a haphazardly structured, sprawling, sloppy, duct-tape-and-baling-wire, spaghetti-code jungle. These systems show unmistakable signs of unregulated growth, and repeated, expedient repair. Information is shared promiscuously among distant elements of the system, often to the point where nearly all the important information becomes global or duplicated.
The complexity of changing and evolving a big ball of mud can be caused by multiple factors: coordinating the work of multiple teams, conflicting non-functional requirements, or an intricate business domain. Either way, we often seek to tackle this complexity by decomposing such clumsy solutions into microservices.
Micro-what?
The term โmicroserviceโ implies that some parts of a service can be measured and its value should be minimized. But what does microservice mean, exactly? Letโs examine a few common ways the term is used.
Micro-teams
The first one is the size of the team that works on a service. And this metric should be measured in pizzas. Seriously. They say that if a team working on a service can be fed with two pizzas, then itโs a microservice. I find this heuristic anecdotal, as Iโve built projects with teams that could be fed with one pizzaย โฆand I dare anyone to call those balls of mud microservices!
Micro-codebases
Another widespread approach is to design a microservice-based on the size of its codebase. Some take this notion to the extreme and try to limit service sizes to a certain number of lines of code. That said, the exact number of lines needed to constitute a microservice is yet to be found. Once this holy grail of software architecture is discovered, we will move on to the next questionโโโwhat is the recommended editor width for building microservices?
On a more serious note, a less extreme version of this approach is the prevalent one. The size of a codebase is often used as a heuristic for deciding whether it is a microservice or not.
In some way, this approach makes sense. The smaller the codebase, the lower the scope of the business domain. Thus, itโs simpler to understand, to implement, and to evolve. Moreover, a smaller codebase has fewer chances of turning into a big ball of mudโโโand if it does happen, it is simpler to refactor.
Unfortunately, the aforementioned simplicity is just an illusion. When we evaluate a serviceโs design based on the service itself, we miss a crucial part of system design. We forget the system itself, the system that the service is a component of.
โThere are many useful and revealing heuristics for defining the boundaries of a service. Size is one of the least useful.โ ~ Nick Tune
We Buildย Systems!
We build systems, not sets of services. We use microservices-based architecture to optimize a systemโs design, not the design of individual services. No matter what others may say, microservices cannot, and will never be neither completely decoupled, nor fully independent. You cannot build a system out of separate components! That would go against the very definition of the term โsystemโ [7]:
1. A set of connected things or devices that operate together
2. A set of computer equipment and programs used together for a particular purpose
Services will always have to interact with each other to form a system. If you design a system by optimizing its services, but ignore the interactions between them, this is what you may end up with this:

Those โmicroservicesโ may be simple individually, but the system itself is a complexity hell!
So how do we design microservices that tackle the complexity not only of the services but of the system as a whole?
Thatโs a tough question, but luckily for us, it was answered a long time ago.
A System-Wide Perspective on Complexity
Forty years ago, there was no cloud computing, no global-scale requirements, and no need to deploy a system every 11.7 seconds. But engineers still had to tame systemsโ complexity. Even though the tools in those days were different, the challengesโโโand more importantly, the solutionโโโare all relevant nowadays, and can be applied to the design of microservices-based systems.
In his book, โComposite/Structured Designโ [8], Glenford J. Myers discusses how to structure procedural code to reduce its complexity. On the very first page of the book, he writes:
There is much more to the subject of complexity than simply attempting to minimize the local complexity of each part of a program. A much more important type of complexity is global complexity: the complexity of the overall structure of a program or system (i.e., the degree of association or interdependence among the major pieces of a program).
In our context, local complexity is the complexity of each individual microservice, whereas global complexity is the complexity of the whole system. Local complexity depends on the implementation of a service; global complexity is defined by the interactions and dependencies between the services.
So which complexity is more importantโโโlocal or global? Letโs see what happens when only one of the complexities is taken care of.
Itโs surprisingly easy to reduce global complexity to a minimum. All we have to do is to eliminate any interactions between the systemโs componentsโโโi.e., implement all functionality in one monolithic service. As weโve seen earlier, this strategy may work in specific scenarios. In others, it may lead to the dreaded big ball of mudโโโprobably the highest possible level of local complexity.
On the other hand, we know what happens when you optimize only the local complexity, but neglect the systemโs global complexityโโโthe even more dreaded distributed big ball of mud.

Hence, if we concentrate on only one type of complexity, it doesnโt matter which one is chosen. In a fairly complex distributed system, the opposite complexity will skyrocket. Therefore, we canโt optimize only one. Instead, we have to balance both the local and global complexities.
Interestingly, the means for balancing complexity described in the โComposite/Structured Designโ book are not only relevant to distributed systems. They also offer insight into how to design microservices.
Microservices
Letโs start by defining what exactly are those services and microservices, that we are talking about.
What is aย Service?
According to OASIS Standard [9], a service is:
A mechanism to enable access to one or more capabilities, where the access is provided using a prescribed interface.
The prescribed interface part is crucial. A serviceโs interface defines the functionality it exposes to the world. According to Randy Shoup [10], a serviceโs public interface is simply any mechanism that gets data in or out of service. It can be synchronous, such as a plain request/response model, or asynchronous. one that produces and consumes events. Either way, synchronous or asynchronous, the public interface is just the means for getting data in or out of service. Randy also describes the serviceโs public interfaces as its front door.
A service is defined by its public interface, and this definition is enough to define what makes a service a microservice.
What is a Microservice?
If a service is defined by its public interface, then โ
A microservice is a service with a micro public interfaceโโโmicro front door.
This simple heuristic was followed in the days of procedural programming, and it is more than relevant in the realm of distributed systems. The smaller the service you expose, the simpler its implementation, and the lower its local complexity. From the global complexity standpoint, smaller public interfaces produce fewer dependencies and connections between services.
The notion of micro-interfaces also explains the widespread practice of microservices not exposing their databases. No microservice can access another microserviceโs database, but only through its public interface. Why is that?โโโWell, a database would be a huge public interface! Just consider how many different operations you can execute on a relational database.
Hence, to reiterate, in distributed systems, we balance local and global complexities by minimizing the servicesโ public interfaces and thus making them microservices.
WARNING
This heuristic may sound deceptively simple. If a microservice is just a service with a micro public interface, then we can just limit public interfaces to only one method. Since the front door canโt be any smaller than that, those should be the perfect microservices, right? Not really. To demonstrate why not, Iโll use the example from my other post [11] on this subject:
Letโs say we have the following backlog management service:

Once we decompose it into eight services, each having a single public method, we get services with perfect local complexities:

But can we connect them into the system that actually manages the backlog? Not really. To form the system, the services have to interact with each other and share changes to each serviceโs state. But they canโt. The servicesโ public interfaces do not support that.
Hence, we have to extend the front doors with public methods that enable integration between the services:

Boom. If we optimize the complexity of each service individually, the naรฏve decomposition works great. However, when we try to connect the services into a system, global complexity kicks in. Not only does the resulting system become an entangled mess; we also have to extend the public interfaces beyond our original intentโโโfor the integrationโs sake. Paraphrasing Randy Shoup, in addition to the tiny front door, we created a huge โstaff onlyโ entrance! Which brings us to an important point:
A service having more integration-related than business-related methods is a strong heuristic for a growing distributed big ball of mud!
Hence, the threshold upon which a serviceโs public interface can be minimized depends not only on the service itself but (mainly) on the system that the service is a part of. A proper decomposition to microservices should balance the systemโs global complexity and the local complexities of its services.
Designing Service Boundaries
โFinding service boundaries is really damn hardโฆ There is no flowchart!โโโโUdi Dahan
The above statement by Udi Dahan is especially true for microservices-based systems. Designing microservicesโ boundaries is hard, and probably impossible to get right the first time. That makes designing a fairly complex microservices-based system an iterative process.
Hence, it is safer to start with wider boundariesโโโprobably the boundaries of proper bounded contexts[12]โโโand decompose them into microservices later, as more knowledge is gained about the system and its business domain. This is especially relevant to services encompassing core business domains[13].
Microservices Outside of Distributed Systems
Even though microservices were โinventedโ only recently, you can find plenty of implementations of the same design principles in other industries. These implementations include:
Cross-Functional Teams
We all know that cross-functional teams are the most effective ones. Such team is a diverse group of professionals all working on the same task. An efficient cross-functional team maximizes the communication inside of the team and minimizes the communication outside of it.
Our industry discovered cross-functional teams only recently, but task forces have been around forever. The underlying principles for these are the same as those for a microservices-based system: high cohesion inside the team, and low coupling between teams. The teamโs โpublic interfaceโ is minimized by incorporating the skills needed to achieve the task into the team (i.e., implementation details).
Microprocessors
I stumbled upon this example in Vaughn Vernonโs wonderful blog on the same topic. In his post, Vaughn draws an interesting parallel between microservices and microprocessors. In particular, he discusses the difference between processors and microprocessors:
I find it interesting that there is a size classification given that helps determine whether or not a central processing unit (CPU) is considered a Microprocessor: the size of its data bus [21]
A microprocessorโs data bus is its public interfaceโโโit defines the amount of data that can be passed between the microprocessor and other components. There is a strict size classification for the public interface that defines whether or not a central processing unit (CPU) is considered a microprocessor.
Unix Philosophy
Unix philosophy, or the Unix way, is a set of cultural norms and philosophical approaches to minimalist, modular software development [22].
One may argue that the Unix philosophy contradicts my case that you cannot build a system out of fully independent components: Arenโt Unix programs fully independent, and yet do form a working system? The opposite is true. The Unix way almost literally defines that programs have to expose micro-interfaces. Letโs see how the principles of the Unix philosophy relate to the notion of microservices:
The first principle calls for programsโ public interfaces to expose one coherent function, instead of cluttering programs with functionality not related to their original goal:
Make each program do one thing well. To do a new job, build afresh rather than complicate old programs by adding new โfeaturesโ.
Even though Unix commands are considered fully independent from each other, they are not. They still have to communicate with others, and the second principle defines how communication interfaces should be designed:
Expect the output of every program to become the input to another, as yet unknown, program. Donโt clutter output with extraneous information. Avoid stringently columnar or binary input formats. Donโt insist on interactive input.
Not only the communication interface is strictly limited (standard input, standard output, and standard error), but according to this principle, the data passed between the commands should be strictly limited as well. I.e., Unix commands have to expose micro-interfaces and never depend on each otherโs implementation details.
What About Nanoservices?
The term โnanoserviceโ is often used to describe a service that is too small. One can say that those naรฏve one-method-services in the previous example are nanoservices. However, I donโt necessarily agree with this classification.
Nanoservices are used to describe individual services while ignoring the overarching system. In the above example, once we put the system into the equation, the servicesโ interfaces had to grow. In fact, if we compare the original single service implementation with the naรฏve decomposition, we can see that once we connect the services into a system, the system goes from 8 public methods overall to 38. Moreover, the average number of public methods per service jumps from the desired 1 all the way to 4.75.
Hence, if instead of codebases, we optimize services (public interfaces), the term nano-service doesnโt hold up any more, as the service is forced to grow back up to support its systemโs use-cases.
Is That All There Is Toย It?
No. Although minimizing the public interfaces of services is a good guiding principle for designing microservices, itโs still just a heuristic and doesnโt replace common sense. In fact, the micro-interface is just a kind of abstraction over the more fundamental, but much more complex design principles of coupling and cohesion.
For example, if two services have micro-public interfaces, but they have to be coordinated in a distributed transaction, they are still highly coupled to each other.
That said, aiming for micro-interfaces is still a strong heuristic that addresses different types of coupling, such as functional, development, and semantic. But thatโs a topic for another blog.
From Theory toย Practice
Unfortunately, we donโt have an objective way to quantify local and global complexities yet. On the other hand, we do have some design heuristics that can improve the design of distributed systems.
The main message of this post is that you should continuously evaluate the public interfaces of your services by asking:
- What is the ratio of business- and integration-oriented endpoints in a given service?
- Are there, business-wise, unrelated endpoints in a service? Can you split them across two or more services without introducing integration-oriented endpoints?
- Would merging two services eliminate endpoints that were added to integrate the original services?
Use these heuristics to guide the design of your servicesโ boundaries and interfaces.
Summary
I want to sum it all up with a quote by Eliyahu Goldratt. In his books, he often repeated these words:
โTell me how you measure me, and I will tell you how I will behaveโ ~ Eliyahu Goldratt
When designing microservices-based systems, itโs crucial to measure and optimize the right metric. Designing boundaries for microcodebases, and microteams is definitely easier. However, to build a system, we have to take it into account. Microservices are about designing systems, not individual services.
And that brings us back to the title of the postโโโโUntangling Microservices, or Balancing Complexity in Distributed Systemsโ. The only way to untangle microservices is to balance the local complexity of each service, and the global complexity of the whole system.
Bibliography
- Gergely Oroszโs tweet on Uber
- Monoliths are the future
- Microservices guru warns devs that trendy architecture shouldnโt be the default for every app, but โa last resortโ
- Monolithic Application(Wikipedia)
- The Majestic MonolithโโโDHH
- Big Ball of Mud(Wikipedia)
- Definition of a System
- Composite/Structures Designโโโbook by Glenford J. Myers
- Reference Model for Service Oriented Architecture
- Managing Data in Microservicesโโโtalk by Randy Shoup
- Tackling Complexity in Microservices
- Bounded Contexts are NOT Microservices
- Revisiting the Basics of Domain-Driven Design
- Implementing Domain-Driven Designโโโbook by Vaughn Vernon
- Modular Monolith: A PrimerโโโKamil Grzybek
- A Design Methodology for Reliable Software SystemsโโโBarbara Liskov
- Designing Autonomous Teams and Services
- Emergent Boundariesโโโa talk by Mathias Verraes
- Long Sad Story of Microservicesโโโtalk by Greg Young
- Principles of DesignโโโTim Berners-Lee
- Microservices and [Micro]servicesโโโVaughn Vernon
- Unix Philosophy
Originally published at vladikk.com on April 09, 2020.



