Capa do artigo: Microservices: the fragmentation of distribution

Microservices: the fragmentation of distribution

Over the past few years, I have participated in several legacy system modernization and digital transformation projects where microservices architecture was adopted as a solution for scalability and software evolution problems. Most of these initiatives were born from the desire to modernize legacy applications, leading organizations to embrace cloud-based microservices as a symbol of technological innovation. In each of these projects, when I perform a careful analysis of service distribution, I identify a concerning pattern that repeats itself consistently: excessive segmentation resulting in hollow services, devoid of any business rules or value. Sometimes these are components that exist solely to perform simple database queries, execute trivial validations, or relay calls to other services. What should be a cohesive distributed architecture becomes a complex web of dependencies, where each node adds latency, failure points, and operational costs without delivering the promised value of microservices architecture.

This phenomenon is not accidental. It happens due to a superficial understanding of the principles that underpin distributed architecture, combined with the mechanical application of patterns without considering the context and inherent trade-offs. This makes it clear that the observed fragility does not stem from the chosen architecture, but from a lack of technical repertoire and business understanding, often aggravated by organizational structures that do not favor coherent architectural decisions. Microservices are not a natural evolution of software design, but an architectural choice that must be justified by real, well-understood, and well-founded needs.

Another thing that was not uncommon to hear about some systems was the same question, repeated in different voices: "wow, this is not a microservice, this is a monolith." This murmur sounds somewhat strange, probably coming from someone who has never worked on true monolith projects.

With that in mind, let me pose some provocations: what "size" defines a monolith? How many modules define a monolith? How many endpoints make a monolith? Let us discuss this a bit.

I can speak about some projects I worked on that were, in fact, monoliths. The first one was an ERP for receiving, evaluating, tracking, and contracting R&D projects. In short, all modules generated a single .war file, which was used to deploy on a Tomcat server.

A curiosity from that era is that there was a role dedicated exclusively to deliveries, called Configuration Manager (CM). This professional was responsible for all version control of artifacts and documents, in addition to managing changes and audits. There was no automated pipeline for this process. The CM followed certain validation rituals, audited what was being delivered, and generated a series of documents before deploying the application.

And, as every good monolith goes, the project had several modules: payment, budget, registration, accounting, documents, approval workflow, configuration, profiling, among others. These modules were not separate projects, nor did they have distinct repositories. Each module was large and complex, with many business rules and details about profiling, authorization, and so on. All of this was organized within the same project, in the same repository, which ultimately generated a single large war file.

Another project I worked on, also a monolith, was an e-commerce platform. All modules generated a single .war file. There were no multiple projects or separate libraries. At that time, working with multi-modules using Maven or Gradle was not yet common practice. The application was divided into logical modules for search, products, user management, orders, inventory, payment, plus a module responsible for managing and configuring the customer journey, among others.

In this context, good coding practices were extremely important, something that, incidentally, should not change much today. Good code will always be good code and reflects the developer's repertoire for identifying and solving problems. Implementing a design that allowed proper modularization, with well-separated and organized contexts, clearly defined contracts between module calls, along with the application of principles like KISS, YAGNI, SOLID, and GRASP, as well as analysis of coupling, references, and cyclomatic complexity, were already valuable practices for large and complex projects like this one.

In the e-commerce case, we were already applying the Strangler Pattern to gradually break the monolith into microservices. Decisions about how to transform the system and which modules would be extracted into microservices were based on metrics. The progression of these moves was coordinated with product evolution, always aligned with the business. Nothing was done based on assumptions or arbitrary evolutions.

In summary, a monolith is the project. Therefore, my first point is that just because a service has many features or dense and complex business rules does not necessarily make it a monolith. If there is a well-defined context and a clear business responsibility with value, that does not make it a monolith. Some microservices can be large and dense in business rules, just as others can be small. It all depends on the problem they set out to solve.

"The architecture of a software system has little to do with the way it is deployed. Architecture is about boundaries and dependencies. You can have a good architecture in a monolith or a terrible architecture distributed across microservices."
β€” Robert C. Martin

Martin Fowler and other icons in the field have already recommended something known as "monolith first." I could put together a huge list of justifications, but I will simplify some points more directly. There are no network issues between module calls, so there is no need for resilience handling. Latency is practically zero, basically limited to processor context switching. There are no costs associated with multiple infrastructures or pipelines. Monitoring and troubleshooting are simpler and more straightforward. There is also no need for delivery coordination across environments, among other benefits.

