Foundations DDD

Domain Modeling

Representing business concepts and rules in code — entities, value objects, aggregates, and the techniques that keep domain logic clean.

⏱ 8 min read

What it is

Domain modeling is the practice of creating a software representation of the concepts, relationships, and rules that govern a business domain. In Domain-Driven Design (DDD), this is done using a set of tactical building blocks: entities, value objects, aggregates, domain events, domain services, repositories, and factories. Together, these patterns provide a vocabulary and structure for capturing business logic in code in a way that mirrors the mental model of domain experts — rather than being shaped primarily by database tables or API endpoints.

The central goal of domain modeling is to make the code speak the language of the business. When a developer can read the code and recognize the business concepts directly — an Order that contains OrderLines, a Product with a Price (value object), a domain event OrderPlaced — the model serves as living documentation and the gap between the business and the code shrinks significantly.

Why it exists

Systems without explicit domain models tend toward the Anemic Domain Model antipattern: objects that are pure data bags (getters and setters) with all business logic scattered across service classes and procedure-style code. This makes the domain logic invisible, difficult to test, and impossible to communicate to non-technical stakeholders. Business rules become hidden in layers of infrastructure and service code, making the system fragile and the codebase resistant to change.

A rich domain model centralizes business rules in the entities and value objects that represent domain concepts. When the business rule "an Order cannot be cancelled after it has shipped" lives in the Order aggregate's cancel() method — rather than scattered across three service classes — it is discoverable, testable, and maintainable. Domain modeling is ultimately a strategy for long-term maintainability through high cohesion.

When to use

  • When building systems with complex business rules that will evolve over years — the domain model is the primary investment in long-term maintainability.
  • When working closely with domain experts and the ubiquitous language is well-established or actively being developed.
  • When building event-sourced systems — domain events are a natural fit for event sourcing.
  • When operating in a regulated industry where business rules must be auditable and traceable in code.
  • When building microservices — aggregates provide natural transaction and consistency boundaries that map well to service boundaries.

When not to use

  • Do not apply a rich domain model to simple CRUD systems — a thin service layer over a database is appropriate when there are no complex business rules.
  • Do not force DDD tactical patterns onto an anemic model — the result is ceremony without substance.
  • Do not use DDD aggregates as the sole driver of service decomposition; they define consistency boundaries, not deployment units.

Typical architecture

A typical rich domain model structures its building blocks in layers. The domain layer contains only pure business logic with no infrastructure dependencies. The application layer orchestrates use cases using domain objects and coordinates persistence via repository interfaces. Infrastructure provides concrete implementations.


Order Aggregate (e-commerce example):
──────────────────────────────────────────────────────
  ┌─────────────────────────────────────────────────┐
  │  Order [Aggregate Root - Entity]                │
  │  ─────────────────────────────────────────────  │
  │  id: OrderId (Value Object)                     │
  │  status: OrderStatus (Value Object / Enum)      │
  │  customerId: CustomerId (Value Object)          │
  │  lines: List<OrderLine> (Entity)               │
  │  shippingAddress: Address (Value Object)        │
  │                                                 │
  │  + place() → OrderPlaced (Domain Event)         │
  │  + cancel() → OrderCancelled (Domain Event)     │
  │  + addLine(product, qty) [enforces invariants]  │
  └─────────────────────────────────────────────────┘
          │ contains
          ▼
  ┌─────────────────────────────────────────────────┐
  │  OrderLine [Entity]                             │
  │  productId: ProductId (Value Object)            │
  │  quantity: Quantity (Value Object, must be > 0)│
  │  unitPrice: Money (Value Object)                │
  └─────────────────────────────────────────────────┘

Aggregate Invariant (always true inside the aggregate):
  "Total order value cannot exceed customer credit limit"
  "An order must have at least one line"
  "Only PENDING orders can be cancelled"
          

Pros and cons

Pros

  • Business rules are co-located with the objects they govern, making them discoverable and testable without infrastructure.
  • Value objects (immutable, equality by value) eliminate an entire class of bugs from mutable shared state.
  • Domain events provide a natural integration mechanism between aggregates without coupling.
  • Aggregates provide clear transaction boundaries that map naturally to service and storage boundaries.
  • The ubiquitous language reduces communication errors between developers and domain experts.

