Microservices’s Dark Side: The Monolith Strikes Back

Microservices’s Dark Side: The Monolith Strikes Back

Microservices architecture offers plenty of benefits, but when dealing with complexity they aren’t the best option. Here we explain Microservices’s Dark side and the implications of using it.

Young man with a hat writing and architecture over a white boardLet’s face the truth, right from the beginning: Microservices won’t save you from designing and writing bad, fragile, and unmaintainable software systems. It is actually the opposite. If your organization is doing a bad job writing good, robust, and scalable software, expect the worst with the added complexity of Microservices architectures.

Does it sound contradictory to you? Do you believe that building very tiny, fine-grained services, strictly following the Single Responsibility principle would allow you to escape from the scary Big Ball of Mud that most gigantic enterprise systems become over the years? If you were told that a cleaner design, the separation of concerns, and modularity are some of the benefits that come with Microservices, you might find it worthwhile to continue reading this article.

Let me rephrase my last sentence: If these benefits in modularity and maintainability are what tilts the balance to go with Microservices, I beg you to continue reading and if it’s not too late, to reconsider your alternatives.

Benefits and Added Complexity

Nowadays, most companies foresee real value implementing Microservices architectures (some companies move even further, going Serverless). However, it is not quite clear that most of these organizations understand the implications of using Microservices.

When applied correctly and for some given contexts, Microservices offer terrific benefits:

  • Scalability can be fine-tuned. It can be restricted to the more stressed Microservices, contrary to the Monolith architecture where due to one single function being heavily accessed by other services or users would require duplicating the whole application server.
  • Possibility of using multiple technologies. Although you can benefit from having a homogeneous technological stack, Microservices’ intrinsic distributed nature allows you to select the platform that works best for the problem being tackled. For example, you can choose Python for a Machine Learning service, .NET for a core backend service tied to existing batch processes, and then go with a new node.js backend for those single-page applications written in AngularJs.
  • Continuous integration + continuous deployment. Automation and Development toolchains get better as more companies continue adopting Microservices. Most of the added complexity with Microservices architecture can be tackled by choosing the right tools and processes in order to automate most tasks of your now more complex Application Lifecycle Management (ALM).

If the above requirements are part of what you really need for your systems, the gained flexibility with Microservices will make a big difference. You get a really good scalable platform where you can add and maintain multiple services, and your software and your development teams will scale well.

However, these benefits don’t come for free. In terms of software design and maintenance, Microservices add a whole new level of complexity over processes, tools, and tasks (think about the entire ALM).

Despite implementing the systems with cutting-edge technology in terms of hardware and software, and even by creating a new entire department for DevOps, the complexity is increased exponentially compared with a traditional Monolithic development architecture.

Related content: Microservices: the biggest challenge might be your own company

Most of the drawbacks that come with Microservices are well-known in the implementation of any distributed system:

  • You might be transforming a very simple and lineal implementation of some business logic into a complex distributed task that requires coordination among several services.
  • It’s easy to stumble upon performance bottlenecks and unreproducible bugs (in most cases, product of the added execution parallelism).
  • If you also have to deal with a legacy relational database that needs to be migrated, split, proxied, or cached in order to adapt to the new architecture, you should get an idea of how complex it could be.

I won’t enter into the details about how hard distributed systems are.  Instead, I want to dig a bit further into the problems with the overall modular design of such a complex system and the premise that Microservices are here to help make it simpler and easier to maintain pieces of software.

Microservices aren’t meant to reduce any complexity (nor the skills required) in the design of your software. The same good design principles (that may fall short with your existing Monolith systems) apply to a Microservices architecture, and if you are not aware of that, the Monolith you were trying to break apart will come back to haunt you.

Dealing with Complexity

Divide and conquer is a directive that applies to the vast majority of complex problems in many aspects of life since the very beginning of humanity. Applied to software, depending on the programming paradigm, you get different types of abstractions to split the system into these compounding chunks of algorithms: in a functional paradigm, the function definition; in an object-oriented paradigm, a prototype or a class.

