Skip to content
Leo Jung
Go back

[DDD Intro #7] Why DDD fails

Edit page

After studying DDD, it feels like design should get better. Once we learn concepts such as Entity, Value Object, Aggregate, Repository, and Domain Event, it feels like complex business logic can be organized cleanly.

But in reality, the opposite often happens. A team adopts DDD and the code becomes more complicated. There are more classes, packages are split, and interfaces increase, but the domain logic is still scattered through Service classes. Aggregates become too large to modify, Repositories are no different from DAOs, and Ubiquitous Language remains only in documents.

Then the team says:

DDD is too hard.
DDD does not fit us.
DDD lowered our productivity.

That can happen. DDD is not suitable for every problem. But many failures are less a failure of DDD itself and more a failure in how DDD is applied.

The most common reason DDD fails is that a way of thinking for handling complexity is misunderstood as a way to apply patterns.

1. Starting from patterns

The most common failed starting point for DDD adoption is starting from patterns.

How should we create Entities?
How far should we split Value Objects?
What should be the Aggregate Root?
Where should Repository interfaces live?
When should we publish Domain Events?

These questions matter. But they should not be the first questions.

The first DDD questions should be:

What domain problem are we solving?
What are the most important rules in this domain?
What language do domain experts use to explain this problem?
Where is the same word used with different meanings?
Where should we spend the most design energy?

When we start from patterns, DDD turns into a set of object-oriented design rules. The code gains names such as AggregateRoot, ValueObject, and Repository, but domain knowledge remains outside the code.

Tactical patterns should be the result of domain understanding. If patterns are applied before the domain is understood, we create empty shells with impressive names.

2. Modeling only among developers without domain experts

DDD is a way for domain experts and developers to build a model together. But in practice, developers often gather and model by themselves.

From a developer’s perspective, subscription status may look like this:

ACTIVE
CANCELED
EXPIRED

But when we talk with domain experts, more differences emerge.

In free trial
In paid subscription
Scheduled for cancellation
Grace state due to payment failure
Suspended due to payment failure
Period expired
Admin-terminated
Refund in progress

This does not simply mean there are more status values. Each state may have different possible actions, policies, and exceptions.

Cancellation during a free trial may remove access immediately. Cancellation of a paid subscription may keep access for the remaining period. Suspension due to payment failure may be recoverable when the payment method is updated. A subscription in refund processing may restrict additional changes.

A model created without domain experts looks simple and clean at first. But once operational rules arrive, it collapses into conditions and exceptions.

DDD without domain experts easily becomes developer-driven guessing, not Domain-Driven Design.

3. Ubiquitous Language does not reach the code

Many teams create a glossary when adopting DDD. But if Ubiquitous Language exists only in documents and not in code, its effect is limited.

Suppose documents say “subscription renewal,” “scheduled cancellation,” and “suspension due to payment failure,” but the code only has names like this:

subscription.updateStatus(2);
subscription.process();
subscription.change("C");

This code does not preserve domain language. Anyone reading it must translate numbers and flags back into domain meaning.

When domain language is reflected in code, things change.

subscription.renew(paymentResult);
subscription.cancelAtPeriodEnd(reason);
subscription.suspendDueToPaymentFailure();

Changing method names alone does not solve every problem. But names matter. A name reveals how we understand the domain.

Ubiquitous Language is not a glossary. It is a living language used together in meetings, documents, code, and tests. If this language does not reach the code, only the shell of DDD remains.

4. Creating one huge model without Bounded Context

Another reason DDD fails is not dividing boundaries. A team tries to solve every problem with one integrated model.

Suppose every team shares a single model called Subscription.

The payment team needs recurring billing cycle and failure count. The entitlement team needs accessible period and content range. Customer support needs compensation history and admin adjustment reasons. Marketing needs free trial status and campaign response information.

If all these concerns enter one Subscription, the model grows bloated.

Subscription
- payment information
- entitlement information
- customer support compensation information
- marketing campaign information
- analytics flags
- operational notes
- various status codes

At first, this looks like reuse. But over time, no team can safely modify the model. Changing one field creates unexpected issues elsewhere. The same word starts to carry several meanings, and the code becomes increasingly ambiguous.

A model is consistent only within a boundary. One word does not always mean one model. Without Bounded Context, tactical patterns cannot prevent models from contaminating each other.

5. Understanding Aggregate as an object graph

Aggregate is one of the most common failure points in DDD implementation. A common misunderstanding is to treat an Aggregate as a group of related objects.

A subscription has payments, a payment has receipts, a subscription has entitlements, an entitlement has a content list, and customer support history is also related to the subscription, so everything goes into one Aggregate.

This makes the Subscription Aggregate too large. Queries become heavy, changes become difficult, and concurrency conflicts increase. Most importantly, it becomes unclear which invariant this boundary is meant to protect.

An Aggregate is not a unit for gathering related objects. An Aggregate is a boundary that protects consistency.

The questions should change to:

What must change together?
Which rules must be preserved in one transaction?
What can be reflected later through events?
Can another Aggregate be referenced by ID rather than object reference?

Subscription and payment are deeply related. But they do not always have to be the same Aggregate. Payment failures or refund history may be better handled by a separate Aggregate or another context. What matters is the invariant, not the relationship.

6. All business logic accumulates in Application Service

Even when teams say they are doing DDD, all business logic often ends up in Application Service.

public void cancelSubscription(SubscriptionId id) {
    Subscription subscription = repository.findById(id);

    if (subscription.getStatus() == EXPIRED) {
        throw new IllegalStateException("The subscription has already expired.");
    }

    if (subscription.isPaid()) {
        refundService.requestRefund(subscription.getPaymentId());
    }

    subscription.setStatus(CANCELED);
    repository.save(subscription);
}

This code can work. But the domain rules live in the Application Service. Which states can be canceled, what paid subscription cancellation means, and how refund relates to cancellation are not inside the domain model.

A DDD-style flow asks the domain object to perform behavior.

subscription.cancelAtPeriodEnd(reason);

Or it may distinguish immediate cancellation and end-of-period cancellation by policy.

subscription.cancelImmediately(reason);
subscription.cancelAtPeriodEnd(reason);

Application Service coordinates a use case. It loads Aggregates, calls domain behavior, saves changes, and publishes required events. But core domain rules should live inside the domain model.

If the Application Service makes every decision, the domain model becomes an empty data object.

7. Repository bypasses domain behavior

Repository is an abstraction that handles the lifecycle of an Aggregate. But in practice, Repository is often used like a DAO and bypasses domain behavior.

subscriptionRepository.updateStatus(id, CANCELED);
subscriptionRepository.extendPeriod(id, nextEndDate);
subscriptionRepository.incrementPaymentFailureCount(id);

This feels fast and convenient. But it is dangerous. Data is changed directly without passing through the rules the domain object must protect. Invariants break easily, and rules scatter across queries and service methods.

A more natural DDD flow is:

Subscription subscription = repository.findById(id);
subscription.suspendDueToPaymentFailure();
repository.save(subscription);

The first style modifies data. The second asks the domain object to perform behavior.

Once Repository starts bypassing domain behavior, the domain model becomes an object we can no longer trust.

8. Applying DDD everywhere

DDD is powerful, but it is also costly. Applying DDD to every feature can make the system unnecessarily complicated.

Simple admin CRUD, code-like data management, and features with almost no change rules may not need a deep domain model.

Notice management
Banner management
FAQ management
Simple category management
Terms text management

If we apply Aggregate, Domain Event, Repository, and Domain Service to all of these, the design becomes larger than the problem.

DDD is a method for handling complex domains. In areas with low complexity, a simpler structure is better. Conversely, design energy should be focused on core domains with high complexity.

Doing DDD well does not mean using DDD patterns everywhere. It means judging where deep modeling is needed.

9. Mistaking microservice splitting for DDD

DDD and microservices are often mentioned together. It is true that Bounded Context helps find microservice boundaries. But they are not the same concept.

Bounded Context is a model boundary. Microservice is a deployment boundary.

Splitting services does not mean doing DDD. In fact, if services are split without understanding model boundaries, the result can be a distributed monolith.

Services are split, but they share the same database.
Every service uses the same DTOs.
A status code change in one service breaks other services.
There are too many calls between services.
Transaction boundaries are unclear.

In this structure, only deployment units are separated. The models are still tangled.

Before splitting services, find model boundaries first. Think about how far the same word has the same meaning, which contexts should evolve independently, and which relationships should be made explicit through events or APIs.

Microservice can be a result of DDD, but it is not DDD itself.

10. Not refining the model continuously

In DDD, a model is not an artifact created once and finished. As domain understanding deepens, the model must change too.

At first, one Subscription may look sufficient. But over time, concepts such as BillingAgreement, Entitlement, Refund, RenewalPolicy, and CancellationPolicy may emerge.

An action that was initially just cancel() may later split into several actions.

subscription.cancelImmediately(reason);
subscription.cancelAtPeriodEnd(reason);
subscription.expire(now);
subscription.terminateByAdmin(reason);

This change is not failure. It is a signal that the domain is better understood.

The problem is insisting on the first model forever. If the model does not change, new requirements accumulate only as exceptional conditions and flags. Eventually the model no longer explains the domain. It becomes a structure that preserves old guesses.

A good model is continuously refined. DDD is not a way to find a perfect model at once. It is a process of repeatedly reflecting domain understanding in code.

What should we do first to avoid failure?

The first thing to do to avoid DDD failure is to set aside patterns for a moment and return to domain questions.

What are the most important business rules in this domain?
Which words do domain experts use repeatedly?
Do those words all have the same meaning?
Which state changes are especially important?
Which rules require immediate consistency?
Which areas should be separated into different models?

Then start small. Do not try to convert the entire system to DDD at once. Choose one core domain with high complexity. Inside it, refine the language, create a model, experiment with Aggregate boundaries, and express rules through tests.

Also abandon the expectation of creating a perfect model from the start. A good model is discovered through conversation, verified through code, and revised through operational experience.

Closing

DDD does not fail only because it is hard. DDD fails when it is misunderstood as a way to apply patterns, when developers guess without domain experts, when language does not reach the code, and when model boundaries are not divided.

Having Entity, Value Object, Aggregate, and Repository does not mean we are doing DDD. DDD exists when the language and rules of the domain are alive in code.

DDD is not a way to create complicated code. It is a way to look honestly at a complex domain and give names, boundaries, and responsibilities so that complexity does not scatter everywhere.

In the end, DDD failure is closer to a failure of conversation than a failure of patterns. Good DDD does not start from a good class structure. It starts from good questions, good language, and good boundaries.


Edit page
Share this post:

Previous Post
[DDD Intro #8] Core Domain: Do not spend the same care everywhere
Next Post
[DDD Intro #6] Domain Event: Expressing what happened in the domain