Table of contents
- Technology Wasn’t the Problem
- Technology-Centric Thinking vs Domain-Centric Thinking
- What Is a Domain
- The Core Premise of DDD
- Strategic Design and Tactical Design
- Problems That DDD Solves
- Is DDD Necessary for Every Project
- Series Outline
- Key Takeaways
Technology Wasn’t the Problem
When you look back at why projects fail, it’s rarely because of a wrong technology choice. Choosing Express when you should have used Spring Boot, or picking MySQL instead of PostgreSQL — projects almost never collapse for those reasons.
The real problems emerge elsewhere. Three months in, you realize that what the product manager calls an order and what the developer modeled in the Order class are entirely different concepts. Payment domain rules seep into shipping code, so changing a payment policy breaks shipping logic. The code runs, but it does nothing to explain the business.
Eric Evans identified the heart of this problem in his 2003 book Domain-Driven Design: Tackling Complexity in the Heart of Software. Software complexity comes from the domain, not the technology. A domain is the business problem space that the software aims to solve. For e-commerce, the domains are orders, payments, shipping, and inventory; for a hospital system, they are medical consultations, prescriptions, and billing.
DDD (Domain-Driven Design) is an approach that places this domain at the center of design. The key idea is that business rules and concepts — not frameworks or database schemas — should determine the structure of the code.
Technology-Centric Thinking vs Domain-Centric Thinking
What’s the first thing that comes to mind when starting development? Let's design the DB tables first, Let's define the REST API first, Shouldn't this be MSA? — technical concerns come first. It’s natural. You’re a developer, after all.
But this ordering creates problems. Let’s compare two approaches.
In technology-centric thinking, you start by creating the orders table.
// Technology-centric: table structure becomes class structure
public class Order {
private Long id;
private Long userId;
private String status; // "PENDING", "PAID", "SHIPPED"
private BigDecimal totalAmount;
private LocalDateTime createdAt;
// dozens of getters and setters...
}
This class is a mirror of the table. status is a string, and the rules for transitioning from PENDING to PAID live somewhere in OrderService. What state transitions the order allows, what happens when the payment amount is zero — there’s no trace of business rules in this class.
Domain-centric thinking starts differently. It begins by asking: “What is an order?”
// Domain-centric: business concepts determine code structure
class Order private constructor(
val id: OrderId,
val customer: CustomerId,
private var status: OrderStatus,
private val lines: MutableList<OrderLine>,
) {
fun place() {
require(lines.isNotEmpty()) { "Cannot place an order with no items" }
require(status == OrderStatus.DRAFT) { "Can only place an order from draft status" }
status = OrderStatus.PLACED
}
fun totalAmount(): Money =
lines.fold(Money.ZERO) { acc, line -> acc + line.subtotal() }
}
See the difference? Business rules live inside the place() method. The code itself tells you that an empty order cannot be placed and that only draft orders can be confirmed. status can’t be swapped out externally as a string, and a Money value object handles monetary calculations.
This is the kind of code DDD strives for. Read the code, and you see the business. When business rules change, you modify the domain objects. Technical infrastructure and business logic don’t get tangled together.
What Is a Domain
Let’s pin down the word domain more precisely. In DDD, a domain is the problem space that the software aims to solve. It’s a business concept, not a technical one.
If you’re building an e-commerce platform, the overall domain is online shopping. Within it lie several subdomains.
- Core Domain: The area where the business gains its competitive advantage. Recommendation algorithms, pricing policies, etc.
- Supporting Subdomain: Not core, but needs to be built in-house. Inventory management, shipment tracking, etc.
- Generic Subdomain: Areas that are similar everywhere. Authentication, payment gateway integration, etc.
The reason the core domain matters is simple. If you don’t focus here, the business won’t differentiate itself. You can use an OAuth library for authentication and plug in a payment processor’s SDK for payments. But nobody else will build the core domain for you.
DDD says to invest your best design efforts in this core domain. You don’t need to apply the same level of design to every subdomain. Use existing solutions for generic subdomains and concentrate your energy on the core domain — that’s the strategically sound approach.
The Core Premise of DDD
Every practice in DDD stems from a single premise.
Software complexity comes from the domain, not the technology.
Once you accept this premise, several conclusions naturally follow.
First, collaboration with domain experts is essential. This isn’t complexity that a developer can solve alone by writing code. You need to continuously converse with people who understand the business to evolve the model. The toss me the spec and I'll code it approach makes it hard to grasp the true structure of the domain.
Second, the code must reflect the domain. If the spec says cancel order but the code reads updateStatus("CANCELLED"), a gap forms between the two. The wider this gap grows, the more you end up in a state where reading the code tells you nothing about the business. DDD insists that class names, method names, and variable names in the code should use domain terminology directly.
Third, the model evolves. You can’t create a perfect domain model from the start. The model becomes increasingly refined as you understand the business, write code, and talk with domain experts. In DDD, modeling is not a one-time task but an ongoing activity.
Strategic Design and Tactical Design
DDD addresses two levels of design: Strategic Design and Tactical Design.
flowchart TB
DDD["DDD<br/>Domain-Driven Design"]
SD["Strategic Design"]
TD["Tactical Design"]
DDD --> SD
DDD --> TD
UL["Ubiquitous Language"]
BC["Bounded Context"]
CM["Context Mapping"]
SUB["Subdomain"]
SD --> UL
SD --> BC
SD --> CM
SD --> SUB
ENT["Entity"]
VO["Value Object"]
AGG["Aggregate"]
REPO["Repository"]
DS["Domain Service"]
DE["Domain Event"]
FAC["Factory"]
TD --> ENT
TD --> VO
TD --> AGG
TD --> REPO
TD --> DS
TD --> DE
TD --> FAC
Strategic Design deals with the big picture. How to partition the system into boundaries, what language to use within each boundary, and how to define the relationships between boundaries. It’s a macro-level design that even influences team structure and organizational communication.
- Ubiquitous Language: Developers and domain experts communicate using a single, shared language
- Bounded Context: Defines the boundary within which a single ubiquitous language is valid
- Context Mapping: Makes the relationships between bounded contexts explicit
- Subdomain: Classifies the overall domain into core, supporting, and generic
Tactical Design deals with the internals of a bounded context. These are the building blocks for translating the domain model into actual code.
- Entity: An object distinguished by its identifier
- Value Object: An immutable object compared solely by its values
- Aggregate: A cluster of objects that guarantees consistency
- Repository: Handles persistence and retrieval of aggregates
- Domain Service: Domain logic that doesn’t belong to a specific entity
- Domain Event: Represents something that happened in the domain
- Factory: Encapsulates complex object creation
If you apply only tactical design without strategic design, you end up using DDD patterns but with a structure that’s still a mess. You’ve created entities and value objects, but without bounded context boundaries, everything gets jumbled into one massive model. Conversely, if you only do strategic design and ignore tactical design, the diagrams look pretty but the code remains procedural.
The two mesh like gears. Strategic design sets the boundaries, and tactical design creates rich domain models within those boundaries.
Problems That DDD Solves
What concretely changes when you apply DDD? Let’s look at a few common problem scenarios.
“I can read the code but can’t understand the business.” With technology-centric design, you have classes like UserService, OrderRepository, and PaymentController, but it’s hard to answer questions like “Where’s the discount policy?” or “Where are the order state transition rules managed?” When the domain model is explicit, business rules live in the code.
“I changed A and B broke.” Without bounded context boundaries, where everything lives in a single model, a payment logic change ripples into shipping logic. When you separate contexts, each area can be changed independently.
Developers and product managers speak different languages. Without a ubiquitous language, you have to constantly verify whether order confirmation in the spec and confirmOrder() in the code refer to the same thing. Using a common language eliminates this translation cost.
“The service class is thousands of lines long.” When domain logic piles up in the service layer, you get what’s called an Anemic Domain Model. Entities are just data bags with getters and setters, and all logic lives in service classes. DDD places domain logic inside domain objects, making services thin and domain models rich.
Is DDD Necessary for Every Project
No. DDD is an approach effective for complex domains.
For admin systems that are mostly CRUD, data pipelines with barely any business rules, or projects that need rapid prototype validation, it can be over-engineering. Creating an Entity, Value Object, Aggregate, and Repository for a single order isn’t always better than slapping a CRUD API on a single table.
DDD shines under these conditions:
- Business rules are complex and change frequently
- Domain experts exist and are willing to collaborate
- The system must be maintained long-term
- Multiple teams develop parts of a single system
If these conditions don’t apply, it’s perfectly fine to lightly reference DDD’s strategic design concepts and selectively apply tactical patterns only where needed.
Series Outline
This series consists of 7 parts, progressively covering DDD from strategic design to tactical design.
| Part | Title | Key Topics |
|---|---|---|
| Part 1 | What Is DDD | Why domain-centric design matters, overview of strategic and tactical design |
| Part 2 | Ubiquitous Language and Bounded Context | Shared language, context boundaries |
| Part 3 | Context Mapping | Patterns for defining relationships between contexts |
| Part 4 | Entity and Value Object | Identity and equality |
| Part 5 | Aggregate and Repository | Consistency boundaries and persistence |
| Part 6 | Domain Service, Application Service | Where to place logic |
| Part 7 | Domain Event and Anti-Corruption Layer | Loose coupling between domains |
Parts 1-3 cover strategic design, and Parts 4-7 cover tactical design. The reason for covering strategic design first is simple. If you apply tactical patterns without boundaries, no matter how well you craft your entities and value objects, they’ll eventually be buried in one Big Ball of Mud. It’s natural to establish the big picture first and then fill in the details.
Code examples use both Java and Kotlin. Java is the most widely used language in the DDD community, and Kotlin’s data class and immutable design pair well with Value Objects, making it well-suited for expressing DDD patterns.
Key Takeaways
Here’s a summary of what this article covered.
- DDD starts from the premise that software complexity comes from the domain, not the technology
- A domain is the business problem space that the software aims to solve
- Technology-centric design lets table structure determine code structure, while domain-centric design lets business concepts determine code structure
- Strategic design (ubiquitous language, bounded context, context mapping) establishes the big picture, and tactical design (Entity, VO, Aggregate, etc.) fills in the details
- DDD is effective for complex domains and is not necessary for every project
The next article covers ubiquitous language and bounded contexts. We’ll explore the problem of developers and domain experts using the same words but thinking of different things, and how to solve that problem with boundaries.




Loading comments...