An Introduction to Domain-Driven Design

Tommy Cheese | · Reading time: 8 min

Overview of Domain-Driven Design

Domain-Driven Design (DDD) is an approach to building software around a business domain. Developers and domain experts explore business knowledge together, develop a domain model, and express its concepts, rules, and language in code to help teams understand and maintain complex business behavior.

DDD combines two interacting levels of design. Strategic design addresses subdomains, bounded contexts, and their relationships. Tactical design addresses entities, value objects, aggregates, domain services, and domain events within a context. Both evolve through practice.

E-commerce example: business subdomains correspond to bounded contexts; the ordering context contains an order aggregate whose root is an entity.
This is one possible design. The mapping between subdomains and bounded contexts requires modeling decisions; aggregates belong within a specific context.

Concepts in Strategic Design

Domain

A domain is the area of business knowledge and activity that software addresses. An e-commerce system might involve product sales, payment settlement, and order fulfillment. Understanding the domain requires discussing business goals, processes, and rules, rather than only listing system features.

Subdomains

Identify subdomains by business capability, then classify them by their contribution to competitive advantage and the availability of established solutions:

  • Core subdomains: capabilities that provide business differentiation and competitive advantage, and merit greater investment in modeling and development.
  • Supporting subdomains: capabilities needed to run the business that have company-specific requirements but do not directly provide its core competitive advantage.
  • Generic subdomains: capabilities for which the industry already offers established solutions that can usually be purchased or reused.

The classification depends on the business. Authentication might be generic for an online retailer but core for an identity-service provider. Being used by several modules does not, by itself, make a capability a generic subdomain.

Ubiquitous Language

Domain experts and developers should establish and continually use a shared language within each bounded context, keeping terminology consistent across conversations, documentation, and code. For example, they need a shared understanding of whether “order paid” requires full payment and when cancellation is allowed. Ubiquitous language is consistent within its context; it does not require a single model for the entire enterprise.

Bounded Contexts

A bounded context defines the scope within which a particular domain model and its language remain consistent and valid. Subdomains partition business problems; bounded contexts delimit models. They should not be defined as a fixed containment hierarchy. A clear mapping is desirable, but existing systems and organizational constraints also affect the design.

For example, a “product” in a sales context might focus on pricing and discounts, while a warehouse context focuses on stock locations and inventory movements. The models can be linked by product identity without sharing one product class containing every attribute and behavior.

An end-to-end business process can span contexts: ordering, payment, and fulfillment contexts can collaborate to place, pay for, and ship an order. A context map makes those relationships and integration contracts explicit. An anti-corruption layer can translate external models to protect a context’s own model when needed.

Bounded contexts help identify service boundaries, but are not deployment units. DDD can be used in modular monoliths and microservices. One context can be implemented by multiple modules or physical services; “one context, one microservice” is not a mandatory rule.

Concepts in Tactical Design

Value Objects and Entities

Distinguish entities from value objects by whether the business needs to track their identity over time:

  • Entity: a domain object identified by its identity. An order remains the same order when its amount or status changes, provided its identity is the same. Two orders with different identities are distinct even when their amounts match.
  • Value object: an object with no independent identity that needs tracking, compared by its business-significant values. Two monetary amounts with the same amount and currency are interchangeable; amounts in different currencies are not equal.

Value objects are usually immutable: replace an old value with a new one to avoid unintended changes through shared references. Entities can also have immutable implementations. Equality follows identity or value semantics, not mutability. The same concept can be modeled differently in different contexts.

In complex domains, entities, value objects, and aggregate roots should encapsulate the business rules that belong to them. An order’s “cancel” behavior can check whether cancellation is allowed, rather than letting callers change its status arbitrarily. The application layer orchestrates use cases, while infrastructure handles database access and other technical details. Rich domain behavior does not require entities to access a database directly.

Objects containing only data and getters/setters, with all business rules moved elsewhere, are commonly associated with an anemic domain model. This is not a general recommendation for complex domain modeling, although simple CRUD scenarios can use simpler data models. A POJO is an ordinary Java object; the term does not restrict whether it contains business logic.

Repository

A repository offers collection-like access to domain objects while hiding query, mapping, and storage details. In DDD, repositories are usually designed around aggregate roots that require persistence and direct access, rather than around every table or internal entity.

