← Back to list

Why Most “DDD Microservices” Projects End Up as Distributed Monoliths

Domain-Driven Design

JIN in JIN System Architect · 2026-06-16 06:53 · 50 claps · 12.7 min read paywalled
#distributed-systems #system-design-interview #microservices #domain-driven-design #monolithic-architecture
Open on Medium ↗
Wiki topics: 🏛️ · Architecture

Why Most “DDD Microservices” Projects End Up as Distributed Monoliths

Domain-Driven Design

Disclosure: I use GPT search to collection facts. The entire article is drafted by me.

There’s a specific arc that plays out on engineering teams when they first discover Domain-Driven Design.

It starts with genuine excitement. Someone reads Eric Evans’s blue book, or watches a conference talk, and suddenly the most frustrating problem in modern software engineering — where do I draw the service boundary? — appears to have a rigorous answer. There’s a framework. There’s a vocabulary. There’s a methodology.

So the team gets to work. Bounded contexts. Aggregate roots. Domain events. CQRS. Saga patterns. The architecture diagram looks impressively sophisticated after six months.

Then, quietly, the system starts to misbehave.

Deployments still require coordinating four teams. A field rename in one service cascades through three others. The Kafka topic count passes 200, and nobody has a complete map of what’s consuming what. A simple order cancellation spawns a debugging session that takes two days because the event chain runs through seven services before anything actually happens.

And eventually someone in a retrospective says something uncomfortable: “This feels like our old monolith, except now the pieces are in different processes.”

That’s because it is. What the team built isn’t microservices. It’s a distributed monolith — and it’s genuinely the worst possible outcome. All the operational complexity of distributed systems, all the coupling problems of a monolith, and the benefits of neither.

Understanding how this happens, specifically and mechanistically, is the thing most DDD tutorials fail to do.

Start With What Microservices Actually Mean

Before diagnosing what goes wrong, you need a clean definition of what microservices are for — because the popular understanding is wrong.

Most developers hear “microservices” and translate it as “small services.” That’s not wrong exactly, but it’s dangerously incomplete. The word “micro” is actually the least important part of the term.

Sam Newman, who wrote the most widely read practical guide on the subject, is explicit: the defining property of microservices is independent deployability. Not small in size. Not separate repositories. Not their own databases. Those are implementation patterns that support the goal — but the goal itself is this:

You should be able to change one service, deploy it to production, and have nothing else need to change.

If you cannot do this routinely — if deploying Service A requires simultaneously deploying Service B and C, or requires coordinating with another team, or requires a regression test spanning the whole system — then you don’t have microservices. You have a distributed monolith, regardless of how your architecture diagram looks.

This definition matters enormously because it reframes the entire project. Microservices aren’t primarily a technical problem. They’re an organizational design problem. The technology serves one purpose: giving teams the ability to move independently.

Amazon’s “two-pizza team” principle wasn’t invented as an architecture philosophy. It was invented to stop teams from blocking each other. The microservices architecture followed from that organizational constraint, not the other way around.

When teams adopt microservices for technical reasons — “this is the modern way to build software,” “we want to use Kubernetes properly,” “our current architecture feels messy” — without addressing the organizational substrate, the outcome is almost always a distributed monolith.

What DDD Actually Teaches (vs. What People Learn)

AI Generated Image

AI Generated Image

When developers encounter DDD, they typically spend 80% of their learning time on the tactical patterns: entities, value objects, aggregate roots, repositories, and domain services. These are concrete, testable, and graspable. You can write code that “looks DDD.”

The strategic patterns — bounded context, context map, ubiquitous language — get less attention because they’re fuzzier and harder to code-review your way through. But they’re the part that actually determines whether a DDD project succeeds or becomes a mess.

Eric Evans himself has said this repeatedly, including at DDD Europe: DDD is not about model perfection. It’s about controlling complexity. And the mechanism for controlling complexity at the system level is the bounded context.

Here’s the thing that trips up most teams: a bounded context is not a service, a module, or a database schema. It’s a semantic boundary — a boundary inside which a particular model of the world is consistent and inside which a particular language means a specific thing.

Consider the word “order.” In a transaction context, an order is a purchase commitment with a customer, a price, a SKU list, and a status machine. In a warehouse context, an order is a picking task with bin locations, a packing sequence, and a weight constraint. In a finance context, an order is a receivable with tax implications and a settlement timeline. In a customer service context, an order is a support case with a customer sentiment attached.

Same word. Four completely different models. Four different teams that, if you mix their models, will produce inconsistency at exactly the moments when the business most needs clarity.

