iTranslated by AI
Is DDD-Lite Really an Anti-Pattern? The Reality of Incremental DDD Adoption
Introduction
In discussions about DDD, the claim that "DDD-Lite is an anti-pattern" is often encountered. Vaughn Vernon issued a warning regarding "DDD-Lite" in Chapter 1 of Implementing Domain-Driven Design (2013). DDD-Lite is a method that selectively adopts only tactical patterns. According to Vernon, this practice leads to the construction of "inferior domain models."
I agree with this observation. If one remains stuck only with tactical patterns, the quality of the domain model will eventually plateau. However, Vernon's warning is a critique of "stopping at tactical patterns," not a rejection of "starting with tactical patterns."
In real-world projects, due to team skill sets, domain understanding, and schedule constraints, there are many situations where a full DDD implementation cannot be adopted from the start. In my experience, lightweight DDD is a "starting point," not a "goal." By using the traction gained from tactical patterns as a foothold and gradually advancing to strategic patterns, you can avoid the "inferior domain models" that Vernon fears.
In this article, after organizing the pros and cons of lightweight DDD, I will share a concrete roadmap for gradually advancing from lightweight DDD to full DDD.
What is Lightweight DDD?
Definition
Lightweight DDD is an approach that adopts only the tactical patterns of DDD (Entities, Value Objects, Repositories, Domain Services, etc.). It omits strategic patterns (Bounded Contexts, Ubiquitous Language, Context Maps, etc.).
Eric Evans himself also emphasizes the importance of strategic patterns.
The really powerful domain models evolve over time, and even the most experienced modelers find that they gain their best ideas after the initial releases of a system.
— Eric Evans, Domain-Driven Design (2003)
Typical Form of Lightweight DDD
The configuration of lightweight DDD frequently seen in Go projects is as follows:
internal/
├── domain/
│ ├── model/ # Entities, Value Objects
│ └── repository/ # Repository interfaces
├── usecase/ # Use cases (Application Services)
├── handler/ # HTTP handlers
└── infrastructure/ # Repository implementation, external services
This structure itself is not bad. However, since Bounded Contexts are not consciously defined, dependencies between aggregates tend to become complex as the project grows.
Pros and Cons of Lightweight DDD
Pros: What You Can Gain from Tactical Patterns Alone
Even with lightweight DDD, you can fully achieve the following benefits:
1. Cohesion of Domain Logic within Aggregates
Business rules that would otherwise be scattered across use case or handler layers are aggregated within the domain layer. This alone significantly improves code readability and maintainability.
// ❌ State without even adopting lightweight DDD
// Business rules scattered in the handler
func (h *OrderHandler) Confirm(w http.ResponseWriter, r *http.Request) {
order := fetchOrder(r)
if order.Status != "draft" { // Business rule here
http.Error(w, "Cannot confirm", 400)
return
}
order.Status = "confirmed" // State change also here
saveOrder(order)
}
// ✅ State with lightweight DDD adopted
// Business rules aggregated in the aggregate
func (o *Order) Confirm() error {
if o.status != OrderStatusDraft {
return fmt.Errorf("Only orders in draft status can be confirmed (current: %s)", o.status)
}
o.status = OrderStatusConfirmed
return nil
}
2. Improved Testability
Because domain logic becomes pure Go code with no external dependencies, tests become significantly easier to write. You can test business rules without setting up databases or HTTP servers.
3. Protection of Invariants via Unexported Fields
By leveraging Go's unexported fields, you can prevent aggregate invariants from being compromised from the outside.
type Order struct {
id string // Unexported field
status OrderStatus // Cannot be changed directly
items []OrderItem
}
Cons: Risks of Lacking Strategic Patterns
1. Model Bloat due to Lack of Context Boundaries
As the project grows, a single domain model ends up handling responsibilities across all contexts. For example, the concept of an "Order" might be reused identically for sales, shipping, billing, and analytics.
// ❌ "God model" carrying responsibilities for every context
type Order struct {
id string
customerID string
status OrderStatus
items []OrderItem
shippingAddress Address // Interest of the shipping context
trackingNumber string // Interest of the delivery context
invoiceNumber string // Interest of the billing context
analyticsTag string // Interest of the analytics context
}
2. Absence of Ubiquitous Language
If development proceeds without a common language shared with domain experts, the names in the code will diverge from business terms. As a result, every time specifications change, there is a verification cost incurred to confirm where that logic corresponds within the code.
3. Lack of Deep Team Understanding of DDD
If you apply tactical patterns purely mechanically, you will not cultivate a fundamental understanding of "why" this design is used. As new members join the team, this leads to an increase in superficial pattern application.
Roadmap for Step-by-Step Adoption
This is the step-by-step roadmap I have practiced while involved in multiple projects with teams of 3 to 10 people. Each stage provides value independently, and you can leverage most of the achievements from the previous stage. However, you may occasionally need to review aggregate structures when separating contexts in Stage 3.
Stage 1: Adopting Tactical Patterns (Lightweight DDD)
Estimated Duration: In my team, this took about 1–2 sprints.
In the first stage, you introduce the basics of tactical patterns:
- Define Entities and Value Objects.
- Aggregate business rules into Aggregates.
- Abstract data access using the Repository pattern.
internal/
├── domain/
│ ├── model/ # Entities, Value Objects
│ └── repository/ # Repository interfaces
├── usecase/
├── handler/
└── infrastructure/
Goal of this stage: Domain logic is aggregated in the domain layer, and unit tests can be written.
Stage 2: Building Ubiquitous Language
Estimated Duration: In my team, this took about 2–4 sprints (and is updated continuously thereafter).
Collaborate with domain experts to create a glossary of terms for the Ubiquitous Language. Refactor the names in the code to align with the glossary.
## Order Domain Glossary
| Term | Definition | Code Name |
| --------------------- | ---------------------------------------------------- | ----------------- |
| Order | An aggregate representing the customer's purchase intent | `model.Order` |
| Confirm | Action to confirm an order and start inventory hold | `Order.Confirm()` |
| Ship | Action to dispatch products to the customer | `Order.Ship()` |
| OrderItem | Value object for products and quantities in an order | `model.OrderItem` |
Goal of this stage: Code naming matches business terms, and domain experts can read and review the code.
Stage 3: Identifying Bounded Contexts
Estimated Duration: In my team, this took about 1–2 sprints (analysis) + 2–4 sprints (separation).
Identify that multiple contexts exist within the domain and separate them.
internal/
├── sales/ # Sales context
│ ├── domain/
│ ├── usecase/
│ └── infrastructure/
├── shipping/ # Shipping context
│ ├── domain/
│ ├── usecase/
│ └── infrastructure/
└── billing/ # Billing context
├── domain/
├── usecase/
└── infrastructure/
Goal of this stage: Each context has its own independent domain model, and dependencies between contexts become explicit.
Stage 4: Introducing Context Maps and Event-Driven Architecture
Estimated Duration: In my team, this took about 2–4 sprints.
Organize the relationships between contexts and introduce coordination through domain events.
Goal of this stage: Coupling between contexts is severed by domain events, bringing each context closer to an independently deployable state.
The Rationality of Not Aiming for "Full DDD"
Not Every Project Needs DDD
DDD is most effective for what Eric Evans calls complex domains.
Domain-Driven Design is an approach to software development for complex needs by deeply connecting the implementation to an evolving model of the core business concepts.
— Eric Evans, DDD Reference
In projects like the following, lightweight DDD may be sufficient:
| Project Characteristic | Recommended Approach |
|---|---|
| CRUD-centric, few business rules | Simple layered architecture (DDD adoption is often unnecessary) |
| Complex domain, many business rules | Full DDD |
| Small team (2-3 people), short-term project | Lightweight DDD |
| Large team, long-term operation | Full DDD |
| Collaboration with domain experts possible | Full DDD |
| No domain expert available | Start with lightweight DDD and adopt step-by-step |
How to Mitigate the "Sins" of Lightweight DDD
Even if you do not migrate to full DDD, there are ways to mitigate the problems of lightweight DDD.
1. Use Domain Terms for Package Naming
Even without creating a full glossary, just using domain terms for package and file names improves code readability.
// ❌ Technical naming
package service
// ✅ Naming using domain terms
package pricing
2. Be Conscious of Aggregate Boundaries
Even without separating into Bounded Contexts, you can prevent model bloat by dividing packages by aggregate units.
internal/domain/
├── order/ # Order aggregate
├── customer/ # Customer aggregate
└── product/ # Product aggregate
3. Observe Dependency Directions
At least follow the dependency rules of Clean Architecture. By maintaining a structure where the domain layer does not depend on outer layers, future extensions become easier.
Decision Criteria Based on Team Size and Project Characteristics
Decision Flowchart
Success Conditions for Step-by-Step Adoption
Here are the success conditions I have found important through my experience:
1. Each stage provides value independently
Avoid a state where "there is no effect until Stage 4 is completed." Stage 1 alone provides clear value in improved testability.
2. The team's understanding is catching up
Mechanical application of patterns has limited effect. Discuss "why this pattern is used" at each stage within the team, and proceed only after reaching a consensus.
3. Time for refactoring is secured
Step-by-step adoption involves refactoring existing code. Do not try to "introduce DDD while developing new features," but explicitly secure time for refactoring.
4. Do not aim for perfection
DDD is a process of continuous learning and improvement. It is important not to try to create a perfect model from the start, but to adopt a mindset of making improvements through repeated iterations.
Conclusion
Lightweight DDD is not an "anti-pattern" but a waypoint toward full DDD.
- Tactical patterns alone are valuable. You can achieve domain logic aggregation, improved testability, and invariant protection through lightweight DDD.
- There are risks in the absence of strategic patterns. It is only a matter of time before model bloat, lack of ubiquitous language, and insufficient team understanding become apparent.
- With a roadmap for step-by-step adoption, you can steadily move toward full DDD even if you start with lightweight DDD.
- Not every project requires full DDD. Make decisions based on the complexity of the domain, team size, and the presence or absence of domain experts.
- That each stage provides independent value is the key success condition for step-by-step adoption.
Instead of a binary choice between "full DDD or nothing," selecting the optimal level of adoption according to the project's situation is a realistic and wise approach.
References
| Content | Source |
|---|---|
| Original DDD Text | Eric Evans, Domain-Driven Design: Tackling Complexity in the Heart of Software (2003) |
| Warning against lightweight DDD | Vaughn Vernon, Implementing Domain-Driven Design (2013) |
| DDD Reference | Eric Evans, DDD Reference |
| Practical application of tactical patterns | Jimmy Bogard, Domain-Driven Design: The Good Parts (NDC Sydney 2016) |
| Concept of step-by-step adoption | Scott Millett & Nick Tune, Patterns, Principles, and Practices of Domain-Driven Design (2015) |
| Clean Architecture | Robert C. Martin, Clean Architecture (2017) |
Discussion