Domain-Driven Design (DDD)

How to apply DDD to model complex domains, define bounded contexts, and build software aligned with the business.

What problem it solves

In complex systems, the biggest challenge is not technical but communicational. Developers speak one language, business experts speak another, and the software ends up being a flawed translation of both. The result: data models that don’t reflect business reality, logic scattered across the wrong layers, and changes that require modifying half the system.

Domain-Driven Design attacks this problem at the root: it proposes that software should directly model the business domain, using the same language that domain experts use. It is not a framework or a library β€” it is a design approach that puts the domain at the center of every architectural decision.

Symptoms that you need DDD

  • The technical team and the business team don’t understand each other
  • Data models are database tables disguised as objects
  • A change in a business rule requires modifying multiple services
  • There are no clear boundaries between the responsibilities of each part of the system
  • Business logic is scattered across controllers, services, and repositories
  • The names in the code don’t match the terms the business uses

A concrete example of the problem

Imagine an e-commerce system where the business team talks about β€œorders,” β€œreturns,” and β€œrefunds,” but the code has classes like DataProcessor, ItemHandler, and TransactionManager. When the business says β€œwe need to change the returns policy,” nobody knows which files to touch because there is no Return class or a Refunds module.

❌ Sin DDD β€” CΓ³digo desconectado del negocio:
   src/
   β”œβ”€β”€ handlers/
   β”‚   β”œβ”€β”€ DataProcessor.java
   β”‚   β”œβ”€β”€ ItemHandler.java
   β”‚   └── TransactionManager.java
   β”œβ”€β”€ models/
   β”‚   β”œβ”€β”€ Record.java
   β”‚   └── Entry.java
   └── utils/
       └── Helper.java

βœ… Con DDD β€” CΓ³digo alineado con el dominio:
   src/
   β”œβ”€β”€ pedidos/
   β”‚   β”œβ”€β”€ Pedido.java
   β”‚   β”œβ”€β”€ LΓ­neaDePedido.java
   β”‚   └── RepositorioPedidos.java
   β”œβ”€β”€ devoluciones/
   β”‚   β”œβ”€β”€ DevoluciΓ³n.java
   β”‚   β”œβ”€β”€ PolΓ­ticaDevoluciΓ³n.java
   β”‚   └── ServicioReembolso.java
   └── envΓ­os/
       β”œβ”€β”€ EnvΓ­o.java
       └── TransportistaAdapter.java

How it works

DDD is divided into two major areas: strategic design (how to divide the system) and tactical design (how to model each part).

Strategic design

Ubiquitous Language

This is the shared vocabulary between developers and domain experts. If the business talks about β€œorders,” β€œshipments,” and β€œreturns,” the code must use exactly those terms β€” not β€œorders_table,” β€œshipping_record,” or β€œreturn_entity.”

❌ Código sin lenguaje ubicuo:
   class DataRecord { processItem() }

βœ… CΓ³digo con lenguaje ubicuo:
   class Pedido { confirmarEnvΓ­o() }

The ubiquitous language is documented, maintained, and used in conversations, code, tests, and documentation. It is not just a naming convention β€” it is an active agreement between business and development that evolves with the domain.

How to build the ubiquitous language:

  1. Run Event Storming sessions with domain experts and developers
  2. Identify the key terms the business uses and document them in a shared glossary
  3. Use those exact terms in the code, the tests, the documentation, and conversations
  4. When ambiguity arises (the same word means different things), it’s a sign that you need separate bounded contexts

Bounded Contexts

A bounded context is an explicit boundary within which a domain model has consistent meaning. The same word can mean different things in different contexts:

  • β€œCustomer” in the Sales context: someone who buys products
  • β€œCustomer” in the Support context: someone who opens tickets
  • β€œCustomer” in the Billing context: an entity with tax data
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Contexto: Ventas   β”‚  β”‚  Contexto: Soporte  β”‚
β”‚                     β”‚  β”‚                     β”‚
β”‚  Cliente            β”‚  β”‚  Cliente            β”‚
β”‚  β”œβ”€ nombre          β”‚  β”‚  β”œβ”€ nombre          β”‚
β”‚  β”œβ”€ historialCompra β”‚  β”‚  β”œβ”€ ticketsAbiertos β”‚
β”‚  └─ descuento       β”‚  β”‚  └─ nivelPrioridad  β”‚
β”‚                     β”‚  β”‚                     β”‚
β”‚  Pedido             β”‚  β”‚  Ticket             β”‚
β”‚  β”œβ”€ lΓ­neas          β”‚  β”‚  β”œβ”€ descripciΓ³n     β”‚
β”‚  └─ total           β”‚  β”‚  └─ estado          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Each bounded context has its own model, its own database, and its own team (ideally). Communication between contexts happens through well-defined interfaces.

