Skip to content
DevPedia

Domain and structureGuide 3 of 11

Domain-Driven Design (DDD)

How to model the domain so the software reflects the business, and when this approach is worth the investment.

Updated 9 min read

Where architectural drivers answer:

What does the system need, and what constrains it?

Domain-Driven Design answers:

How do we understand and correctly model the business problem?

DDD puts the domain at the center of the design: its concepts, its rules, and its language.

DDD has two broad dimensions: strategic design and tactical design.

Strategic design

Strategic design tries to answer:

  • which parts of the business exist?
  • how do they relate to each other?
  • where are the boundaries?
  • what language is used?
  • which parts are strategically important?

Domain and subdomains

Picture an e-commerce platform:

Each of these areas can represent a different subdomain, and they don’t all necessarily carry the same importance.

Core Domain

The part where there’s a real advantage or differentiation. Payments would be core if the platform has payment processing capabilities that amount to a competitive advantage.

Supporting Domain

Necessary for the business, but with no differentiating value. Inventory is a typical example.

Generic Domain

Common functionality, like Authentication.

This classification helps decide where to invest in your own design and where to lean on existing solutions.

Bounded Context

One of the most important concepts in DDD. A Bounded Context defines a boundary within which a model and its language have a specific meaning.

The term Customer can exist in all three contexts without representing the same model in each one.

Bounded Context ≠ microservice

This distinction is fundamental:

ConceptWhat it represents
Bounded ContextA conceptual, domain boundary
MicroserviceA deployment / runtime boundary

A Bounded Context can be implemented as a module, as a boundary inside a monolith, or as a microservice. That’s why DDD doesn’t imply microservices.

Ubiquitous language

The ubiquitous language is the one the team and the domain experts should share. In Payments:

Authorize
Capture
Refund
Payment
Transaction
Provider
Settlement

That terminology should show up in the software too:

payment.authorize()
payment.capture()
payment.refund()

instead of hiding important concepts behind generic names like payment.process(), payment.update(), or payment.handle() when those methods span several business rules.

Context Map

Bounded Contexts don’t exist in isolation. We can represent their relationships:

DDD has patterns for describing these relationships, such as customer/supplier, conformist, anti-corruption layer, shared kernel, open host service, and published language.

An anti-corruption layer, for instance, protects a context’s internal model from concepts coming out of another system:

It’s particularly useful when integrating legacy or third-party systems whose models don’t line up well with yours.

Tactical design

Strategic design defines the boundaries. Tactical design works on the model inside them, with building blocks such as:

  • Entity
  • Value object
  • Aggregate
  • Aggregate root
  • Domain service
  • Domain event
  • Repository
  • Factory

Entity

Has a persistent identity:

Payment
PaymentId = 123

Two Payments can hold the same values and still be different entities if their identities differ.

Value object

Defined by its values:

Money(100, ARS)
Currency(ARS)
Email(...)
Address(...)

A Money(100, ARS) doesn’t need a MoneyId.

Aggregate

An aggregate is a boundary within which a set of invariants is protected.

The aggregate root is the entry point:

payment.authorize()
payment.capture()
payment.refund()

We don’t want arbitrary code to be able to set payment.status = CAPTURED, because that could skip domain rules.

Protecting those invariants inside the aggregate is a design problem with a known solution: when what varies by state is the set of allowed operations — a shipped order can no longer be canceled — the State pattern keeps that rule from being scattered across conditionals all over the code.

Aggregate design

One important idea is not to build giant aggregates. An aggregate should contain what needs to stay transactionally consistent. It can be tempting to model everything as a single aggregate:

But that would produce very large transactions, lock contention, high coupling, and worse scalability: a shipping change would also lock payments and inventory. DDD is exactly what helps you ask:

What actually needs to change together?

Domain service

Some domain logic doesn’t naturally belong to a single entity. For example, a FraudDecisionService that combines several sources:

That doesn’t mean every service in an application should be a domain service. A PaymentService with twenty responsibilities is usually a sign that the domain model isn’t expressing the rules clearly enough.

Domain events

A domain event states a fact — PaymentAuthorized, PaymentCaptured, PaymentRefunded — not a command like CapturePayment.

Domain events connect DDD to event-driven communication patterns, but on their own they don’t mean we need Kafka or microservices.

Repository

A repository abstracts the persistence of an aggregate. PaymentRepository could be implemented as PostgresPaymentRepository: the domain depends on the abstraction, not on PostgreSQL. This idea connects directly to Hexagonal, Onion, and Clean Architecture.

Factory

A factory can encapsulate a complex creation, like Payment.create(...), when creating a Payment means validating multiple conditions or guaranteeing invariants. For trivial objects like new Money(100, ARS), introducing a factory may be unnecessary.

A combined example

When DDD is especially useful

  • complex domains;
  • lots of business rules;
  • ambiguous terminology;
  • large teams;
  • multiple subdomains;
  • systems that will evolve over years.

When you don’t need all of DDD

For a very simple CRUD (Users, Products, Categories), creating aggregates, domain services, factories, and context maps for every operation can bring more ceremony than value. DDD is above all a tool for dealing with domain complexity, not a checklist.

Tools and technologies

DDD doesn’t require any specific technology. It can be implemented with Java + Spring, C# + .NET, Kotlin, TypeScript, Go, or Python. What matters is the model, not the framework.

References

  • Eric Evans laid the foundations of Domain-Driven Design in Domain-Driven Design: Tackling Complexity in the Heart of Software.
  • The Microsoft Azure Architecture Center covers how to use Bounded Contexts and Domain-Driven Design principles to define microservice boundaries.

Share this guide

Search by concept, pattern or practice.