Skip to main content

The Basic Concepts of DDD — Domain Objects, Aggregates, Repositories, Ubiquitous Language

What is domain-driven design?

Domain-Driven Design (DDD) is a software design approach centered on business logic.

DDD starts design from the "business problem," whereas the traditional approach starts from "technical constraints."

Definitions of terms

Let's define the important terms for learning DDD.

Basic terms

TermDefinitionExample
DomainThe business area that software tries to solve"Order" and "inventory management" in an e-commerce site
Domain objectA general term for objects that express the domainTwo kinds: entities and value objects
Domain modelThe overall design made up of domain objects and aggregatesThe overall picture of the structure that expresses the domain
Business ruleA constraint or rule in the domain"Products cannot be added after confirmation"

The two kinds of domain objects

KindIdentifierBasis for identityExample
EntityYesJudged by identifierOrder, User, Product
Value objectNoJudged by all attribute valuesMoney, Address, Email

A more detailed comparison of domain objects

Let's look more closely at the differences between value objects, entities, and aggregates.

AspectValue objectEntityAggregate
DefinitionA concept expressed by a combination of attributesA concept with a unique identifierA collection of related domain objects
IdentifierNoneYes (ID)The aggregate root has an ID
MutabilityImmutableMutableChangeable via the aggregate root
Identity judgmentAll attribute valuesIdentifier (ID)The aggregate root's ID
LifecycleDepends on the parent objectExists independentlyFollows the aggregate root's lifecycle
Table representationEmbedded or a value-object tableA dedicated tableMultiple tables (within the aggregate boundary)

Understanding with concrete examples

// Value object: money
class Money
{
private int $amount;
private string $currency;

public function __construct(int $amount, string $currency)
{
$this->amount = $amount;
$this->currency = $currency;
}

// Immutable: the result of a calculation returns a new instance
public function add(Money $other): Money
{
return new Money($this->amount + $other->amount, $this->currency);
}

// Identity: judged by all attribute values
public function equals(Money $other): bool
{
return $this->amount === $other->amount
&& $this->currency === $other->currency;
}
}

// Entity: an order
class Order
{
private OrderId $id; // Identifier
private OrderStatus $status;
private Money $total;

// Mutable: the state changes
public function confirm(): void
{
$this->status = OrderStatus::CONFIRMED;
}

// Identity: judged by ID
public function equals(Order $other): bool
{
return $this->id->equals($other->id);
}
}

// Aggregate: the Order aggregate (groups Order and OrderLine)
class Order // Aggregate root
{
private OrderId $id;
private array $orderLines; // Entities within the aggregate

// Accessed only via the aggregate root
public function addItem(ProductId $productId, int $quantity): void
{
$line = new OrderLine($this->id, $productId, $quantity);
$this->orderLines[] = $line;
}
}

Criteria for choosing between them

Examples of judgment:

"Money"
Is an identifier needed? → NO
→ Value object

"Order"
Is an identifier needed? → YES
A relation to other objects? → YES (related to order lines)
Consistency boundary? → An order and its order lines are managed together
→ The aggregate root of the Order aggregate

"Order line"
Is an identifier needed? → YES
Exists independently? → NO (depends on the order)
→ An entity within the Order aggregate

Aggregate

An aggregate is not a domain object; it is a structure (boundary) that groups domain objects.

Rules of an aggregate

  1. Access from outside only via the aggregate root
  2. The aggregate root guarantees consistency
  3. Update one aggregate per transaction is recommended (handle updates across multiple aggregates with eventual consistency)
// Good: access via the aggregate root (Order)
$order = $orderRepository->findById($orderId);
$order->addItem($productId, $quantity);

// Bad: accessing OrderLine directly
$orderLine = $orderLineRepository->findById($lineId); // This is bad

Repository

A repository is an object responsible for persisting and restoring an aggregate.

The core ideas of DDD

The ubiquitous language

The ubiquitous language is the language used in common across the entire team. When everyone involved in the project—developers, domain experts (business stakeholders), product managers, and so on—communicates using the same words, you prevent misunderstandings.

Why the ubiquitous language matters

In the traditional approach, business stakeholders and developers use different words, which causes the following problems.

❌ Without a ubiquitous language
────────────────────────────

Business stakeholder: "Please confirm the order"
↓ (translation)
Developer: "I'll update the status column of the Order table to 1"

# Problems:
# - A "translation" is needed between the business and the code
# - The concept of "confirm" disappears from the code
# - It is easy for perceptions to diverge when requirements change
✓ With a ubiquitous language
───────────────────────────

Business stakeholder: "Please confirm the order"
↓ (as-is)
Developer: "I'll call Order.confirm()"

# Benefits:
# - The business and the code are expressed in the same words
# - You can understand the business flow by reading the code
# - The whole team shares the same understanding

A practical example of the ubiquitous language

Business language Code

"Confirm an order" → Order.confirm()
"Cancel an order" → Order.cancel()
"Add a product" → Order.addItem()
"Calculate the shipping fee" → Order.calculateShippingFee()

# The business language and the code match
note

The ubiquitous language is not a "business glossary." It is a living language that everyone on the team uses every day. It is important to keep using the same words across meetings, code, and documentation.

The bounded context

A bounded context is the range within which a particular domain model is valid. You logically separate a large system and keep a consistent model within each context.

Why a bounded context is needed

The same word can mean different things depending on the context. The bounded context solves this problem.

The meaning of the word "product" differs by context
────────────────────────────────────

Order context:
Product = the purchase target included in an order line
- Product ID
- Product name
- Price
- Quantity

Inventory context:
Product = stock managed in a warehouse
- Product ID
- Stock quantity
- Warehouse location
- Expected arrival date

Catalog context:
Product = product information shown to the customer
- Product ID
- Product description
- Images
- Category

# Even for the same "product," the information each context should hold differs

For beginners: how to think about a bounded context

A bounded context is like "the scope of work for each department."

A real-world example
────────────

Sales department:
"Customer" = a business partner that generates revenue
→ contract amount, transaction history, contact-person information

Support department:
"Customer" = the party you provide support to
→ inquiry history, product information, status of responses

# Even for the same word "customer," each department's concerns differ

An example of separating contexts

Each context has its own independent domain model.

note

In a small system, it is fine to handle everything in a single context. You only need to consider separating contexts once the system becomes complex.

From the next chapter, we look at each concept in detail. Let's start with value objects.

Further reading

If you want to learn more about the basic concepts of DDD, see the following resources.

Books

  • Eric Evans, "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2003) - the original text of DDD, with detailed explanations of every concept
  • Vaughn Vernon, "Implementing Domain-Driven Design" - a practical DDD book rich in implementation examples for value objects and entities

Online resources

note

The concepts of DDD may feel difficult at first, but your understanding deepens as you actually write code. Let's get used to them gradually through the implementation examples in the following chapters.