Avoiding Distributed Monoliths

sidsid
5 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 points 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.

Microservices should not be used as a way to enforce modularity and increase agility. If you properly architect your system using correct methodologies like SOLID you should already have a modular and agile system. Decomposing a complex system into smaller manageable parts is not a new idea.

Microservices are the wrong tool for this job. They can solve other problems such as independent deployability, scalability, and fault-tolerance which are indeed important problems to solve in their own right.

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.

A Note on Domain-Driven Design and Microservices

One misconception people make with Domain Driven Design (DDD) is that they always associate it with microservices. It's true that you should have well-defined domains for your microservices, but DDD does not require them. The DDD book by Eric Evans does not mention microservices. Domains are defined by the business. In software, "domains" translate to modules. The book makes recommendations on how modules should interact with one another. For example, you can use layers to isolate modules with different purposes. The goal of DDD is to manage complexity and enable agility. An organization should be able to organize and adapt with either a monolith or a microservice.

Microservices should be used to solve technical problems, not business ones. Sometimes these two align for things like scale. However, if you can solve your business problems with a low technical overhead, do so. If you can't, then take on more technical overhead. Have a customer-first, business-driven approach to software architecture.

Conclusion

The goals for any system include agility, independence, and simplicity. Agility since we want to move quickly; independence since we don't want teams to step on each other's toes; simplicity since we want the system to be comprehensible so that it's easy and predictable to change. How we get here is where the disagreement lies.

Microservice-first means you have to figure out a lot at the same time. It's an infrastructure/ observability/ maintainability challenge that gets worse if you don't have your boundaries well defined. Modular monoliths are good temporary in-between. They can introduce more independence, simplicity, agility while minimizing the technical challenges of achieving these goals.

In the end, it's not about monoliths vs microservices or macro vs micro. It's about services.

0
Subscribe to my newsletter

Read articles from sid directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

sid
sid