Practical rules for identifying bounded contexts:

  • If two teams need different models of the same concept β†’ separate contexts
  • If a change in one area shouldn’t affect another β†’ separate contexts
  • If the ubiquitous language changes β†’ you’re crossing a context boundary
  • A bounded context isn’t necessarily a microservice, but it’s a natural candidate to become one

Context Mapping

Defines how bounded contexts relate to each other:

RelationshipDescriptionExample
Shared KernelTwo contexts share a subset of the modelCurrency types shared between Sales and Billing
Customer-SupplierOne context provides data that another consumesInventory (supplier) provides stock to Orders (customer)
ConformistA context adapts to another’s model without negotiationYour system adapts to the model of an external provider’s API
Anti-Corruption LayerA context translates the external model into its own languageAn adapter that translates responses from a legacy system
Open Host ServiceA context exposes an open protocol for others to consumePublic REST API with OpenAPI documentation
Published LanguageA documented shared language is used (JSON Schema, Protobuf)Event contracts in Avro format

Tactical design

Entities

Objects with a unique identity that persists over time. Two entities with the same attributes but a different ID are different.

Pedido #1234 β‰  Pedido #5678
(aunque ambos tengan los mismos productos y el mismo total)

Entities encapsulate behavior, not just data. An Order is not a DTO with getters and setters β€” it has methods that reflect domain operations:

// ❌ Modelo anémico (anti-patrón)
pedido.setEstado("confirmado");
pedido.setFechaConfirmacion(new Date());

// βœ… Modelo rico con comportamiento
pedido.confirmar();  // internamente valida reglas y cambia estado

Value Objects

Objects defined by their attributes, with no identity of their own. Two value objects with the same attributes are equal. They are immutable.

DirecciΓ³n("Calle 1", "Ciudad A") == DirecciΓ³n("Calle 1", "Ciudad A")
Dinero(100, "USD") == Dinero(100, "USD")

Value objects are more common than they seem. Many concepts we model as strings or primitive numbers should be value objects:

// ❌ Tipos primitivos
String email = "user@example.com";
double precio = 99.99;

// βœ… Value objects con validaciΓ³n
Email email = new Email("user@example.com");  // valida formato
Dinero precio = new Dinero(99.99, "USD");      // evita mezclar monedas

Aggregates

A cluster of entities and value objects that are treated as a unit of consistency. Each aggregate has a root (Aggregate Root) that is the only external point of access.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Aggregate: Pedido          β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Pedido (Aggregate Root)β”‚  β”‚
β”‚  β”‚  β”œβ”€ id                β”‚  β”‚
β”‚  β”‚  β”œβ”€ estado            β”‚  β”‚
β”‚  β”‚  └─ fechaCreaciΓ³n     β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ LΓ­neaPed β”‚ β”‚ LΓ­neaPed β”‚  β”‚
β”‚  β”‚ producto β”‚ β”‚ producto β”‚  β”‚
β”‚  β”‚ cantidad β”‚ β”‚ cantidad β”‚  β”‚
β”‚  β”‚ precio   β”‚ β”‚ precio   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”‚
β”‚  β”‚ DirecciΓ³nEnvΓ­o (VO)  β”‚   β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key rules for aggregates:

  • Transactions should only modify one aggregate at a time
  • References between aggregates are made by ID, not by direct reference
  • If you need to modify several aggregates, use Domain Events
  • Keep aggregates small β€” a large aggregate is a sign of incorrect boundaries

Domain Events

They represent something that happened in the domain. They are expressed in the past tense and in business language.

