A team splits its monolith into microservices. Each service lives in its own repository, builds in its own pipeline and runs in its own containers. Then a new feature arrives and the same thing happens again: the order, inventory and invoicing services all change at once, the teams write a joint release note, and the services go live in a fixed order on the same night. That situation has a name: the distributed monolith. The services run in separate processes, yet they still have to change together and deploy together.

In the definition James Lewis and Martin Fowler published in 2014, the microservice architectural style develops a single application as a suite of small services, each of them independently deployable. In his QCon London 2020 talk, Sam Newman described a monolith in terms of the unit of deployment and stressed that the goal of decomposition is independent deployability. The distributed monolith, which Newman called "the worst of the monoliths", inverts that criterion: the number of services grows while the effective unit of deployment stays at one. The system pays the costs of distribution, such as network latency and partial failure, and still carries the monolith's need to change in lockstep.

The first decisions of a migration can already set the trap. When teams draw service boundaries around technical layers or database tables instead of business capabilities, share the database between services, or turn in-process method calls one-for-one into network calls, the monolith's dependencies move to the two ends of a network link without being resolved. The first task is therefore to measure where those dependencies are.

The test that defines a distributed monolith: independent deployment

The number of services or repositories does not show whether a system is a distributed monolith. The deciding question is this: can a service go to production without changes in other services and without checking their deployment schedule? If the answer is no for most changes, the system has been distributed without being decomposed.

A modular monolith is a different case. It deploys as a single unit and makes no secret of it; module boundaries are enforced in code. A distributed monolith has separate deployment units, yet teams have to manage them as one. In a modular monolith, calls between modules stay inside one process; in a distributed monolith, the same dependency is paid for as a network call.

Symptoms: when do services move together?

If services have to be deployed in sequence within the same maintenance window, they are coupled at the deployment level; this is called lockstep deployment. When a provider renames a field and the consumer still reads the old name, the system only works once both services ship together. Newman offered a signal for this coordination load: if someone in the organization holds a full-time "release coordination manager" role, the inability to deploy services independently may already have shaped the org chart.

If a single business request, say a new discount type, regularly requires code changes in three or four services, the business rule is spread across several services. Coordination between teams grows and the share of work a single team can finish on its own shrinks.

If a meaningful test of one service needs a shared integration environment where every other service is running, this points to tight runtime coupling between the services. The shared environment can turn into a bottleneck as well, since one team's broken service stops other teams' tests. A slowdown in one service that halts unrelated user operations shows the same coupling at runtime.

The fatal mistakes behind a distributed monolith

Sharing the database between services

The easiest-looking path is to leave the monolith's database as it is and split only the application code into services. The Shared Database pattern page on microservices.io lists the drawbacks of that path. Developers of services that touch the same tables must coordinate schema changes with each other, a long-running transaction in one service can block another, and a single database may not fit the storage needs of every service. In this setup the table schema becomes the real contract between services, even though it was designed as the monolith's internal data model. A team that changes the meaning of a column may not know which other services read it.

Splitting services by technical layer

A layer-based split into a UI service, a business-rule service and a data-access service moves the monolith's layered structure onto the network. Every business feature passes through all of those layers, so a small change to one feature can touch several services. In the Lewis and Fowler description, services are organized around business capabilities, and one service, owned by one team, covers a capability from the user interface down to the data.

Turning method calls into network calls and chaining synchronous requests

A method call whose cost nobody noticed inside the monolith becomes a request over the network once the code is split. Lewis and Fowler point out that remote calls are more expensive than in-process calls, so remote APIs need to be coarser-grained. Keep the monolith's fine-grained interface as it was and you get chatty APIs, where one service sends many small requests to another to fill a single screen. When those requests run in sequence, their latencies add up.

If a user request travels synchronously from service A to B, from B to C and from C to D, it succeeds only when every service in the chain is up and fast enough at that moment. The availability of the chain therefore cannot exceed that of its weakest link. If a service in the chain makes a backward-incompatible change to its interface, the service that calls it has to be updated at the same time; the longer the chain, the more of these pairings there are.

Putting the domain model in a shared library

Moving domain classes such as Customer and Order into a shared library to avoid duplicated code looks tidy at first. When a field in that library changes, the services that use the class in data exchanged between them may need to upgrade together and deploy together. With technical libraries that carry no business rules, such as logging, services can upgrade on their own schedule. A domain model can mean something different in each context, so collecting it into one shared class creates a hidden dependency between services.

Protecting atomicity with two-phase commit

