Avoiding Distributed Monoliths

·

4 min read

Img Source: The First Rule of Building Microservices: Don’t Build Microservices

The recommended approach I've gathered from watching several talks and reading articles on this topic is monolith -> modular monolith -> microservices.

This way you know where the boundaries are.

The term "distributed monolith" implies that there is coupling between services, possibly because the boundaries are not well defined. Hence, this is far from the ideal of microservices which are meant to be independently deployable. IMO, microservices are essentially 3rd-party services maintained internally. Take for example Stripe or Auth0. When you deploy your application that uses Stripe and/ or Auth0 you don't have to contact these companies. Similarly when they upgrade their services, you are not affected as long as you use a specific version and they do not change that version's contract.

This is what I see as the goal of microservices. We don't care that in reality Stripe uses a monolith to run their service. All that matters is the version of Stripe you use and its contract. The same is true with Auth0. We don't know, or care, about Auth0's software architecture. All that matters is the version of Auth0 you use and its contract.

The reason why you don't communicate much with Auth0 or Stripe is because there are clear established boundaries between their services and yours.

Premature abstractions

Donald Knuth warned us about premature optimizations when he said that they are "the root of all evil (or at least most of it) in programming.” This is because according to him "[p]rogrammers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered" (Structured Programming with go to Statements).

We can think of abstractions in a similar way. When we create abstractions, we are optimizing to create a generalized solution. As such, premature abstractions can make debugging and maintenance difficult. Our solutions can become complex and inflexible because we do not fully understand how different parts of a system should interact or be reused. In the end we commit resources to design a system which (1) may not align with the business' actual needs and (2) build resistance to evolving in that direction.

Such advice is resonated by the Rule of Three and by Sandi Metz (The Wrong Abstraction) which further illustrates the dangers of premature abstractions. Microservices can be seen as an attempt to create such abstractions for reusability and scalability. As such they should be approached with caution.

Robert Martin poineds out that architects design buildings differently to serve different purposes. The blueprint or framework of a symphony hall is different from that of a library which is different from that of a university. Even though all three are made of bricks or concrete, their architecture is different. Software architecture should be approached in a similar fashion. An architecture that works for one business' needs may not necessarily work for another business' needs. As such architects and developers should practice caution when introducing abstractions.

This is why modular monoliths are recommended as a stepping stone towards microservices. They give you a way to define boundaries in a low-risk environment. Code is separated within modules and there is no overhead with a network layer. If you make the wrong abstractions or incorrectly define your boundaries, you can easily correct yourself since it's all in the same codebase.

If you think about it, the debate is not really between monolith and microservices; it's between less-distributed and more-distributed architectures. The former are easier and the latter are harder.

The Approach

I like Sam Newman's approach to making microservices:

  1. Start with a monolith since it's a small distributed system.

  2. Break it down into modules.

  3. Extract a module and turn into a microservice.

Microservices should not be an all or nothing approach. There shouldn't be a large undertaking to make the transition. It should happen naturally. Start by turning one module into a microservice and learn from that process. There is nothing wrong in having one microservice while everything else is still in a monolith.

Additionally, an existing microservice can be broken down into more microservices. This is an ongoing journey, not a destination.

I like this approach since it emphasizes the important step that many overlook before making microservices: modularity. You have to first have a well defined modular unit before you can have a microservice.