When does the monolith become a problem?

For a long time, vertical scalability had a ceiling. The ability to add more resources to a machine was physically limited. Then came the idea of virtualizing hardware to expand the processing power, memory, and disk of servers, allowing support for the growth of software that was becoming increasingly larger and more complex. With load distribution mechanisms and stateless systems, monoliths can comfortably scale horizontally as well. So where does the monolith problem really come in, or rather, what do microservices actually solve?

As the system grows, more people are needed to compose the teams responsible for maintenance and evolution. Any type of change begins to require a maintenance window. Large and heavy applications tend to have slow startups. Remember the CM (Configuration Manager)? He also ended up becoming a bottleneck.

With many teams delivering simultaneously, it was necessary to version, audit, and document everything that went into branches. The CM performed the merges, generated the artifact, and only then made the scheduled delivery to the server.

Thus, problems began to manifest at some critical points. First, there were many different developers changing the same codebase, which significantly increased the chances of problems, such as conflicts during merges, especially because these merges were extremely painful. There was a great effort, both from the Configuration Manager and the developers themselves, to ensure that the integration of different branches did not remove features already delivered by other work fronts, and tools like CVS and SVN were, at the time, the best available for repository management.

The second problem was that any and all deliveries required the complete shutdown of the application for deployment. This meant that if a critical bug was identified or reported at noon, requiring an urgent bug fix, the entire application had to be stopped for the fix to be applied.

At the time, some experiments were already emerging to ensure rollout deployment with zero downtime, usually through environment duplication and call redirection. However, for that context, this type of approach was very expensive, as it required maintaining another equivalent machine to bring up the application and reconfigure the gateway or load balancer.

It is in this context that the microservices model emerges with the proposal to solve these two problems. From there, other potentials of the model begin to emerge, such as the ability to scale teams independently and perform independent deployments.

With good design, an eventual problem does not bring down the entire application, and scaling can be done according to need. It is not necessary to maintain a large number of instances for services that receive few calls; you can scale only the most critical ones that handle high volume.

This model, however, does not sustain itself with technical changes alone. It also requires new forms of management and team organization. Structures more aligned with business domains, small and autonomous teams, responsible end-to-end for the service lifecycle (you build it, you run it), become reference models for mature teams capable of delivering with consistency, quality, and speed. Concepts like Domain-Driven Design, bounded contexts, and team ownership cease to be merely best practices and become organizational pillars.

To support this model, new management techniques and processes also emerge, such as value-oriented product teams, outcome-focused metrics instead of output-focused ones, along with practices like SLOs, SLIs, and error budgets, which help balance delivery speed and system reliability.

"Great product teams focus on outcomes, not just outputs. It's not about shipping features, but about generating value and impact for customers." β€” Marty Cagan, Inspired: How to Create Products Customers Love (2018)

As a natural consequence, tools, models, processes, and practices emerge to support, accelerate, and secure the evolution of distributed systems. Among them are containers, automated integration pipelines, twelve-factor apps, DevOps practices, continuous delivery, and continuous deployment, among others.

Conway's Law and organizational structure

An important effect that has governed since 1967 is Conway's Law, which basically states that an organization's communication structure model influences software architecture. Various team models then emerge, along with attempts to architecturally engineer the inverse of Conway's Law, but without success. Over time, the law takes effect.

"Any organization that designs a system will produce a design whose structure is a copy of the organization's communication structure." β€” Melvin Conway, 1967

This is an important point that often goes unnoticed, and the technical team ends up being penalized, because the primary benefit of microservices is not technical, it is organizational. The real value lies in allowing multiple teams to work independently, with autonomy to develop, test, and deploy their parts of the system without excessive coordination with other teams. If the organization is not structured for this, microservices are probably not the right answer.

Amazon's experience with this model has become legendary in the industry. In the early 2000s, Jeff Bezos issued a mandate that became known as the "API Mandate." All teams were required to expose their functionalities through service interfaces. All communication between teams had to happen through these interfaces. There would be no exceptions. This decision forced Amazon to think in terms of service contracts, API versioning, and team autonomy. The result was an organization capable of scaling to thousands of developers working in parallel without stepping on each other's toes.

Another important lesson from this story is that Amazon did not start by distributing everything. The company gradually evolved from a monolith to services, identifying natural boundaries along the way. The services created represented significant business capabilities, such as product catalog, order processing, and inventory management. Each with substantial business logic, each managed by a team that deeply understood that domain.