PedidoConfirmado { pedidoId, fecha, total }
PagoRecibido { pagoId, pedidoId, monto }
EnvΓ­oDespachado { envΓ­oId, pedidoId, transportista }

Events allow bounded contexts to communicate in a decoupled way. They are the foundation of Event-Driven Architecture and of the Saga Pattern for coordinating distributed transactions.

Example of an event-driven flow:

Contexto Ventas:
  pedido.confirmar()
  β†’ publica PedidoConfirmado

Contexto Inventario:
  escucha PedidoConfirmado
  β†’ reserva stock
  β†’ publica StockReservado

Contexto EnvΓ­os:
  escucha StockReservado
  β†’ programa envΓ­o
  β†’ publica EnvΓ­oProgramado

Repositories

Abstractions that encapsulate data access. The domain doesn’t know whether the data comes from a database, an API, or a file. The repository speaks the language of the domain.

❌ pedidoDAO.selectByStatusAndDate(status, date)
βœ… repositorioPedidos.buscarPendientesDesde(fecha)

Domain Services

Operations that don’t naturally belong to any entity or value object. They contain business logic that involves multiple aggregates.

ServicioDePrecios.calcularDescuento(cliente, pedido, promociΓ³n)
ServicioDeEnvΓ­o.seleccionarTransportista(pedido, destino)

Practical example: E-commerce with DDD

Let’s see how DDD is applied to a real e-commerce system:

Step 1: Identify bounded contexts

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚    CatΓ‘logo      β”‚  β”‚    Pedidos       β”‚  β”‚    Pagos         β”‚
β”‚                  β”‚  β”‚                  β”‚  β”‚                  β”‚
β”‚  Producto        β”‚  β”‚  Pedido          β”‚  β”‚  TransacciΓ³n     β”‚
β”‚  CategorΓ­a       β”‚  β”‚  LΓ­neaDePedido   β”‚  β”‚  MΓ©todoDePago    β”‚
β”‚  Precio          β”‚  β”‚  DirecciΓ³nEnvΓ­o  β”‚  β”‚  Reembolso       β”‚
β”‚  Inventario      β”‚  β”‚  EstadoPedido    β”‚  β”‚  EstadoPago      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         β”‚                    β”‚                    β”‚
         β”‚    Eventos         β”‚    Eventos         β”‚
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 2: Define aggregates in the Orders context

Aggregate Root: Pedido
β”œβ”€β”€ id: PedidoId (Value Object)
β”œβ”€β”€ cliente: ClienteId (referencia por ID, no por objeto)
β”œβ”€β”€ lΓ­neas: List<LΓ­neaDePedido>
β”‚   β”œβ”€β”€ productoId: ProductoId
β”‚   β”œβ”€β”€ cantidad: Cantidad (Value Object, > 0)
β”‚   └── precioUnitario: Dinero (Value Object)
β”œβ”€β”€ direcciΓ³n: DirecciΓ³nEnvΓ­o (Value Object)
β”œβ”€β”€ estado: EstadoPedido (enum)
└── total(): Dinero (calculado)

Invariantes del agregado:
- Un pedido debe tener al menos una lΓ­nea
- El total nunca puede ser negativo
- Solo se puede confirmar un pedido en estado "borrador"
- Solo se puede cancelar un pedido que no ha sido enviado

Step 3: Implement behavior in the aggregate

class Pedido {
  confirmar() {
    if (this.estado !== 'borrador')
      throw new Error('Solo se puede confirmar un pedido en borrador');
    if (this.lΓ­neas.length === 0)
      throw new Error('El pedido debe tener al menos una lΓ­nea');

    this.estado = 'confirmado';
    this.fechaConfirmaciΓ³n = new Date();
    this.registrarEvento(new PedidoConfirmado(this.id, this.total()));
  }

  cancelar(motivo) {
    if (this.estado === 'enviado' || this.estado === 'entregado')
      throw new Error('No se puede cancelar un pedido ya enviado');

    this.estado = 'cancelado';
    this.motivoCancelaciΓ³n = motivo;
    this.registrarEvento(new PedidoCancelado(this.id, motivo));
  }
}