When order, inventory and payment updates that used to run inside one database transaction are split across services, teams may try to keep the same atomicity with protocols such as two-phase commit. Lewis and Fowler note that distributed transactions are hard to implement and that microservices favor coordination without transactions, eventual consistency and compensating operations. A distributed transaction requires every participant to be reachable during the operation and to support the same protocol, so it locks services to each other at runtime. We covered the Saga pattern in earlier posts. The point here is that a workflow that keeps needing atomic updates through a protocol such as two-phase commit can point to a misplaced boundary.

How to detect a distributed monolith

Symptoms can be spotted by intuition, but showing which services move together takes data. The places to look are version control history, deployment records, call traces and database access logs.

Change coupling in version control history

Change coupling means that two or more modules change together over time. The CodeScene documentation counts two modules as coupled in three cases: when they are modified in the same commit, when the same developer modifies both within a specific period of time, or when their commit messages refer to the same ticket ID. If services live in separate repositories, the same-commit criterion stops working; the most direct method, provided commit messages include ticket IDs, is the third. Pull the last few months of history from each service repository, group commits by ticket ID and list the services each ticket touched. A pair of services that keeps appearing together can point to a dependency the code does not show.

The same documentation states that change coupling "in itself is neither good nor bad; it all depends on what modules that are coupled and for what reason they co-evolve". The documentation therefore recommends comparing change coupling data with your architectural principles, since deviations might indicate real problems. For services, the signal to watch is services that change together regularly without an architectural reason to do so.

Change coupling shows that code changes together; deployment coupling shows that it has to ship together. Groups of services deployed within a short interval for the same ticket in CI/CD records, and release notes with instructions like "deploy X first, then Y", are traces of it. If a rollback means rolling back several services together, or if the team keeps a table of which service versions work with each other, the services are coupled at the version level.

Call graph and database access map

Service mesh metrics show which service calls which; distributed tracing data shows which services a single user request passes through synchronously. Together they are enough to build a call graph. Look first at the longest synchronous chain, then at cycles between services. A cycle in which A calls B and B also calls A can indicate that one responsibility has been split across two services. The number of calls between two services during one request also exposes chatty APIs.

If services share a database, also map which tables each service reads and writes. When each service connects with its own database user and auditing is enabled, the audit log can provide this. Tables written by more than one service have unclear data ownership, and they come first in the decomposition work.

The way out: rebuilding boundaries and data ownership

Drawing boundaries with bounded contexts

Domain-Driven Design deals with a large model by dividing it into bounded contexts, each with its own unified model. Fowler quotes Eric Evans: total unification of the domain model for a large system "will not be feasible or cost-effective". Fowler also notes that polysemes such as "Customer" and "Product" cause confusion when teams try to give them a single meaning across contexts. The customer in a sales context and the customer in a support context may not share the same fields and rules, and the attempt to merge them into one model is one of the sources of the coupling a shared domain library creates.

The detection data feeds into redrawing service boundaries along these contexts. Two services that keep changing together and call each other in a cycle may belong to the same context, and merging them is one legitimate way out of a distributed monolith. Reducing the number of services can look like a step back, yet putting code that changes together into one deployment unit cuts both network cost and coordination load.

Data ownership

Each piece of data should have one owner, and other services should reach it only through the owner's API or the events it publishes. The Database per Service pattern on microservices.io defines three options for relational databases: private tables per service, a schema per service, or a database server per service. The first two make ownership explicit without moving to separate servers. The decisive step is closing off direct table access for every other service.

The same page also states the cost: business transactions that span services and queries that join data across databases become harder. For these it points to the Saga, API Composition and CQRS patterns.

Turning synchronous calls into events: when?

If the calling service does not need the other service's answer at that moment to finish its work, publishing an event instead of making a synchronous call decouples the two at runtime. An invoice issued after an order is confirmed fits this case: the order service publishes an OrderConfirmed event, and the invoicing and notification services process it at their own pace. If the invoicing service stops for a short while, the business can keep taking orders.

For steps that need a definite answer right away, such as a stock reservation or a payment approval, a synchronous call can remain the right choice. Asynchronous communication has a cost too. Fowler's Microservice Premium post lists eventual consistency among the extra costs of microservices, and the user interface and reporting have to account for it. We described the Transactional Outbox pattern, which records the event in the same transaction as the database change so it can be published without loss, in our post on data consistency in event-driven architectures.

Contract and versioning discipline