Imagine the situation where your team depends on another team to create a new API, but that team does not have the availability to do it. Furthermore, the environment you work in is so rigid that, if you tried to bring the code over to implement it yourself, you would need to request access, time to understand how everything works, and approval from the other team, and you do not have all that time.

What is the consequence of this? Generally, teams end up implementing solutions on their own, due to the difficulty of depending on another team. As a result, the project's architecture starts generating systems that do the same thing, doubling the operational cost: there is time spent by the team developing, validating, testing, and delivering to production. Additionally, the team now has one more project to maintain over time, increasing maintenance and infrastructure costs.

When business refinement is aligned with architecture, the team can identify needs at the boundaries with other teams. This way, a demand can be requested in advance and planned to be absorbed by other teams within delivery cycles. This level of maturity simultaneously involves technology, architecture, and product, allowing teams to proactively coordinate dependencies, reduce rework, and deliver value consistently.

"Organizing teams around the flow of change and allowing them to own their own services reduces cognitive load and improves the speed and quality of deliveries." β€” Matthew Skelton & Manuel Pais, Team Topologies (2019)

As Skelton & Pais reinforce in Team Topologies, this principle explains how maturity in technology, architecture, and product allows teams to anticipate needs and coordinate dependencies efficiently.

When we fragment the system without understanding the domain model, we create services that do not express coherent ideas, only disconnected operations. The organizational structure may even appear agile, but the resulting system is rigid, difficult to evolve, and costly to operate.

"The model is not the diagram. The model is not the code. The model is the idea that the code and the diagram express." β€” Eric Evans

The real problem: poor design, not size

The confusion starts here. These monoliths were not problematic because they were large or because they ran in a single process. There is a mantra I always reinforce in this type of discussion: legacy code does not mean bad code; bad code is bad code, whether it is part of a monolith or distributed across microservices.

The real problem arises from the absence of consistent application of design fundamentals and best practices, whether due to lack of technical mastery, inability to negotiate priorities, poorly managed organizational pressures, or technical debts that should have been temporary. Often, these debts become permanent because professionals cannot find space to address them. Over time, the problem worsens and knowledge is lost, either because the code is no longer actively developed or because team changes mean that nobody fully knows the solution. This is the root cause, which is reflected in both the code and the architecture.

"A well-designed system is not necessarily a distributed system. A well-designed monolith is infinitely preferable to a set of poorly designed microservices." β€” Sam Newman

System distribution does not solve design problems. If the monolith suffers from excessive coupling, poorly defined responsibilities, and blurred boundaries between components, splitting it into microservices will only distribute those problems across the network. The additional complexity of distributed computing amplifies existing problems instead of solving them.

I have seen cases where an intermediary service is introduced solely to access a distributed cache, like Redis. In practice, this makes no sense: the cache is a resource that should be accessed directly, close to the application. By adding an extra layer, you end up creating unnecessary overhead, increasing latency, complexity, and failure points, instead of improving any performance.

Furthermore, new operational challenges arise, such as monitoring, logging, testing, and maintaining the intermediary service. The team's effort is spent managing this extra layer, while business value is not increased.

This decision reveals a lack of architectural repertoire. It is not about technological limitation, but about technical competence to correctly identify the problem before applying a solution. Repertoire is not just for solving problems, but for recognizing them and choosing the most direct and efficient approach.

Additionally, adding unnecessary layers generates extra maintenance costs, increases the risk of failures, and degrades performance, making system evolution slower and more error-prone.

The illusion of partial resilience

One of the most common arguments in favor of microservices is resilience. If a microservice fails, only the affected functionality becomes unavailable, not the entire system. This argument ignores the fact that, in distributed systems, partial failures are often more problematic than total failures. A monolith can fail completely, but at least the system is in a known state: unavailable. In a poorly designed microservices system, partial failures propagate in unpredictable ways.

Let me illustrate a case I experienced at a fintech that was born in a modern environment, with microservices architecture from its inception. Dozens of specialized services were created. Each feature of the mobile application was mapped to distinct microservices with teams for cards, investments, transfers, payments, and transaction history. The architecture's promise should have been clear: if one service fails, the others will continue working, guaranteeing partial availability for customers.

Then reality hits. When the balance inquiry microservice experienced instability, the entire application became unusable. Customers could not view cards, the investment area would not load, transfers were blocked. Even apparently independent features, like viewing transaction history, failed. The reason was in the architectural design: every screen of the application, every feature, made a call to the balance service to display or validate using the customer's current balance. This microservice, considered basic and simple, was actually a single point of failure disguised as a distributed service.