Advantages

  • Business-technology alignment: The code directly reflects the domain, reducing the communication gap
  • Clear boundaries: Bounded contexts define explicit boundaries that make independent evolution easier
  • Rich model: Entities contain behavior, not just data, avoiding anemic models
  • Organizational scalability: Each bounded context can be developed by an independent team
  • Controlled evolution: Changes in one context don’t affect others as long as the interfaces are maintained
  • Foundation for microservices: Bounded contexts are natural candidates to become microservices
  • Clearer testing: Tests reflect real business rules, not technical implementations

Trade-offs / Disadvantages

  • Steep learning curve: DDD introduces many concepts that take time to master
  • Overhead in simple domains: For basic CRUDs, DDD adds unnecessary complexity
  • Requires access to domain experts: Without constant collaboration with the business, the model drifts
  • More initial code: Entities, value objects, repositories, and events require more code than a direct approach
  • Risk of over-engineering: It’s easy to apply all the tactical patterns when you only need a few
  • Intentional duplication: Each bounded context may have its own representation of similar concepts
  • Investment in discovery: Event Storming and modeling sessions require time and active participation from the business

When to use

  • Complex domains with many business rules
  • Systems that evolve frequently due to business changes
  • Large teams that need to work in parallel with clear boundaries
  • Projects where communication between business and development is critical
  • Systems being migrated from a monolith to microservices
  • When the cost of an incorrect model is high (finance, healthcare, logistics)

When to avoid

  • Simple CRUD applications with no significant business logic
  • Prototypes or MVPs where speed matters more than structure
  • Small teams without access to domain experts
  • Systems with well-understood, stable domains that don’t change
  • Projects with a very limited budget or time for the design phase

Common technologies and implementations

CategoryOptions
DDD frameworksAxon Framework (Java), EventSourcing.NetCore (C#), Ecotone (PHP)
Event SourcingEventStoreDB, Axon Server, Marten (.NET)
CQRSMediatR (.NET), Axon (Java), custom implementations
MessagingApache Kafka, RabbitMQ, Amazon EventBridge
ModelingEvent Storming, Domain Storytelling, Context Mapping
PersistencePostgreSQL, MongoDB (per bounded context)

Relationship with other patterns

DDD is the conceptual foundation on which many other distributed architecture patterns are built:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚              Domain-Driven Design                β”‚
β”‚         (Bounded Contexts + Ubiquitous Language) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚          β”‚          β”‚
           β–Ό          β–Ό          β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚Microserv.β”‚ β”‚  Event   β”‚ β”‚    Shared    β”‚
    β”‚ Pattern  β”‚ β”‚  Driven  β”‚ β”‚    Kernel    β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚          β”‚
           β–Ό          β–Ό
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚   Saga   β”‚ β”‚  Outbox  β”‚
    β”‚ Pattern  β”‚ β”‚ Pattern  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Microservices Pattern: DDD’s bounded contexts are the guide for defining the boundaries of each microservice
  • Event-Driven Architecture: DDD’s Domain Events are the foundation of asynchronous communication between services
  • Saga Pattern: Coordinates transactions that cross multiple bounded contexts using events and compensations
  • Outbox Pattern: Guarantees that Domain Events are published reliably together with data changes
  • Anti-Corruption Layer (ACL): Implements the Context Mapping relationship that protects your domain from external models
  • Shared Kernel: Implements the Context Mapping relationship where two contexts share a subset of the model
  • API Gateway / BFF: Provide entry points that can map external requests to the internal bounded contexts

Common mistakes when applying DDD

MistakeConsequenceSolution
Applying DDD to the entire systemOver-engineering in simple areasUse DDD only in the core domain, CRUD for the rest
Bounded contexts that are too largeDistributed monolithSplit by real subdomains, not by technical layers
Bounded contexts that are too smallChatty servicesGroup functionality that changes together
Ignoring the ubiquitous languageCode disconnected from the businessRegular sessions with domain experts
Anemic modelLogic scattered across servicesMove behavior into the entities
Aggregates that are too largeConcurrency problemsKeep aggregates small, use events between them

Next steps

With DDD as the foundation for modeling complex domains, the natural next step is to understand how to handle transactions that cross multiple bounded contexts. That’s what the Saga Pattern is for, coordinating distributed operations with compensations. Also explore the Microservices Pattern to understand how bounded contexts turn into independent services.