The purpose of a bounded context is not to separate codebases. It’s to protect semantic integrity. When you violate a bounded context boundary — when the finance team starts reading the order table that the transaction team owns — you’re not just creating a technical coupling. You’re creating a situation where “order” means different things to different parts of the codebase simultaneously, and every future change has to account for all those meanings at once.

This is how the Big Ball of Mud forms. It doesn’t form because people are lazy. It forms because the model boundaries weren’t clear, so every team used the same model for their own purposes, and eventually, the model means nothing specific to anyone.

The Wrong Way to Draw Service Boundaries

AI Generated Image

AI Generated Image

The most common decomposition mistake is splitting services by data entity rather than by business capability.

# This is what most teams build:
user_service        → owns users table
order_service       → owns orders table
product_service     → owns products table
inventory_service   → owns inventory table
payment_service     → owns payments table

It feels clean. Every service has clear ownership of a database table. The diagram is neat.

Then the first real feature request arrives. “When a customer checks out, we need to: validate their account is in good standing, check product availability, reserve inventory, create the order, charge payment, send a confirmation, and update their order history.”

Now watch what happens to the call chain:

# What "checkout" actually looks like in a table-split microservice system:
def checkout(cart_id, user_id):
    user = user_service.get_user(user_id)           # call 1
    if not user_service.is_eligible(user_id):       # call 2
        raise UserNotEligibleError()

    items = cart_service.get_cart(cart_id)          # call 3

    for item in items:
        product = product_service.get_product(      # call 4..N
            item.product_id
        )
        if not inventory_service.check(             # call N+1..2N
            item.product_id, item.quantity
        ):
            raise OutOfStockError()

    # Now we're in distributed transaction territory
    order = order_service.create_order(user_id,     # call 2N+1
        items)

    for item in items:
        inventory_service.reserve(                  # call 2N+2..3N
            item.product_id, item.quantity
        )

    payment_result = payment_service.charge(        # call 3N+1
        user_id, order.total
    )

    if payment_result.failed:
        # Now we need to undo the inventory reservations
        # and cancel the order...
        # Welcome to distributed saga territory
        pass

    notification_service.send_confirmation(...)     # call 3N+2
    return order

What was a single transactional function in a monolith is now a chain of 10+ network calls, each of which can fail independently, timeout independently, or return stale data. You’ve traded one database transaction for a distributed coordination problem.

The system is technically “microservices.” But the business capability — checkout — is not owned by any single service. It lives in the gaps between services, in the coordination logic that now has to exist somewhere.

This is the distributed monolith pattern. The coupling didn’t go away. It moved from the database layer to the network layer, where it’s harder to see and much harder to fix.

Why Aggregates Are About Transactions, Not Objects

This is the most consistently mis-taught concept in DDD.

Most tutorials present aggregates as “a cluster of related objects that should be treated as a unit.” They show examples: an Order contains OrderLines, which contain Products. An Account contains Transactions.

That’s not wrong, but it misses the actual architectural purpose. An aggregate defines a transaction boundary — specifically, the boundary inside which you need strong consistency.

This matters enormously for microservices because every time you discover that two operations need to be atomically consistent, you’re implicitly discovering that they belong in the same aggregate — and possibly in the same service.

The classic example: when a customer places an order, does inventory need to be deducted in the same transaction?

If the answer is “yes, absolutely, inventory must reflect the order atomically,” then you’ve just discovered that Order and Inventory might need to be in the same bounded context, not different services.

If the answer is “we can accept brief oversell, we’ll reconcile later,” then eventual consistency is acceptable, and they can be separate services communicating through events.

This is why asking “Should Order and Inventory be in the same service?” is the wrong question. The right question is: what consistency guarantee does the business actually require, and what’s the cost of relaxing it?

Most businesses, when pushed, accept more eventual consistency than they initially claim. E-commerce platforms routinely accept oversell. Airlines routinely overbook. Ticket platforms routinely show availability that’s slightly stale. The business accepts these because the alternative — strict consistency at scale — requires architectural choices (two-phase commit, synchronous coordination) that destroy availability.

Netflix has been explicit about this since their early distributed systems work: in large-scale distributed systems, “fully consistent” is usually the wrong target. The real question is whether the system can recover from inconsistency, not whether it can prevent it.

The aggregate boundary forces you to answer this question explicitly rather than assuming you need distributed transactions everywhere.

The Saga Trap: When Your Architecture Is Telling You Something

A lot of teams eventually introduce Saga patterns — choreography or orchestration — to handle cross-service consistency.

Sagas are real and useful. But there’s a specific way they get misused that reveals a boundary problem rather than solving it.