From a technical standpoint of system availability and resilience, the result was different from what was expected. The promise that if one service goes down, other parts of the system would keep working, is not always true. The balance service became a microservice on which all others depended directly or indirectly. When it went down, the domino effect was immediate, just like a monolith that crashes and takes the entire operation with it. The difference is that the distributed architecture added all the complexity of network, latency, serialization, additional failure points, and operational costs, without delivering the promised benefit of partial resilience. The interdependence that brings down the entire system is not exclusive to monoliths. It can exist in microservices too, but it is more complex to handle and identify the root cause because it is distributed, with problems cascading through the network in less obvious ways.

This pattern repeats itself in countless organizations. Netflix extensively documented its battles against critical dependencies in its microservices architecture. Even as a pioneer in resilience practices, the company acknowledges that certain services are so central that their failure compromises the entire platform. The difference lies in preparation. Netflix invests heavily in circuit breakers, aggressive timeouts, intelligent fallbacks, and chaos tests that deliberately take down services in production to validate system resilience.

When someone thinks about creating a new microservice or rewriting one because the old one is not easy to maintain, I always say: if you do not have new repertoire, you will repeat the same mistakes, with the risk of producing the opposite of the desired effect, meaning the new service is already born "legacy" (I have already mentioned that legacy does not mean bad or hard-to-maintain code), because the project ended up worse than what was considered problematic. I have seen this happen too. It is worth remembering that a microservice that calls others has complexities and non-functional requirements that need to be guaranteed beyond the rewrite of functional requirements, such as resilience defense mechanisms, system and business log instrumentation, observability instrumentation, security mechanisms, among others.

Cloudflare learned this lesson the hard way in July 2019. It was not a single large problem that brought down their global network. It was a combination of small failures that added up catastrophically and the entire house of cards collapsed. A BGP routing configuration change, seemingly harmless, interacted badly with a bug in the load balancing software. This problem, by itself, would have caused slight degradation. But it coincided with high load on their PostgreSQL database, which was close to its capacity limits. Slow database queries caused timeouts in services that depended on it. Those timeouts triggered automatic retries, which further increased the load on the already overloaded database. The caching system, which would normally absorb this load, was being aggressively invalidated due to recent deployments. Within minutes, the cascade was complete. It was not a single point of failure that brought down Cloudflare. It was the resonance between multiple system fragilities, each small, but combined in a lethal way.

"Your system will fail. The question is not if, but when, and how gracefully it will degrade." β€” Michael Nygard

Resilience does not come from avoiding individual failures. It comes from designing systems that degrade gracefully even when multiple things go wrong simultaneously.

The reality of the network: the invisible enemy

When we move from a monolith to microservices, we introduce the network as an intermediary for all communications between components. The network, however, is not reliable. This truth was formalized by Peter Deutsch and other Sun Microsystems engineers in the Fallacies of Distributed Computing. The first fallacy is the belief that the network is reliable. The second is the assumption that latency is zero. These fallacies are dangerous illusions that inexperienced architects and developers often assume or do not even consider when designing distributed systems.

Each call between microservices traverses the complete network stack: data serialization, TCP/IP transmission, routing through load balancers, deserialization, processing, response serialization, and the way back. What was an in-memory method invocation, executed in nanoseconds, becomes a network operation consuming milliseconds. When a service depends on multiple other services, these latencies accumulate. A request requiring sequential calls to five services, each with 50ms of network latency, introduces 250ms in communication overhead alone, before even considering actual processing.

The financial cost of networking in cloud computing environments is frequently underestimated. Providers charge for data transfer between availability zones and between regions. A system with hundreds of microservices, making millions of requests per day between them, can accumulate network costs that rival compute costs. In a monolith, these communications are method invocations with no additional cost.

"When you turn a method invocation into a remote call, you are not just adding latency. You are adding a failure point, a versioning requirement, a serialization problem, and an operational cost item." β€” Gregor Hohpe

Beyond latency and cost, the network introduces failure modes that do not exist in local calls: packets can be lost, connections interrupted, or servers become unreachable. Each of these situations requires explicit handling. A robust microservices system needs to implement retries with exponential backoff, circuit breakers to avoid overloading unstable services, aggressive timeouts to prevent thread blocking, and fallbacks to gracefully degrade functionality when dependencies fail. The absence of these mechanisms, which I commonly see in many projects, turns a distributed architecture into a house of cards, ready to crumble at the slightest breeze.

