Chapter 16
Microservices

Divide and conquer.
Reading time : 5 minutes


In previous chapters, we referred to a "software brick" as a set of features around a business domain. It provides a service independently, hiding its internal complexity from the outside.

  • "Brick" is a generic term, and the technical solution can take different forms: a third-party library to include in your project, a remote module requiring network communication, or conversely, an internal module callable directly in our code.
  • Thanks to its API, a brick can receive input data and produce output data: HTTP request, direct code call, event via a message queue. All these exchanges are governed by contracts.
  • These bricks and their APIs are designed following the Domain Driven Design (DDD) principle: a coding approach that aims to mirror the real-world business domain.

In this chapter, we'll dive into microservices, a particular type of software brick that combines all these elements.

But first, let's talk about their nemesis: the monolith.

The Monolith

Let's go back to the 2000s, when:

  • Cloud computing was emerging. Most companies managed their own server infrastructure. Virtual machines optimized the use of physical machines by running multiple isolated operating systems, but their management remained heavy and cumbersome.
  • Networks were less efficient. Fiber optics were rare. Protocols, especially HTTP, were less optimized. Increasing network calls between software bricks hurt performance.
  • Practices and tools for testing and deployment across environments were not yet widespread. Having a single application simplified production releases and reduced failure risks.
  • Web browsers were less standardized, asynchronous requests were just starting to gain traction, and JavaScript was limited. We were on the brink of Web 2.0. The backend (PHP, Java, .NET) directly generated frontend code (HTML, CSS, JavaScript) interpreted by the browser. The frontend developer role was quite new, and most developers were versatile.

This is why IT departments, especially in web companies, centralized all code in a single project.

Backend, frontend, user account management, financial reports... everything was done in one place.
This is what we call a monolith, a developer's nightmare, the very definition of "legacy code":

  • Even the best-organized monoliths were corrupted by the dozens of developers who worked on them over time. Short deadlines or lack of skills often led developers to take shortcuts, ignoring testing and design best practices. These projects became massive spaghetti bowls, where everything is intertwined, making them very fragile with each change.
  • They are challenging to evolve technically. Whenever you want to change a tool or a language version, you need to update absolutely everything. It takes months of work. As a result, these monoliths often run on outdated technologies. This is a security nightmare, and good luck finding new developers to work on them.
  • Developers often work on the same files, leading to frequent conflicts.

On top of that, there are some technical constraints. In particular, scaling a monolith is complicated. A monolith usually runs on a single machine and is connected to a single database. If the number of customers doubles tomorrow, you either need to increase the machine's power or duplicate the entire system.

A monolith—everyone who launched their digital platform in the 2000s has one. Deezer, L'Équipe, Rue du Commerce—at every company I've worked for, the monolith played a central role and still managed entire business domains.
All of them had a plan to break this monolith into smaller bricks.

Microservices

The technological boom of the 2010s offered developers a new way to design their architectures. They applied Domain Driven Design principles to define clear boundaries for their business domains and export them into independent projects.
Example: a payment project, user account, anti-fraud...

Note: This breakdown sometimes reveals bricks not based on business but on a major technical feature. Example: authentication, security... For the rest of this chapter, I'll focus only on business-oriented breakdowns.

Instead of a monolith, we now have a set of autonomous bricks with:

  • Dedicated data sources: databases, cache...
  • Independent production deployment.
  • The ability to scale only this part if needed.
  • The freedom to use different technologies and languages.
  • And, most of the time, an isolated infrastructure.

Ideally, these bricks follow the single responsibility principle: they focus on a single part of the business and expose a well-defined service.
They are thus called "microservices," or "ms."

All these microservices communicate through their respective APIs, most often via HTTP or by broadcasting events.

From a maintainability standpoint, microservices seem perfect at first glance:

  • The code is no longer in the same project, making it harder for less conscientious developers to take shortcuts.
  • If a developer makes a poor design choice, it is limited to a microservice without impacting the rest.
  • Technical debt is easier and faster to address. You can evolve the entire system microservice by microservice.

But this system also has its downsides:

  • Communications go through the network: it's slow, there's a risk of failures, monitoring, and debugging are more challenging.
  • If developing a feature impacts several microservices, you need to develop the feature itself and manage the communications between services. It's time-consuming, and you must carefully handle the deployment order.

Since the 2020s, with hindsight from microservice architectures designed in the previous decade, we are starting to see their main problem: the breakdown is often poor.