You can always find a way to split functionality/requirements in order to deal with your problem but on a lower scale. The problem with this approach is that as long as your software grows, the number of these small abstractions grows with it. You end up with hundreds and thousands of functions or classes.

Programming languages attempt to cope with this problem by adding new structures that allow them to encapsulate these little abstractions into coarse-grained modules (call it packages, components, or artifacts). However, these new constructs have a very lightweight contract, and dependencies and relationships continue to be at a class/function level.

Classes as the building blocks to cope with complexity
Classes as the building blocks to cope with complexity. Regarding grouping them into packages/components the interface of each element is at “class level”.

This lack of a well-defined interface among modules makes it hard to keep a clean separation of responsibilities. Moreover, there are no perfect abstractions and the inner works of its implementation leaks to some degree in most situations. Developers trying to reuse a small part of code from a given library need to add an obscure dependency to another piece of code from another library, which leads to some external code being executed, thereby producing unforeseen results.

As time goes by, some boundaries that weren’t supposed to be crossed are finally surpassed, for example, by adding a class or library to a project as a new dependency that shouldn’t be there according to the original design.

Over time, the entire, clean, original design succumbs to a cryptic Monolith.

A large piece of software and its complex class diagram
A large piece of software and its complex class diagram. How well are you dealing with your technical debt?

Now, how does this relate to Microservices? Well, the reasoning is as follows: Given a program small enough, you have the tools and the programming language constructs to cope with complexity by encapsulating behavior and data into either functions, classes, or procedures.

As the program grows, maintaining the original, clean design becomes harder and instead of performing the necessary refactorings to keep complexity low and get rid of undesired secondary effects, developers begin to cut corners. Note that this is not a problem of laziness. Take into account the restrictions imposed by the environment and that you work for a project where there is a finite budget and time.

During this pursuit of continuous delivery, the development team (including seasoned Software Architects) trick themselves into thinking that if they could depend on a well-defined interface to communicate among components at a higher degree than the function/class level, then they can restrict the scope of problems to this new abstraction. In other words, we think that raising the level of abstraction would make it easier to cope with the increasing complexity, without having to be very careful about the mess we already have in the first place (kind of hiding “technical debt” under the carpet).

Thinking about this higher order of abstraction, our frustrated developers may see their heroic saviour in Microservices (note that I’m referring to Microservices here from the point of view of software design). With Microservices, developers stop thinking about interactions among lower level structures like classes and functions and begin building the software, composing well-defined and (hopefully) truly isolated services.

This works ok until the software continues growing, and small simple services begin to interact with some “federated” services. With such interactions, your services start to leak some internal works, and dependencies begin to pile up, again. Given enough time, your development team (that was previously unable to work with a non-distributed system and had to deal with an unmaintainable Monolith) is now working in a Microservices environment where they will be facing exactly the same problem as with the former Monolith approach but at a higher scale.

A simple Microservices component diagram
A simple Microservices component diagram. Imagine how it would look like with 50, or 100 Microservices?

Microservices architecture is great when applied to solving scalability and infrastructure maintenance issues. However, taking full advantage of using Microservices rely upon how well you can already manage the complexity. Be aware you will be dealing with a lot more complex and highly distributed systems, and Microservices designs and good practices won’t solve any of your existing problems. Attack your technical debt first and make sure you can cope with the technical skills necessary to clean your Monolith up. Later, take a look at what your next step would be.

To Sum Up

Microservices aren’t meant to cope with complexity. They should not be adopted to simplify/split a complex domain. In order to deal with complex systems, you need to apply good software design principles, not an architectural style.

If you are planning to invest in a Microservices architecture, be aware that the same problems you have keeping a software system tidy and under control will apply to Microservices but at a higher degree of complexity. In other words, besides dealing with the same problems to get a clean scalable design, now, you’re facing new problems as a result of running a very complex networked system.

Don’t forget that Microservices architectures imply building and growing a completely distributed computer system, so you need to work with the highest skilled professional developers in order to succeed.

White Paper

Comments?  Contact us for more information. We’ll quickly get back to you with the information you need.