# A Saga that looks like this is a warning sign:
class CheckoutSaga:
    def start(self, order_data):
        self.reserve_inventory(order_data)      # Step 1

    def on_inventory_reserved(self, event):
        self.create_order(event.order_data)     # Step 2

    def on_order_created(self, event):
        self.charge_payment(event.order_id)     # Step 3

    def on_payment_charged(self, event):
        self.confirm_order(event.order_id)      # Step 4

    # Compensating transactions for every step...
    def on_payment_failed(self, event):
        self.cancel_order(event.order_id)
        self.release_inventory(event.order_id)

When your Saga has four steps, four compensating transactions, and coordinates four different services for a single business operation, the complexity cost is real: you have to reason about 4! possible failure orderings, implement idempotent compensations for each, and write a test matrix that most teams don’t actually complete.

The question worth asking: if these four steps are always executed together, always need to be coordinated, and always need to compensate together, what was the organizational benefit of splitting them into four services?

If the answer is “these are maintained by four different teams with independent release cycles,” that’s a legitimate reason. The complexity cost buys organizational independence.

If the answer is “we wanted to keep the services small,” you’ve paid the distributed systems tax without getting the organizational benefit in return.

I’m not saying Sagas are wrong. They’re necessary in genuinely multi-team workflows with real independence boundaries. But they’re frequently introduced as a technical fix for what is actually a boundary design error.

Conway’s Law Will Override Your Architecture Document

This is the constraint that trumps everything else.

Conway’s Law isn’t a theory or a heuristic. It’s closer to a physical law: the system your organization builds will replicate the communication structure of your organization. You cannot engineer your way out of it.

An order flow that crosses the jurisdiction of five teams — product, marketing, payment, risk, and inventory — will have five-way coordination built into it at the code level, no matter what your architecture diagram says. The coordination that exists between teams will exist between their services. It cannot be otherwise, because the people writing the interfaces are the same people who have to negotiate across team boundaries to agree on what those interfaces contain.

This is why microservices succeed at Amazon and fail at mid-sized companies imitating Amazon’s architecture. Amazon structured its organization first — small, fully autonomous teams with end-to-end ownership of a business capability — and the distributed architecture followed from that. Companies that copy the architecture without changing the organization get the distributed complexity without the organizational independence.

The Inverse Conway Maneuver — deliberately restructuring teams to match your desired architecture — is real and sometimes the right approach. But it requires organizational authority that most engineering leads don’t have, and it’s a slower process than most teams realize.

If your organization hasn’t changed how teams are structured, how ownership is assigned, and how releases are coordinated, your “microservices transformation” will produce a distributed monolith. The code topology will converge on the org chart topology. It always does.

Event-Driven Architecture Is a Design Philosophy, Not a Technology Choice

Introducing Kafka doesn’t make your system event-driven any more than buying a piano makes you a pianist.

Here’s the difference in a single scenario. An order is cancelled. The system needs to: restore inventory, refund the payment, return loyalty points, and send a notification.

The wrong pattern — order service calls everyone:

# Order service becomes the central coordinator
def cancel_order(order_id):
    order.status = "CANCELLED"
    inventory_service.restore(order.items)          # sync call
    payment_service.refund(order.payment_id)        # sync call
    loyalty_service.restore_points(order.user_id)  # sync call
    notification_service.send_cancellation(...)     # sync call

The order service now knows about inventory, payment, loyalty, and notification systems. It’s the hub of a wheel. Adding any new behaviour on cancellation requires modifying the order service, which means the order service owns a dependency on every other system in your organization. The coupling is real, even if it’s over Kafka instead of REST.

The right pattern — order service expresses a fact:

# Order service publishes a domain event and stops
def cancel_order(order_id):
    order.status = "CANCELLED"
    event_bus.publish(OrderCancelledEvent(
        order_id=order_id,
        user_id=order.user_id,
        items=order.items,
        payment_id=order.payment_id,
        cancelled_at=now()
    ))
    # Order service is done. It doesn't know or care
    # who handles this event.

Each downstream service independently subscribes to OrderCancelledEvent and handles its own concern. When the loyalty team adds a new behaviour on cancellation, they write a new subscriber. The order service doesn't change.

The difference isn’t the message bus. The difference is whether the publishing service knows who its consumers are. If it does, the coupling is still there, just slower. If it doesn’t, you’ve achieved genuine decoupling.

But event-driven architecture has real costs that don’t appear in the conference slides: message loss, duplicate delivery, ordering guarantees, dead letter queues, consumer lag monitoring, event schema evolution, and the particular misery of debugging a failure that happened four hops back in an async event chain.