The "micro" in "microservices" wrongly encouraged developers to produce bricks as small as possible. The more bricks there are, the more communications increase, which extends development time and increases network latency. But above all, over time, developing new features creates dependencies between microservices: they all end up calling each other.

Example: ms-user asks ms-payment, "Hey, give me the payments for user #42." Payment retrieves the information from various systems. To get Google payments, it needs the user's Google ID. So, it asks, "Hey ms-user, give me the details of client #42." These two microservices are interdependent!

We're back to our spaghetti bowl with everything tangled up. It's even worse than before because now it's spread across different bricks over the network, making it much more challenging to follow the thread and untangle everything. What a nightmare! This is called a distributed monolith.

This situation arises when developers try too hard to do the right thing. They over-split, thinking they'll amplify the positive effects of microservices. They forget that they are also amplifying the negative. If a new requirement appears tomorrow and changes the scope of a microservice, it can ruin their entire design.

Real-life example: I created a microservice dedicated to subscription management, thinking it was a sufficiently complex business domain to justify its independence. This ms makes calls to payment partners. The company now wants to move one-time purchases out of its legacy monolith: gift cards... The payment partners are the same. Should I integrate this into ms-subscription even if these aren't subscriptions? Should I create a new ms-payment to centralize my relationship with partners, which would then be used by ms-subscription?
The choice is not easy and will impact the company in the long term.

For the past few years, we've been hearing more and more about a middle ground between our good old monolith and microservices.

The Modular Monolith

Let's jump straight into an example:

My company is an online media platform offering paid articles.
Different offers allow customers to subscribe to various service levels: premium, super premium.
These service levels unlock different entitlements: reading articles, sharing my account, posting comments.

This example highlights the management of several business concepts: article, account, offer, service level, subscription, entitlements, comment.

In a microservices architecture, we would probably have created a dedicated microservice for each of these concepts: article, subscription, account, offer, entitlements, comment.
They are all interconnected:

  • An account accepts an offer.
  • This acceptance starts a subscription.
  • The subscription grants entitlements to the account.
  • An account is therefore linked to entitlements.
  • Entitlements that unlock an article.
  • An account reads an article.
  • An account posts a comment.
  • This comment is associated with both the account and the article.

The idea of the modular monolith is to reduce complexity by grouping closely dependent concepts within the same project.

We might imagine here that subscriptions are responsible for 90% of the entitlements' setup and removal. It might be a good idea to bring them together.
Let's consider the downside first: in our system, the only high-load features linking accounts and subscriptions are the purchase flow and the subscription management page in the customer area. However, account entitlements are frequently used: upon login, when displaying an article...
In a microservices setup, we would have given more power to ms-entitlement, while ms-subscription could run on less powerful machines. Combining the two would eliminate this distinction.

Decisions will need to be made. Sometimes, they are obvious. Often, you accept losses in one area to gain in another.
You can choose to merge only certain concepts, turning the monolith into a sort of macroservice.
Or, the choice could be to consolidate everything into a single project: no more microservices. This approach is increasingly common in the early phases of product launches, to make it available to users as quickly as possible.

But then, aren't we falling back into the pitfalls of the 2000s?

In "modular monolith," there's "module," and that's the key difference from the old monolith.

Sure, we group several concepts in the same place, but we introduce a clear boundary between them to keep them independent. The goal is to ensure that, if needed, we can easily extract a module and turn it into a microservice.

Essentially, where I previously had microservices ms-subscription and ms-entitlement, I now have a macroservice with "subscription" and "entitlement" modules.
Microservices communicated via their APIs over the network, losing up to 100 milliseconds per communication.
Modules now communicate through their APIs with direct code calls, governed by all the established contracts. We're talking microseconds now.

Ideally, modules also have independent data sources: databases, cache... If not, care must be taken to segment these sources and assign a responsible party to each section.
Example: the "user" module is responsible for the cache associated with the account. After a purchase journey, the "subscription" module should not directly clear this cache. Instead, it should communicate, "Hey, user 42 just subscribed" to the "account" module, which will handle the logic around this event, triggering the cache update.

In theory, this modular system reduces the risk of recreating spaghetti code. In practice, not all languages offer features to prevent a developer from bypassing a contract. We'll see the results in a few years.


Most developers avoid legacy monoliths like the plague. But for some, like me, they represent an opportunity. Extracting features from a monolith isn't just about copy-pasting; you need to rework the current need and propose functional and technical improvements. Most of the time, you have to rethink an architecture from scratch and create a migration plan so the new system can gradually take over without disrupting production.
It's fascinating!