Any system that communicates with another service over the network, using HTTP, TCP, or UDP, in addition to increasing latency through round-trip time (RTT), requires cache mechanisms and connection pools, as well as error handling and propagation, whether business or infrastructure errors. Furthermore, it is necessary to implement defense mechanisms for resilience, such as retries, circuit breakers, and rate limits, especially in synchronous calls.

Part of these defense mechanisms can be delegated to infrastructure. Using a service mesh with sidecars allows configuring, without code implementation, aspects such as retries, timeouts, circuit breakers, rate limiting, and observability. This reduces development effort and standardizes resilience behaviors across services, but does not eliminate the need for conscious architectural decisions.

In messaging-based communication, there are also defense mechanisms that need to be considered to ensure fault tolerance. In this case, the concerns are different, such as consumer idempotency, safe reprocessing, dead-letter queues, deduplication control, ordering, and acknowledgment or offset management. These aspects are not solved by service mesh and require explicit design, whether in code or in broker configuration.

This reinforces that these treatments are not optional. Ignoring them results in fragile systems, and when this fragility propagates in a chain, the scenario resembles a game of Jenga with systems: each interaction adds the risk of a localized failure escalating and bringing down larger parts of the system or even the entire application. Some failures are immediate and simple to fix; others only manifest in production, during incidents with customers.

Beyond the technical and operational costs, these failures can generate data inconsistencies, affect business reliability, and cause direct financial losses, whether through user churn, reduced product usage, or negative impact on the company's image. In a scenario amplified by social media, a poor perception of the product or service can spread rapidly, making growth and market trust recovery more difficult.

"The first law of distributed architecture is: don't distribute. The second law is: if you can't avoid distribution, minimize it as much as possible." β€” Martin Fowler

The point is not to argue that everything should be a monolith. Monoliths are not inherently good or bad. The problem arises when we adopt microservices and create services to solve technical problems instead of business problems, generating excessive fragmentation with no real value. A microservice that encapsulates a complex business capability, such as complete order lifecycle management, justifies its independent existence. A microservice that only performs isolated validation or database queries just to separate it into an "isolated microservice" is purposeless fragmentation, adding all the complexity of the network with no corresponding value.

Extreme fragmentation: when micro becomes nano

The temptation to create increasingly smaller services has led many teams to a problematic extreme. Microservices that are so tiny, so granular, that they resemble serverless functions, but without the benefits of the serverless model. The term microservices has generated misunderstanding since its origin. The word micro suggests small size, leading teams to seek increasingly smaller services, but the concept was never about lines of code or deploy artifact size. Microservices should be sized by the business responsibility they encapsulate, not by technical size metrics. A microservice rich in business logic, which fully implements a bounded context, can have tens of thousands of lines of code and still be appropriately sized. What we call nanoservices, services so small they do almost nothing, are an anti-pattern.

"The goal of microservices is not to have many small services. The goal is to have services that are independently deployable and represent coherent business capabilities." β€” Sam Newman

This fragmentation results in services that violate cohesion principles. The granularity is so fine that the system loses any sense of logical organization. These microservices run in dedicated containers or virtual machines, consuming infrastructure resources for trivial operations that could be executed in memory. Unlike serverless functions, which scale to zero when not in use and are charged only per execution, these microservices maintain continuously running infrastructure.

The result is an architecture where simple business operations require dozens of network calls between services. Each call adds latency, each service introduces operational complexity, and the application becomes slower and more expensive without proportional gains in flexibility or scalability.

In this scenario, the indiscriminate pursuit of microservices leads to the creation of nanoservices and architectures with excessively "chatty" services, where simple operations require multiple remote calls. This fragmentation surpasses any reasonable benefit and is clearly characterized as an anti-pattern.

Anemic microservices: when security and business evaporate

The concept of an anemic domain model describes objects that contain only data, without behavior and with weak encapsulation. An anemic object is essentially a bag of getters and setters, with all business logic residing in separate service classes. An anemic microservice is one that exposes simplistic operations on entities without encapsulating significant business rules. Consider a product service that offers endpoints to create, read, update, and delete products, but has no logic about pricing, categorization, supplier relationships, or inventory management. Those rules exist in other services, or worse, in BFFs or in the client application consuming the service. The result is an architecture where business knowledge is scattered across multiple services and clients, with no clear place where the domain model resides.

