Domain Driven Design¶
What is Domain-Driven Design?¶
An approach to software development that centers the design around the business domain and its logic.
Strategic Design¶
Domain Types¶
Bounded Contexts¶
A boundary within which a domain model is defined and applicable.
Context Mapping¶
Relationships between bounded contexts.
Tactical Design¶
Building Blocks¶
Entities¶
Objects with identity that persists over time.
public class Order {
private final OrderId id; // Identity
private CustomerId customerId;
private OrderStatus status;
private List<OrderLine> lines;
private Money total;
private Instant createdAt;
// Identity comparison
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order)) return false;
Order order = (Order) o;
return id.equals(order.id); // Only compare by ID
}
@Override
public int hashCode() {
return Objects.hash(id);
}
// Business logic belongs in entity
public void addLine(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify confirmed order");
}
lines.add(new OrderLine(product, quantity));
recalculateTotal();
}
public void confirm() {
if (lines.isEmpty()) {
throw new IllegalStateException("Cannot confirm empty order");
}
this.status = OrderStatus.CONFIRMED;
}
}
Value Objects¶
Immutable objects defined by their attributes, not identity.
// Value Object - defined by attributes, immutable
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
Objects.requireNonNull(currency);
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
}
// Another example
public record Address(
String street,
String city,
String state,
String zipCode,
String country
) {
public Address {
Objects.requireNonNull(street);
Objects.requireNonNull(city);
Objects.requireNonNull(zipCode);
Objects.requireNonNull(country);
}
}
// Usage
Money price1 = new Money(new BigDecimal("10.00"), Currency.USD);
Money price2 = new Money(new BigDecimal("5.00"), Currency.USD);
Money total = price1.add(price2); // New object created
// Value objects are equal if attributes match
Address addr1 = new Address("123 Main", "NYC", "NY", "10001", "USA");
Address addr2 = new Address("123 Main", "NYC", "NY", "10001", "USA");
addr1.equals(addr2); // true
Aggregates¶
Cluster of entities and value objects with defined boundaries.
// Order Aggregate
public class Order { // Aggregate Root
private final OrderId id;
private final CustomerId customerId; // Reference by ID, not Customer object
private OrderStatus status;
private final List<OrderLine> lines = new ArrayList<>(); // Internal entity
private ShippingAddress shippingAddress; // Value object
// All modifications through root
public void addLine(ProductId productId, int quantity, Money unitPrice) {
validateCanModify();
OrderLine line = new OrderLine(productId, quantity, unitPrice);
lines.add(line);
}
public void removeLine(ProductId productId) {
validateCanModify();
lines.removeIf(line -> line.getProductId().equals(productId));
}
public void updateShippingAddress(ShippingAddress address) {
validateCanModify();
this.shippingAddress = address;
}
public void submit() {
if (lines.isEmpty()) {
throw new OrderEmptyException(id);
}
if (shippingAddress == null) {
throw new ShippingAddressRequiredException(id);
}
this.status = OrderStatus.SUBMITTED;
// Register domain event
registerEvent(new OrderSubmitted(id, customerId, getTotal()));
}
private void validateCanModify() {
if (status != OrderStatus.DRAFT) {
throw new OrderNotModifiableException(id);
}
}
// Expose immutable view of internal state
public List<OrderLine> getLines() {
return Collections.unmodifiableList(lines);
}
}
// Internal entity (not accessible directly from outside)
public class OrderLine {
private final ProductId productId;
private int quantity;
private final Money unitPrice;
public Money getSubtotal() {
return unitPrice.multiply(quantity);
}
}
Aggregate Design Guidelines¶
Domain Services¶
Operations that don't naturally belong to an entity or value object.
// Domain Service - cross-aggregate business logic
public class PricingService {
private final DiscountPolicy discountPolicy;
private final TaxCalculator taxCalculator;
public Money calculateOrderTotal(Order order, Customer customer) {
Money subtotal = order.getSubtotal();
// Apply customer-specific discounts
Money discount = discountPolicy.calculate(customer, subtotal);
Money discountedTotal = subtotal.subtract(discount);
// Calculate tax based on shipping address
Money tax = taxCalculator.calculate(
discountedTotal,
order.getShippingAddress()
);
return discountedTotal.add(tax);
}
}
// Another example - Transfer between accounts
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
if (!from.canWithdraw(amount)) {
throw new InsufficientFundsException();
}
from.withdraw(amount);
to.deposit(amount);
// Publish event
eventPublisher.publish(new TransferCompleted(
from.getId(), to.getId(), amount
));
}
}
Repositories¶
Abstract persistence for aggregates.
// Repository interface (domain layer)
public interface OrderRepository {
Optional<Order> findById(OrderId id);
Order save(Order order);
void delete(Order order);
List<Order> findByCustomerId(CustomerId customerId);
}
// Implementation (infrastructure layer)
@Repository
public class JpaOrderRepository implements OrderRepository {
private final OrderJpaRepository jpaRepo;
private final OrderMapper mapper;
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepo.findById(id.value())
.map(mapper::toDomain);
}
@Override
public Order save(Order order) {
OrderEntity entity = mapper.toEntity(order);
OrderEntity saved = jpaRepo.save(entity);
return mapper.toDomain(saved);
}
}
// Repository returns aggregates, not entities
// One repository per aggregate root
Domain Events¶
Record of something significant that happened in the domain.
// Domain Event
public record OrderSubmitted(
OrderId orderId,
CustomerId customerId,
Money totalAmount,
Instant occurredAt
) implements DomainEvent {
public OrderSubmitted(OrderId orderId, CustomerId customerId, Money totalAmount) {
this(orderId, customerId, totalAmount, Instant.now());
}
}
// Aggregate with event registration
public abstract class AggregateRoot {
private final List<DomainEvent> domainEvents = new ArrayList<>();
protected void registerEvent(DomainEvent event) {
domainEvents.add(event);
}
public List<DomainEvent> getDomainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearDomainEvents() {
domainEvents.clear();
}
}
public class Order extends AggregateRoot {
public void submit() {
// Business logic...
this.status = OrderStatus.SUBMITTED;
// Register event for publishing
registerEvent(new OrderSubmitted(id, customerId, getTotal()));
}
}
// Event publishing in application service
@Service
public class OrderApplicationService {
@Transactional
public void submitOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.submit();
orderRepository.save(order);
// Publish events after transaction
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearDomainEvents();
}
}
Factories¶
Encapsulate complex object creation.
// Factory for complex aggregate creation
public class OrderFactory {
private final CustomerRepository customerRepository;
private final ProductCatalog productCatalog;
private final PricingService pricingService;
public Order createOrder(CreateOrderRequest request) {
// Validate customer exists
Customer customer = customerRepository.findById(request.customerId())
.orElseThrow(() -> new CustomerNotFoundException(request.customerId()));
// Create order
Order order = new Order(
OrderId.generate(),
customer.getId(),
request.shippingAddress()
);
// Add lines with current pricing
for (OrderLineRequest lineRequest : request.lines()) {
Product product = productCatalog.getProduct(lineRequest.productId());
Money price = pricingService.getPrice(product, customer);
order.addLine(product.getId(), lineRequest.quantity(), price);
}
return order;
}
}
Layered Architecture¶
Anti-Corruption Layer¶
Protect your domain from external models.
// External system's model (legacy or third-party)
public class LegacyCustomerDTO {
public String cust_id;
public String cust_name;
public String addr_line1;
public String addr_city;
public int cust_status; // 0=inactive, 1=active, 2=premium
}
// Your domain model
public record Customer(
CustomerId id,
CustomerName name,
Address address,
CustomerTier tier
) {}
// Anti-Corruption Layer
@Component
public class CustomerAntiCorruptionLayer {
public Customer translate(LegacyCustomerDTO legacy) {
return new Customer(
new CustomerId(legacy.cust_id),
new CustomerName(legacy.cust_name),
new Address(legacy.addr_line1, legacy.addr_city, /* ... */),
translateTier(legacy.cust_status)
);
}
private CustomerTier translateTier(int status) {
return switch (status) {
case 0 -> CustomerTier.INACTIVE;
case 1 -> CustomerTier.STANDARD;
case 2 -> CustomerTier.PREMIUM;
default -> throw new IllegalArgumentException("Unknown status: " + status);
};
}
public LegacyCustomerDTO translateBack(Customer customer) {
LegacyCustomerDTO dto = new LegacyCustomerDTO();
dto.cust_id = customer.id().value();
dto.cust_name = customer.name().value();
// ... more translations
return dto;
}
}
Common Interview Questions¶
- Entity vs Value Object?
- Entity: Has identity, mutable, compared by ID
-
Value Object: No identity, immutable, compared by attributes
-
What is an Aggregate?
- Cluster of entities/value objects with consistency boundary
- Accessed only through root
-
One transaction per aggregate
-
How to handle cross-aggregate transactions?
- Use eventual consistency
- Domain events and Saga pattern
-
Avoid distributed transactions
-
What is a Bounded Context?
- Explicit boundary where domain model applies
- Same term can mean different things in different contexts
-
Maps to microservice boundaries
-
How does DDD relate to microservices?
- Bounded contexts → Service boundaries
- Aggregates → Transaction boundaries
- Domain events → Service communication