Skip to content
ioob.dev
Go back

DDD Part 1 — Why Domain-Centric Design Matters

· 9 min read
DDD Series (1/7)
  1. DDD Part 1 — Why Domain-Centric Design Matters
  2. DDD Part 2 — Ubiquitous Language and Bounded Context
  3. DDD Part 3 — Context Mapping
  4. DDD Part 4 — Entity and Value Object
  5. DDD Part 5 — Aggregate and Repository
  6. DDD Part 6 — Domain Service and Application Service
  7. DDD Part 7 — Domain Event and Anti-Corruption Layer
Table of contents

Table of contents

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.

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.

Tactical Design deals with the internals of a bounded context. These are the building blocks for translating the domain model into actual code.

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:

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.

PartTitleKey Topics
Part 1What Is DDDWhy domain-centric design matters, overview of strategic and tactical design
Part 2Ubiquitous Language and Bounded ContextShared language, context boundaries
Part 3Context MappingPatterns for defining relationships between contexts
Part 4Entity and Value ObjectIdentity and equality
Part 5Aggregate and RepositoryConsistency boundaries and persistence
Part 6Domain Service, Application ServiceWhere to place logic
Part 7Domain Event and Anti-Corruption LayerLoose 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.


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.

-> Part 2: Ubiquitous Language and Bounded Context


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Software Architecture Part 6 — Modular Monolith: What You Can Try Before Microservices
Next Post
DDD Part 2 — Ubiquitous Language and Bounded Context