The logic of encapsulation in domain models does not change when we migrate from monoliths to microservices. The same principles that guide us not to directly expose database tables in a monolithic application should guide API design in microservices. Exposing queries without proper security criteria, business validation, and access control is a serious architectural failure, regardless of the deployment strategy.

A microservice that offers a GET /users endpoint returning all system users without pagination, without filters, without authorization verification, violates architecture design principles. In a world governed by GDPR and similar regulations around the globe, exposing personal data without appropriate controls is not just bad architecture, it is illegality. A service must validate whether the requester has authorization to access that specific data. It must apply filters based on the security context. It must audit access. It must respect privacy preferences and consent.

This is different from separating into services by applying CQRS, for example, which solves a specific volume problem by separating mutation commands to one database and queries to another, which can even be a different type of database based on the problem. In this case, there is a clear technical justification based on read versus write performance and scalability requirements.

The concept of Zero Trust is particularly relevant in microservices architectures. In a monolith, internal components often trust each other implicitly, as they all run in the same process and under the same security context. In microservices, each service is a security boundary. A service should not blindly trust requests coming from other services. It should validate authentication tokens, verify authorizations, and apply security policies independently.

A truly rich microservice encapsulates not only business logic, but also security policies, validations, and access rules. An order management service does not just store orders. It verifies whether the user has permission to create that order. It validates whether products are available. It applies pricing rules considering promotions and discounts applicable to the customer's context. It checks credit limits. It generates events for other contexts. It audits the operation. This is a cohesive service that fully encapsulates a business capability.

"Without a shared model, developers don't speak the same language, and code becomes a tangle of translations and adaptations." β€” Eric Evans

The common practice of creating one microservice per database table is an extreme manifestation of the anemic model. Each entity gets its own service with CRUD operations. When a business operation requires coordination across multiple entities, that coordination happens through inter-service calls, orchestrated by some external component. This design breaks the cohesion of the domain model, distributes business logic across the network, exposes data without adequate protection, and creates temporal coupling between services that should be independent.

When we fragment excessively, we lose the shared model. Each microservice has its limited view of a small part of the domain, and nobody has a holistic understanding of the system. The architecture becomes a maze where even simple changes require coordinated modifications across multiple services, and where implementing a comprehensive security policy becomes nearly impossible.

The irrational confusion: distribution is not modularization

"Architectural boundaries are not drawn on infrastructure maps. They are drawn on dependency diagrams." β€” Robert C. Martin

The most expensive mistake I observe in microservices projects is the confusion between distribution and modularization. Modularization is the decomposition of a system into components with clear responsibilities, low coupling, and high cohesion. Distribution is the decision to run these components in separate processes, possibly on different machines. These are orthogonal decisions, but they are often treated as synonymous.

It is perfectly possible, and often desirable, to have a highly modularized system running in a single process. Modern languages and frameworks offer powerful modularization mechanisms, such as namespaces, packages, modules, and interfaces. A well-structured monolith in Java can use packages and interfaces to define clear boundaries. A .NET system can use separate assemblies. A Node.js application can organize itself into cohesive modules. In all these cases, modularization offers the benefits of maintainability and orderly evolution without the costs of distribution.

When teams jump straight to microservices without first mastering modularization, the results are predictable: microservices that are as coupled as the monolith they replaced, but now with the additional complexity of the network. Dependencies between services proliferate. Changes in one service require coordinated changes in others. Independent deployments become impossible because the services are not truly independent.

The result is what we call a distributed monolith. A system that retains all the disadvantages of a traditional monolith, with high coupling, need for coordination on changes, deployments that affect multiple services, but now with all the additional costs of distribution: network latency, distributed failure points, operational complexity, need for resilience mechanisms. It is the worst of both worlds. As we discussed earlier, a monolith is THE single project. When we distribute that project without properly breaking dependencies, we create a distributed monolith, where services are so interconnected that any change requires coordination between multiple teams, exactly the problem microservices were supposed to solve.

Domain-Driven Design: the necessary foundation

Eric Evans developed Domain-Driven Design as an approach to tackle software complexity through a deep focus on the business domain and the model that represents it. DDD concepts, especially bounded context and context map, are absolutely essential for designing microservices that work. They are not optional. They are the foundation upon which everything else rests.

"The model is the heart of the design. If the model is weak, the code will be weak, no matter how sophisticated the architecture." β€” Eric Evans