Cons

  • Higher upfront investment in design and modeling compared to simple CRUD architectures.
  • ORM mapping of rich aggregates can be complex — lazy loading and aggregate boundary enforcement require careful configuration.
  • Domain modeling skills are not uniformly distributed across engineering teams; adoption requires education.
  • Can produce over-engineered solutions in simple domains where CRUD would suffice.

Implementation notes

Entities vs Value Objects: An entity is an object with a unique identity that persists over time — an Order is still the same Order even after its status changes. A value object is defined entirely by its attributes and has no identity — two Money objects with the same amount and currency are interchangeable. Value objects should be immutable: "changing" a money value means creating a new one. Modeling primitive obsession (using raw strings and numbers instead of value objects) is one of the most common domain modeling mistakes and produces anemic code with scattered validation.

Aggregate design: An aggregate is a cluster of entities and value objects treated as a single unit for data changes and invariant enforcement. Every aggregate has a root entity — the only entry point for mutations. External objects hold references only to aggregate roots, never to internal entities. Aggregate boundaries are defined by business invariants: everything that must always be consistent must be inside one aggregate. Keep aggregates small — large aggregates become contention bottlenecks.

Event Storming: Event Storming (Alberto Brandolini) is the most effective technique for collaborative domain modeling. Stakeholders and developers use sticky notes to map domain events (things that happened), commands (things that cause events), aggregates (what handles commands), and policies (automated reactions to events) on a timeline. The output is a shared understanding of the domain model that informs bounded context boundaries and aggregate design.

Domain services: When an operation belongs to the domain but does not naturally fit within a single aggregate (e.g., a funds transfer that affects two accounts), model it as a domain service — a stateless operation expressed in the ubiquitous language. Do not confuse domain services with application services; a domain service contains business logic, an application service orchestrates the use case and coordinates persistence.

Common failure modes

  • Anemic Domain Model: Aggregates with only getters and setters, all business logic in service classes — the most common failure mode and the one DDD is specifically designed to avoid.
  • Large aggregates: An Order aggregate that contains Customer, Product catalog, and Inventory creates massive contention and violates single responsibility.
  • Repositories returning internal entities: Exposing OrderLine outside the Order aggregate boundary, allowing external code to bypass the aggregate's invariant enforcement.
  • Domain events used as DTOs: Treating domain events as mere data transfer objects rather than meaningful business facts with their own ubiquitous language names.
  • No domain events: Aggregates that mutate state but never publish events, making integration with other aggregates and contexts require polling or tight coupling.

Decision checklist

  • Have we identified the business invariants that define each aggregate's boundaries — the consistency rules that must always hold?
  • Are primitive types (strings, integers) replaced by value objects at the domain boundary (e.g., OrderId instead of String, Money instead of BigDecimal)?
  • Are value objects immutable in the implementation language?
  • Are all mutations to an aggregate routed through the aggregate root — never directly to child entities?
  • Does each significant domain operation produce a domain event that other aggregates and contexts can react to?
  • Is the domain layer free of infrastructure dependencies (no database calls, no HTTP clients, no framework annotations)?
  • Have we conducted or reviewed an Event Storming session with domain experts before finalizing the model?
  • Are aggregates small enough that a single database transaction on one aggregate will not become a contention bottleneck?

Example use cases

  • An e-commerce platform models Order as an aggregate with OrderLines as child entities and Money as a value object, enforcing the invariant "order total must not exceed customer credit limit" inside the aggregate.
  • A banking system models Account as an aggregate, with every debit and credit producing a TransactionRecorded domain event that feeds the ledger and reporting contexts.
  • A healthcare system models Prescription as an aggregate root, with Medication as a value object and a DrugInteractionChecker domain service that validates prescriptions against the patient's medication history.
  • Bounded Contexts — the strategic DDD pattern that provides the boundary within which a domain model applies.
  • Architecture Principles — domain modeling operationalizes the SRP and high cohesion principles at the code level.
  • CAP Theorem — aggregate boundaries define the scope of strong consistency, connecting domain modeling to distributed systems constraints.

Further reading