Skip to content

Domain Driven Design

What is Domain-Driven Design?

An approach to software development that centers the design around the business domain and its logic.

DDD Core Principles


Strategic Design

Domain Types

Domain Types

Bounded Contexts

A boundary within which a domain model is defined and applicable.

Bounded Contexts

Context Mapping

Relationships between bounded contexts.

Context Mapping Patterns


Tactical Design

Building Blocks

DDD 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.

Aggregate Rules

// 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

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

DDD 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

  1. Entity vs Value Object?
  2. Entity: Has identity, mutable, compared by ID
  3. Value Object: No identity, immutable, compared by attributes

  4. What is an Aggregate?

  5. Cluster of entities/value objects with consistency boundary
  6. Accessed only through root
  7. One transaction per aggregate

  8. How to handle cross-aggregate transactions?

  9. Use eventual consistency
  10. Domain events and Saga pattern
  11. Avoid distributed transactions

  12. What is a Bounded Context?

  13. Explicit boundary where domain model applies
  14. Same term can mean different things in different contexts
  15. Maps to microservice boundaries

  16. How does DDD relate to microservices?

  17. Bounded contexts → Service boundaries
  18. Aggregates → Transaction boundaries
  19. Domain events → Service communication