A bounded context defines the boundaries within which a particular model makes sense. Consider an e-commerce company. The concept of product means different things in the catalog context, the inventory context, and the pricing context. In the catalog, a product has description, images, and categories. In inventory, a product has available quantity, warehouse location, and reorder point. In pricing, a product has cost, margin, applicable discounts, and dynamic pricing strategy. These are three different bounded contexts, each with its own model of the product entity.

These bounded contexts are the natural candidates for microservices. When we design microservices around bounded contexts, each service owns its own model, its own database, and its own cohesive business logic. The catalog service does not need to know about inventory. The inventory service does not need to know about pricing. Communication between them happens through well-defined interfaces and, preferably, asynchronously through events. This design maximizes service independence and minimizes coupling.

Context map, another central strategic technique of DDD, describes the relationships between bounded contexts. Some contexts are upstream, providing data and functionality to downstream contexts. Some relationships are partnerships, where both contexts evolve together. Others are conformist, where one context accepts the other's model without influence. Understanding these relationships is essential for designing APIs between microservices and for avoiding excessive coupling.

Ubiquitous language, a central concept of DDD, gains even more importance in microservices architectures. When different teams work on different services, the shared language about the domain becomes the thread that maintains system coherence. If the catalog team calls something a SKU and the inventory team calls it a product code, we already have the beginning of a confusion that will propagate throughout the entire architecture.

When we ignore DDD and create microservices around technical entities or CRUD operations, we inevitably create services that do not represent cohesive business capabilities. The result is anemic, highly coupled microservices that require constant coordination to implement any real business functionality. Microservices with weak models are microservices doomed to many problems and failure.

The solution is to start with rigorous modularization within the monolith. Identify genuine bounded contexts using Domain-Driven Design techniques. Organize the code around these contexts, with well-defined interfaces between them. Ensure that modules do not directly access the database or internal state of other modules. When this modularization is solid, when bounded contexts are stable and genuinely independent, then, and only then, consider extracting some of them as separate microservices, if there is real business or technical justification for doing so.

The correct journey: from monolith to microservices

Given all the problems discussed, what is the correct approach? The answer is not to avoid microservices entirely, but to adopt them with maturity and only when genuinely necessary. The Monolith First concept proposes starting with a well-designed monolith and extracting microservices only when there are clear reasons to do so.

A well-designed monolith is modular, with clear boundaries between components. It is testable, with tests that can run quickly without external dependencies. It is deployable, with automated build and deployment processes. This monolith serves as a solid foundation. As the system grows, certain modules may emerge as extraction candidates: a module with radically different scale requirements from the rest, or a module that would benefit from independent deployment cycles because it changes much more frequently, or much less frequently, than the rest of the system.

But what to do when the existing monolith is not well designed? That is the reality for most organizations. In this case, starting with refactoring becomes imperative: reorganizing the system into modules, mapping responsibilities based on usage metrics, cyclomatic complexity, and rate of change. Identifying parts of the system with high volatility and isolating them into well-defined modules prepares the ground for safer and more predictable extractions.

At this point, the role of enterprise architecture becomes fundamental. The decision to extract microservices is not only technical but also organizational. Enterprise architecture acts as an alignment element between business domain, team structure, and technical decisions, ensuring that system boundaries reflect real boundaries of responsibility. It helps avoid duplication, defines minimum standards for interoperability, observability, and security, and supports the distribution of responsibilities among teams in a sustainable way.

When we extract a microservice, it should be done with care. It is essential to ensure that it represents a genuine bounded context. From the outset, we implement resilience mechanisms such as circuit breakers, retries, and timeouts, and establish complete observability with structured logging, metrics, and distributed tracing. We define clear SLAs and continuously monitor their fulfillment. Communication between the monolith and the new service must be carefully designed, preferably using asynchronous messaging to reduce temporal coupling.

This incremental approach allows learning from each extraction. Design mistakes made in the first microservice do not automatically propagate to the others. The organization develops distributed systems competencies progressively, avoiding an architectural big bang that distributes the entire system at once and collapses under the weight of its own complexity.

Resilience components: the essential infrastructure

A microservices system without adequate resilience components is a disaster waiting to happen. The network will fail. Services will become slow. Databases will become overloaded. These are not remote possibilities; they are certainties.

Circuit breakers are the first essential component. A circuit breaker monitors calls to an external service. If a high percentage of calls fail or exceed a timeout, the circuit breaker opens, preventing new calls for a period. This avoids overloading an already unstable service and gives it time to recover. After the waiting period, the circuit breaker allows some test calls to check whether the service has recovered. This simple pattern prevents failure cascades that bring down entire systems.