Interfaces should express clear domain intent, such as loading an order by its identity or saving an order aggregate. Short method names are not a design criterion. Avoid exposing database fields and arbitrary single-table updates directly to the domain layer.

MyBatis can implement a repository. Whether a Mapper can directly fulfill the repository interface depends on its ability to load, save, and encapsulate aggregates. An ordinary single-table CRUD Mapper is not automatically a DDD repository merely because it provides storage operations.

Aggregates and Aggregate Roots

An aggregate is a transactional consistency boundary that protects business invariants. Invariants are rules that must hold whenever a business update commits. An aggregate can contain several entities and value objects, or only a root entity. The aggregate root is itself an entity, serving as the entry point and protecting the rules inside the aggregate.

Suppose an order model requires the total amount always to equal the sum of its line amounts. The order and its lines can form one aggregate, with the order root controlling changes to lines. The boundary follows consistency requirements, rather than the assumption that every related object belongs together.

  • External code should change aggregate state through the root’s behavior, avoiding direct changes to internal entities that bypass business rules. Immutable value objects can safely be returned to callers.
  • When a business update commits, the aggregate must satisfy the rules requiring immediate consistency, such as positive line quantities and a correctly calculated total. Persistence also requires appropriate transactions and concurrency control.
  • Prefer identity-based references between aggregates. Domain events and eventual consistency can coordinate them. Business requirements determine whether stronger consistency is necessary; association alone is not a reason to combine all related objects.

“Load, modify, save” can describe a use case, but does not mean every read must occur inside a database transaction covering the entire process. Aggregate boundaries express consistency requirements; transaction and concurrency strategies remain implementation decisions.

Domain Events

A domain event expresses a business-significant fact that has already occurred in the domain, such as “order canceled.” Business meaning and rules determine events; arbitrary changes to entity attributes do not automatically produce them. Names commonly use the past tense, and the facts carried by events are usually immutable.

Internal domain events can be dispatched synchronously or asynchronously within a process. For cross-context communication, facts from successfully committed operations are typically translated into integration events and delivered to other services through messaging infrastructure. A message queue is an implementation choice, not part of the definition of a domain event.

For example, cancellation can notify the inventory context to release reserved stock. The recorded fact that cancellation occurred cannot be rewritten as though it never happened, but the business may perform later actions or compensation. Event immutability does not mean every business outcome is impossible to adjust.

Domain Services

An important domain operation that does not naturally belong to an entity or value object can be modeled as a domain service. In a model that combines destination, shipping method, and carrier quotations, shipping-cost calculation might be such a service. The decision depends on where behavior belongs, not on how many objects it involves.

Domain services are usually stateless, and their names and operations should use the ubiquitous language. If “add a line to an order” naturally belongs to the order aggregate, it can be an order behavior without an additional service to synchronize entity state.

Application services receive use-case requests, load aggregates, invoke domain behavior, and coordinate persistence and transactions. Domain services implement business rules that do not belong to other domain objects. Neither concept is equivalent to an independently deployed microservice.

Domain Modeling in DDD

The following sequence offers a starting point. Return to earlier steps as new knowledge and implementation feedback emerge:

  1. Discuss representative business scenarios, rules, and goals with domain experts; develop the ubiquitous language and identify initial subdomains.
  2. Determine where each model remains consistent, define bounded contexts, and describe their collaboration and integration contracts.
  3. Identify entities, value objects, and business invariants within each context; use them to design aggregate boundaries, roots, and behavior.
  4. Design domain services, events, repositories, and object creation mechanisms as needed; clarify the responsibilities of the application layer and infrastructure.
  5. Check the model against concrete scenarios and verify key rules. As business understanding changes, revise code, language, and model boundaries together.

Starting with an initial strategic partition and then refining tactical models is practical, but these are not two irreversible phases. DDD uses ongoing collaboration and feedback to make software models express the business accurately. Choose patterns according to business complexity, rather than adding structure simply to include every concept.

References

Comments

Join the conversation with your GitHub account. All language editions share this discussion.

Type Home, News or Frontier Research to navigate, or search posts.

↑↓ Select ↵ Open Esc Close