| name | ddd |
| description | Use when working on DDD (Domain-Driven Design) architecture โ bounded contexts, entities, value objects, aggregates, domain events, repositories, domain services, factories, CQRS, event sourcing. Trigger when user mentions DDD, domain modeling, strategic design, tactical design, CQRS, event sourcing, or asks about architectural patterns for complex business domains. |
Domain-Driven Design (DDD) Best Practices
Overview
Domain-Driven Design is an approach to software development that focuses on modeling software based on the business domain. It bridges the gap between technical and business language through a shared model.
When to Apply DDD:
- Complex business domains with rich logic
- Teams that need a shared language (ubiquitous language)
- Long-lived systems where maintainability matters
- Systems where domain experts and developers collaborate
When NOT to Apply DDD:
- Simple CRUD applications with minimal business logic
- Highly standardized domains (e.g., basic e-commerce without complex rules)
- Short-lived projects with disposable code
Strategic Design
Strategic design is about the big picture โ how the system is divided into bounded contexts and how they relate to each other.
Bounded Context
A bounded context is a linguistic and conceptual boundary where a particular domain model is valid and consistent.
โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ
โ Order Context โ โ Warehouse Context โ
โ - Order โ โ - Inventory โ
โ - OrderLine โ โ - StockLevel โ
โ - CustomerRef โ โ - Shipment โ
โโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโโโโ
Key principle: Each bounded context has its own ubiquitous language. The word "Order" may mean different things in different contexts.
Ubiquitous Language
A shared language between developers and domain experts, used consistently within a bounded context.
- Rule: Use the same terminology in code, documents, and conversation
- Example: If domain experts say "backorder", the code should use
Backorder, not DelayedOrder or OutOfStockItem
Context Mapping
How bounded contexts relate to each other:
| Pattern | Description | When to Use |
|---|
| Partnership | Two contexts cooperate, joint roadmap | Mutually dependent domains |
| Shared Kernel | Shared subset of the domain model | Closely related, stable shared model |
| Customer-Supplier | Upstream produces, downstream consumes | Clear dependency, async communication |
| Conformist | Downstream conforms to upstream model | No influence over upstream |
| Anti-Corruption Layer | Translates between contexts | Integrating legacy/external systems |
| Open Host Service | Context defines a published protocol | Multiple consumers |
| Published Language | Shared exchange format (e.g., JSON schema) | Public APIs |
| Separate Ways | No integration needed | Completely independent |
| Big Ball of Mud | Unstructured, avoid if possible | Legacy systems |
Tactical Design
Tactical design is the implementation-level building blocks within a bounded context.
Building Blocks Overview
Domain Model
โโโ Value Objects # Immutable, identity-less concepts
โโโ Entities # Objects with identity continuity
โโโ Aggregates # Cluster of related objects with one root
โโโ Domain Events # Something notable happened
โโโ Domain Services # Operations that don't belong to entities
โโโ Repositories # Persistence abstraction
โโโ Factories # Object creation logic
Value Objects
Value objects are immutable descriptions of qualities. They have no identity โ two value objects with the same attributes are interchangeable.
Characteristics:
- Immutable after creation
- No identity (equality based on attributes)
- Self-validating
- Side-effect-free methods
Example โ Money:
public final class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
this.amount = Objects.requireNonNull(amount, "amount must not be null");
this.currency = Objects.requireNonNull(currency, "currency must not be null");
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("amount must not be negative");
}
this.amount = amount.stripTrailingZeros();
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException(
"cannot add different currencies: " + this.currency + " and " + other.currency
);
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(double factor) {
return new Money(
this.amount.multiply(BigDecimal.valueOf(factor)),
this.currency
);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0
&& currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
public BigDecimal getAmount() { return amount; }
public Currency getCurrency() { return currency; }
}
Entities
Entities are objects with a distinct identity that persists across state changes. Identity is primary, not attributes.
Characteristics:
- Mutable state
- Identity persists across lifecycle
- Equality by identity (ID), not attributes
- May have invariants that span multiple attributes
Example โ Order:
public class Order {
private final OrderId id;
private CustomerId customerId;
private OrderStatus status;
private List<OrderLine> lines;
private Money totalAmount;
public Order(CustomerId customerId) {
this.id = OrderId.generate();
this.customerId = Objects.requireNonNull(customerId);
this.status = OrderStatus.DRAFT;
this.lines = new ArrayList<>();
this.totalAmount = new Money(BigDecimal.ZERO, Currency.getInstance("USD"));
}
public void addLine(ProductId productId, int quantity, Money unitPrice) {
if (this.status != OrderStatus.DRAFT) {
throw new OrderDomainException("cannot modify order in status: " + this.status);
}
if (quantity <= 0) {
throw new IllegalArgumentException("quantity must be positive");
}
OrderLine line = new OrderLine(productId, quantity, unitPrice);
this.lines.add(line);
recalculateTotal();
}
public void submit() {
if (this.lines.isEmpty()) {
throw new OrderDomainException("cannot submit empty order");
}
this.status = OrderStatus.SUBMITTED;
}
private void recalculateTotal() {
this.totalAmount = this.lines.stream()
.map(OrderLine::getSubtotal)
.reduce(
new Money(BigDecimal.ZERO, Currency.getInstance("USD")),
Money::add
);
}
public OrderId getId() { return id; }
public OrderStatus getStatus() { return status; }
public List<OrderLine> getLines() { return Collections.unmodifiableList(lines); }
public Money getTotalAmount() { return totalAmount; }
}
Aggregates
An aggregate is a cluster of related entities and value objects with one as the root. It defines a transactional boundary โ all changes to objects within the aggregate happen through the root.
Rules:
- The aggregate root is the only object accessible from outside
- External references point only to the aggregate root
- Changes within an aggregate are atomic (single transaction)
- Invariants are enforced at the aggregate boundary
public class OrderAggregate {
private final Order order;
private final List<OrderLine> lines;
public OrderAggregate(Order order) {
this.order = Objects.requireNonNull(order);
this.lines = new ArrayList<>(order.getLines());
}
public void addLine(ProductId productId, int qty, Money price) {
order.addLine(productId, qty, price);
}
public void submit() {
order.submit();
}
}
Domain Events
Domain events represent something significant that happened in the domain. They are immutable records of past occurrences.
Characteristics:
- Named in past tense (OrderPlaced, PaymentReceived)
- Immutable and immutable payload
- Published after the state change is committed
- Can trigger side effects in the same or other bounded contexts
public abstract class DomainEvent {
private final UUID eventId;
private final Instant occurredOn;
protected DomainEvent() {
this.eventId = UUID.randomUUID();
this.occurredOn = Instant.now();
}
public UUID getEventId() { return eventId; }
public Instant getOccurredOn() { return occurredOn; }
}
public class OrderSubmittedEvent extends DomainEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final Money totalAmount;
public OrderSubmittedEvent(OrderId orderId, CustomerId customerId, Money totalAmount) {
super();
this.orderId = orderId;
this.customerId = customerId;
this.totalAmount = totalAmount;
}
public OrderId getOrderId() { return orderId; }
public CustomerId getCustomerId() { return customerId; }
public Money getTotalAmount() { return totalAmount; }
}
Domain Services
Domain services handle operations that don't naturally belong to any entity or value object โ typically multi-entity operations.
Use domain services when:
- The operation conceptually belongs to the domain but not a specific entity
- The operation involves multiple aggregates
- The operation is a pure transformation (no side effects)
public class PricingService {
public Money calculatePrice(List<OrderLine> lines, CustomerTier customerTier) {
Money baseTotal = lines.stream()
.map(OrderLine::getSubtotal)
.reduce(
new Money(BigDecimal.ZERO, Currency.getInstance("USD")),
Money::add
);
DiscountPolicy policy = DiscountPolicy.forTier(customerTier);
return policy.applyTo(baseTotal);
}
}
Repositories
Repositories abstract the persistence of aggregates. They provide a collection-like interface for accessing aggregate roots.
Rules:
- Repositories operate on aggregate roots only
- Never expose internal entities or value objects directly
- Hide the persistence infrastructure details
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
void save(Order order);
void delete(Order order);
}
Factories
Factories encapsulate the complex logic of creating objects, especially aggregates. They hide construction complexity and ensure invariants are established.
public class OrderFactory {
public OrderAggregate createOrder(CustomerId customerId, List<OrderItemData> items) {
Order order = new Order(customerId);
if (items != null && !items.isEmpty()) {
for (OrderItemData item : items) {
order.addLine(item.getProductId(), item.getQuantity(), item.getUnitPrice());
}
}
return new OrderAggregate(order);
}
}
Layered Architecture Overview
DDD follows a strict four-layer architecture. Each layer has clear responsibilities and dependencies point inward only:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Interfaces Layer๏ผๆฅๅฃๅฑ๏ผ โ โ Driving adapters: REST, gRPC, UI
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Application Layer๏ผๅบ็จๅฑ๏ผ โ โ Use cases, orchestration
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Domain Layer๏ผ้ขๅๅฑ๏ผ โ โ Business rules, domain model
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Infrastructure Layer๏ผๅบ็ก่ฎพๆฝๅฑ๏ผ โ โ Persistence, messaging, external services
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Dependencies point inward only
| Layer | Responsibility | Dependency Rule |
|---|
| Interfaces | Driving adapters โ accepts external input (REST, gRPC, CLI) | Depends only on Application |
| Application | Orchestrates use cases, coordinates domain objects | Depends on Domain |
| Domain | Contains all business rules, entities, value objects, aggregates | No external dependencies |
| Infrastructure | Driven adapters โ implements ports from Domain/Application | Supports all layers |
Dependency Direction
Critical rule: Dependencies ALWAYS point inward. The Domain layer never depends on Application, Infrastructure, or Presentation. This is enforced through interfaces (e.g., OrderRepository is defined in Domain, implemented in Infrastructure).
public interface OrderRepository {
Optional<Order> findById(OrderId id);
}
@Repository
public class JpaOrderRepository implements OrderRepository {
}
Interfaces Layer
The interfaces layer contains driving adapters that bridge external systems to the application. It handles HTTP, gRPC, CLI, or any external input โ accepting requests, validating input, and returning responses. It knows nothing about business rules.
REST Controller
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderApplicationService orderService;
public OrderController(OrderApplicationService orderService) {
this.orderService = orderService;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderId orderId = orderService.placeOrder(
new PlaceOrderCommand(
request.getCustomerId(),
request.getItems()
)
);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(OrderResponse.of(orderId));
}
@GetMapping("/{id}")
public ResponseEntity<OrderResponse> getOrder(@PathVariable String id) {
OrderDto order = orderService.getOrder(new OrderId(id));
return ResponseEntity.ok(OrderResponse.fromDto(order));
}
}
Request/Response DTOs
public record CreateOrderRequest(
@NotNull(message = "customerId is required")
String customerId,
@NotEmpty(message = "items cannot be empty")
List<OrderItemRequest> items
) {}
public record OrderResponse(
String orderId,
String customerId,
OrderStatusDto status,
List<OrderItemResponse> items,
MoneyDto totalAmount,
Instant createdAt
) {
public static OrderResponse of(OrderId id) {
return new OrderResponse(id.getValue(), null, null, null, null, null);
}
public static OrderResponse fromDto(OrderDto dto) {
return new OrderResponse(
dto.orderId(),
dto.customerId(),
OrderStatusDto.fromStatus(dto.status()),
dto.items().stream().map(OrderItemResponse::fromDto).toList(),
MoneyDto.of(dto.totalAmount()),
dto.createdAt()
);
}
}
Application Layer
The application layer coordinates the domain objects to accomplish user goals. It is thin โ it doesn't contain business logic, only orchestration.
Application Service
@Service
@Transactional
public class OrderApplicationService {
private final OrderRepository orderRepository;
private final DomainEventPublisher eventPublisher;
public OrderId placeOrder(PlaceOrderCommand command) {
OrderAggregate order = orderFactory.createOrder(
new CustomerId(command.getCustomerId()),
command.getItems().stream()
.map(i -> new OrderItemData(
new ProductId(i.getProductId()),
i.getQuantity(),
new Money(i.getUnitPrice(), Currency.getInstance("USD"))
))
.toList()
);
order.submit();
orderRepository.save(order.getRoot());
eventPublisher.publish(new OrderSubmittedEvent(
order.getRoot().getId(),
new CustomerId(command.getCustomerId()),
order.getRoot().getTotalAmount()
));
return order.getRoot().getId();
}
}
CQRS (Command Query Responsibility Segregation)
Separate models for reading and writing. Commands modify state; queries read state.
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Write Side โ
โ Command โ Application Service โ Domain โ Repository โ
โ (Order submitted, price updated) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ Domain Events
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Read Side โ
โ Event Handlers โ Projection โ Read Model (DTOs) โ
โ (order_summary_view, customer_dashboard) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Event Sourcing
Instead of storing current state, store the sequence of events that led to it. The state is reconstructed by replaying events.
When to use:
- Full audit trail is required
- Temporal queries ("show me all state at point X")
- Complex aggregate history needed
- Event-driven integration between contexts
Trade-offs:
- Added complexity in event schema evolution
- Projections needed for read models
- Event replay can be slow for large aggregates
Anti-Patterns
1. Anemic Domain Model
Entities and value objects with little to no behavior โ just getters and setters. Business logic spills into application services.
Problem:
public class Order {
private Long id;
private BigDecimal total;
public BigDecimal getTotal() { return total; }
public void setTotal(BigDecimal total) { this.total = total; }
}
Solution: Move behavior into the domain model.
2. God Objects
Putting too much logic in a single class/aggregate. Violates single responsibility.
Problem: A single Order class that handles pricing, validation, fulfillment, notifications, reporting.
Solution: Separate aggregates with clear boundaries. Use domain events for cross-aggregate communication.
3. Violating Aggregate Boundaries
Accessing internal entities from outside the aggregate. Breaks encapsulation.
Problem:
Order order = orderRepo.findById(id);
order.getLines().get(0).setQuantity(5);
Solution: All modifications through aggregate root methods.
4. Anemic Services
Application services that contain all business logic instead of domain model.
Problem: Service methods that look like: calculatePrice(), validateOrder(), sendNotification() โ all in one service.
Solution: Distribute logic to the appropriate domain objects (entities, value objects, domain services).
5. Anemic Value Objects
Value objects that are mutable and have no validation.
Solution: Make value objects immutable and self-validating.
Quick Reference
Entity vs Value Object
| Question | Answer = Entity | Answer = Value Object |
|---|
| Does it need identity? | Yes | No |
| Should it be mutable? | Often yes | No (immutable) |
| Does it have lifecycle continuity? | Yes | No |
| Can two with same attributes be interchangeable? | No | Yes |
Aggregate Size
| Too Small | Just Right | Too Large |
|---|
| One entity per aggregate | Cluster of related objects | Entire system as one aggregate |
| High inter-aggregate coordination | Clear transactional boundary | Transaction conflicts, performance issues |
When to Publish Domain Events
- State changes that other parts of the system care about
- Cross-aggregate business rules are satisfied
- After the aggregate root commits successfully
- Never within a constructor (use factory or factory method instead)
Bounded Context Discovery
Look for:
- Different business processes with different rules
- Teams that work independently
- Different terminology for the same concept
- Different rates of change
- Distinct persistence needs
Package Structure (Java)
com.example.interfaces/ # Layer 1: Interfaces (driving adapters)
โโโ rest/
โ โโโ OrderController.java // REST adapter
โโโ dto/
โ โโโ request/
โ โ โโโ CreateOrderRequest.java // Input DTO
โ โ โโโ OrderItemRequest.java
โ โโโ response/
โ โโโ OrderResponse.java
โ โโโ MoneyDto.java
com.example.application/ # Layer 2: Application
โโโ order/
โ โโโ OrderApplicationService.java // Application service
โ โโโ PlaceOrderCommand.java // Command DTO
โ โโโ OrderDto.java // Query DTO
โโโ event/
โโโ OrderEventHandler.java // Event handler
com.example.domain/ # Layer 3: Domain (core, no deps)
โโโ model/
โ โโโ order/
โ โ โโโ Order.java // Aggregate root (entity)
โ โ โโโ OrderId.java // Value object
โ โ โโโ OrderLine.java // Entity (internal to aggregate)
โ โ โโโ OrderStatus.java // Value object (enum-like)
โ โ โโโ Money.java // Value object
โ โ โโโ OrderSubmittedEvent.java // Domain event
โ โ โโโ OrderDomainException.java // Domain exception
โ โโโ customer/
โ โ โโโ CustomerId.java
โ โโโ pricing/
โ โโโ PricingService.java // Domain service
โ โโโ DiscountPolicy.java // Value object / strategy
โโโ repository/
โ โโโ OrderRepository.java // Repository interface (defined here)
โโโ factory/
โโโ OrderFactory.java // Factory
com.example.infrastructure/ # Layer 4: Infrastructure
โโโ persistence/
โ โโโ JpaOrderRepository.java // Repository implementation
โ โโโ entity/
โ โโโ OrderEntity.java // JPA entity
โโโ messaging/
โ โโโ DomainEventPublisher.java // Event publishing (e.g., Kafka)
โโโ external/
โโโ PaymentGatewayAdapter.java // Anti-corruption layer
Dependency Rules Summary
presentation โ application โ domain โ infrastructure
โ
(interface only)
domain/ โ Core. Zero dependencies on any other layer. Defines repository interfaces.
application/ โ Depends on domain/. Orchestrates domain objects.
interfaces/ โ Depends on application/. Driving adapters (REST, gRPC, CLI) handle HTTP only.
infrastructure/ โ Implements interfaces from domain/. Driven adapters (DB, messaging, external services).