Aggressive timeouts are equally critical. A network call without a timeout can block a thread indefinitely. In systems with a limited thread pool, a few blocked calls can exhaust all available threads, making the service completely unresponsive. Each call to another service must have an explicit timeout, and that timeout should be shorter than the timeout of the client waiting for the response. The lack of this discipline in timeout configuration is a common cause of distributed deadlocks.

Bulkheads isolate resources, ensuring that failures in one part of the system do not consume all available resources. If a service depends on two external services, separate thread pools for each dependency ensure that if one dependency becomes slow, it will not exhaust all threads and prevent calls to the other dependency. This resource isolation is essential for graceful degradation.

Retries with exponential backoff allow recovery from transient network failures, but they must be used carefully. Cascading retries, where a service retries a call that internally retries another call, can amplify the load on downstream services beyond what is sustainable. Idempotency is critical. If an operation is going to be retried, it must produce the same result when executed multiple times. Implementing idempotency in complex business operations is non-trivial and repeatedly overlooked.

Microservices without these resilience components do not degrade gracefully. They collapse spectacularly, often at three in the morning while you are sleeping.

The hidden cost: operational complexity

Beyond all the technical challenges discussed, microservices introduce operational complexity that is frequently underestimated. A monolith is one artifact to deploy. Microservices are dozens or hundreds of artifacts, each with its own lifecycle. Coordinating deployments, managing versions, tracking dependencies between services becomes a full-time job.

"Distributing a system multiplies the number of things that can go wrong. And they will." β€” Gregor Hohpe

Debugging in distributed systems is exponentially harder. When a request fails in a monolith, the stack trace reveals exactly where and why. In microservices, a failure could have occurred in any of the services in the call chain. Without distributed tracing, identifying which service failed, and why, can take hours or days. Even with tracing, analyzing traces that traverse dozens of services requires specialized tools and expertise.

Data management becomes exponentially more complex. In a monolith, ACID transactions guarantee consistency. A transactional block can update multiple tables with the guarantee that either all changes will be committed or none will. In microservices, where each service owns its own database, transactional consistency across services does not exist. To handle this, patterns such as Two-Phase Commit (2PC), Try-Confirm-Cancel (TCC), Event Sourcing, and the Saga pattern exist. The most common is the Saga, where a business operation is broken into multiple local transactions with compensations to undo changes in case of failure. Implementing Sagas correctly is difficult, and bugs in compensation logic can leave the system in inconsistent states that are hard to fix.

The operational complexity of microservices is not an implementation detail. It is an ongoing cost, paid every day, for the entire life of the system.

Conclusion: the wisdom of moderation

There is no universally correct architectural solution. Microservices are a powerful tool when applied to the right problems, but they are a heavy burden when applied indiscriminately. The excessive fragmentation I observe in so many projects does not result from microservices themselves, but from the mechanical application of a pattern without understanding the principles that underpin it.

The strong point of microservices is not technical, it is organizational. It is not primarily about scale, resilience, or technological flexibility. The real value lies in being able to distribute complex problems among multiple teams that work independently. To ensure a good microservices model, it is important that the organization builds teams that reflect the desired microservices organization and, preferably, follows Domain-Driven Design principles to identify natural boundaries.

A monolith is not a problem, and the size of a service does not necessarily make it a monolith either. What defines a monolith is whether the entire project is a single entity, with all modules in a single deploy artifact. A large microservice dense in business rules is not a monolith; it is a service well-sized around a bounded context.

The wisdom lies in starting simple. A well-designed monolith, with modules clearly defined around bounded contexts of the domain, offers most of the benefits attributed to microservices without any of their costs. As the system and the organization mature, microservices can be selectively extracted where they truly add value. Each extraction should be justified, each service should represent a cohesive business capability, each interface between services should be carefully designed.

Domain-Driven Design is not optional on this journey. It is the map that guides us to identify natural boundaries where distribution makes sense. Resilience components are not later refinements; they are basic requirements from the first microservice. Observability is not a nice-to-have; it is the difference between a system that can be operated and one that collapses in mystery. The intellectual honesty to recognize that our current problems do not justify microservices requires repertoire and courage.

In the end, the measure of success is not how many microservices we have, but how well our software serves users and the business. A simple monolith that works perfectly infinitely surpasses a microservices architecture that collapses under the weight of its own complexity. The wise choice is not always the most sophisticated. Often, it is the simplest one that still solves the problem. Simplicity is the ultimate sophistication.