These costs are worth bearing when the independence benefits are real. They’re not worth bearing for internal CRUD APIs on a ten-person team. Over-event-ification of small systems is a real failure mode, and teams that have been through it come back to synchronous calls for good reason.

The Modular Monolith Conversation Nobody Wants to Have

A 2024 academic survey found that modular monolith architecture is increasingly being treated not as a step backward, but as a legitimate destination — an architecture that combines monolith deployment simplicity with internal module boundary discipline.

Amazon, Shopify, and Segment have all published retrospectives on service consolidation — merging services back together after discovering that the independence benefits weren’t materializing, but the operational costs were real. This isn’t failure. It’s engineering.

The modular monolith gets you most of what DDD promises: clear bounded contexts, enforced dependency rules between modules, ubiquitous language per domain, and clean aggregates. What you give up is independent deployability across teams. But if you don’t actually have independent teams — if the same three engineers own four services — you weren’t getting that benefit anyway.

My honest position: most teams under about 30 engineers, with business models that are still evolving, should start with a well-structured modular monolith and extract services at the boundary points where the organizational need for independence actually materializes. Not where it theoretically might materialize. Where it actually has.

The cost of a premature microservices decomposition is years of distributed systems complexity paid before you know whether your service boundaries were right. Boundaries that seemed obvious at design time turn out to be wrong all the time — business domains shift, team structures change, products pivot. In a monolith, moving a boundary is a refactor. In microservices, moving a boundary is a data migration across services, a protocol renegotiation, and a redeployment coordination exercise.

The Only Three Things That Actually Matter

If I had to compress the entire DDD microservices body of knowledge into three operational principles:

1. Draw boundaries by team ownership, not by data tables.

The question isn’t “which database tables belong together.” It’s “which business capability can a single team own end-to-end, including the decision authority to change the model without asking anyone’s permission.”

If answering a feature request requires coordinating with another team to modify their service’s interface, your boundary is in the wrong place.

2. Treat consistency requirements as architecture signals, not implementation problems.

When you find yourself reaching for a distributed transaction, a Saga, or a two-phase commit — stop and ask whether that’s a sign that the two things you’re trying to coordinate belong in the same service rather than different ones. Sometimes the answer is no, and the Saga is the right tool. But often the answer is yes, and you’re about to add tremendous complexity to solve a problem that a better boundary would have prevented.

3. Match your architecture to your org structure, or change your org structure first.

You cannot have a microservices architecture with a monolith org. The coordination patterns of the organization will express themselves in the coupling patterns of the code. Either the org is structured around autonomous teams with end-to-end ownership, or the architecture will revert to a distributed monolith over time, regardless of your intent.

The Real Question at the Start of Any DDD Project

At the beginning of any architecture conversation, before the first whiteboard diagram, the question worth asking is: what coordination problem are we actually trying to solve?

If the answer is “different teams need to deploy on different schedules without blocking each other,” microservices solve that — but only if the teams are actually structured to be independent, and the boundaries are drawn around their ownership.

If the answer is “our codebase is too complex to understand,” that’s a modular design problem. Better module boundaries inside a monolith likely solve it at a fraction of the operational cost.

If the answer is “everyone on the team read about microservices and is excited about them,” that’s not an architecture problem at all. It’s an expectation management problem.

DDD is, at its best, a method for making the implicit complexity of a business domain explicit and manageable. It’s a set of tools for having the hard conversations about what words mean, where change propagates, and what must be consistent with what. Those conversations are valuable regardless of whether the result is microservices, a modular monolith, or anything else.

The architecture is downstream of the conversations. Get the conversations right first.

Everything else is implementation.

If you’d like to show your appreciation, you can support me through:

**Patreon ✨ [Ko-fi](https://ko-fi.com/jinlowmedium) ✨ [BuyMeACoffee](https://buymeacoffee.com/jinlowmedium)**

Every contribution, big or small, fuels my creativity and means the world to me. Thank you for being a part of this journey!


메타데이터
post_id
da45f3c797e4
slug
why-most-ddd-microservices-projects-end-up-as-distributed-monoliths-da45f3c797e4
url
https://medium.com/jin-system-architect/why-most-ddd-microservices-projects-end-up-as-distributed-monoliths-da45f3c797e4
canonical_url
https://medium.com/jin-system-architect/why-most-ddd-microservices-projects-end-up-as-distributed-monoliths-da45f3c797e4
author_url
https://medium.com/@jinlow
status
ok
fetched_at
2026-06-21 15:33:18