Backward-incompatible interface changes are one direct cause of lockstep deployment. If teams evolve the API and event contracts between services through backward-compatible additions, and serve the old and new versions side by side for a period when a breaking change is unavoidable, provider and consumer can deploy on their own schedules. These contracts also replace the shared domain library: each service keeps its own model and only the data defined in the contract crosses service boundaries. Designing the contract before the code and verifying it with consumer-driven contract tests is the subject of our API-First post.

The modular monolith: an intermediate step, and sometimes the destination

In his 2015 Microservice Premium post, Fowler writes that microservices bring extra work such as automated deployment, monitoring, dealing with failure and eventual consistency. He ties the decision to the complexity of the system and advises against considering microservices for a system that is not too complex to manage as a monolith. In Monolith First, published the same year, he writes that "even experienced architects working in familiar domains have great difficulty getting boundaries right at the beginning", and that refactoring functionality between services is much harder than inside a monolith. Fowler also notes that the evidence is still sparse and that anybody's advice on these topics must be seen as tentative.

Read together, the two posts make the role of the modular monolith clear: while boundaries are still uncertain, a team can try them out inside one deployment unit without paying the network cost. In the same QCon talk, Newman said microservices are not a good choice for most startups and proposed a modular monolith with multiple databases instead.

What separates a modular monolith from an ordinary monolith is that its boundaries hold. Each module exposes only a defined public interface, modules do not reach into each other's tables, and architecture tests in the CI pipeline, such as checks on dependencies between packages, enforce these rules. As Fowler warns, a monolith that accumulates too many inter-module dependencies may not split into services easily later.

Fowler's Microservice Prerequisites post lists the baseline capabilities a team needs before adopting the microservice style: rapid provisioning, basic monitoring and rapid application deployment. Our recommendation: if a module needs different scaling or a separate release cadence, and those capabilities are in place, extracting it as a service starts to make sense.

A hypothetical scenario: three services sharing one database

The following example is fictional and only illustrates the method. An e-commerce company split its monolith into order, inventory and invoicing services but kept the database shared. The order service writes to the orders table, and the invoicing service reads the same table and updates the invoice status in it. When an order is created, the order service calls the inventory service separately and synchronously for each order line. The Order class lives in a shared library.

When the team groups six months of commit history by ticket ID, it finds that most tickets touching the order service also required changes in the invoicing service. Deployment records show the two services shipping mostly on the same day and always in the same order. Based on these findings, the team plans the following steps:

  1. The invoicing service takes ownership of its own invoices table, learns that an order is ready for invoicing from the OrderConfirmed event, and loses its access to the orders table.
  2. The Order class leaves the shared library; each service maps the fields in the event contract to its own model.
  3. Stock reservation needs an immediate answer before an order can be confirmed, so it stays synchronous, but the per-line calls collapse into one coarse-grained reservation request.
  4. After the changes, the team repeats the same measurements and tracks the share of tickets that touch both the order and invoicing services, along with the number of days on which the two services deploy together.

If the two services keep changing together despite the event contract, merging order and invoicing into a single service becomes one of the options on the table.

Running the decomposition step by step

A one-shot rewrite risks repeating the same boundary mistakes in new code. Newman advises an incremental approach to decomposition that starts with the question "what does your current architecture not let you do?". One starting point is the pair of services that shows the strongest coupling in the detection data: the team clarifies data ownership and the contract for that pair, then measures again. The Strangler Fig pattern can be used to bring new boundaries into service alongside the old system step by step.

The metrics that show progress are the ones used for detection: the share of changes that touch more than one service, the number of deployments that must happen together, and the depth of synchronous call chains. As these numbers fall, teams can expect to spend less time on release coordination.

Sources: Microservices, James Lewis and Martin Fowler (https://martinfowler.com/articles/microservices.html), Microservice Premium (https://martinfowler.com/bliki/MicroservicePremium.html), Monolith First (https://martinfowler.com/bliki/MonolithFirst.html), Microservice Prerequisites (https://martinfowler.com/bliki/MicroservicePrerequisites.html), Bounded Context (https://martinfowler.com/bliki/BoundedContext.html), Shared Database (https://microservices.io/patterns/data/shared-database.html), Database per Service (https://microservices.io/patterns/data/database-per-service.html), InfoQ, Sam Newman at QCon London 2020 (https://www.infoq.com/news/2020/05/monolith-decomposition-newman/), CodeScene Change Coupling (https://docs.enterprise.codescene.io/versions/6.0.0/guides/technical/change-coupling.html)