Domain-Driven Design in Java: Advanced Patterns (2025)


Domain-Driven Design in Java

Domain-Driven Design (DDD) provides a powerful approach to building complex software systems by focusing on the core domain and its logic. This comprehensive guide explores advanced DDD patterns and their implementation in Java.

Pro Tip: Understanding DDD patterns helps developers create more maintainable and scalable applications by aligning code with business domain concepts.

DDD Basics

Note: DDD focuses on creating a shared understanding of the domain between technical and business stakeholders.

Core Concepts


// Entity
@Entity
@Table(name = "orders")
public class Order {
    @Id
    private OrderId id;
    
    @Embedded
    private CustomerId customerId;
    
    @OneToMany(cascade = CascadeType.ALL)
    private List orderLines;
    
    @Enumerated(EnumType.STRING)
    private OrderStatus status;
    
    public void addOrderLine(Product product, int quantity) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot modify non-draft order");
        }
        orderLines.add(new OrderLine(product, quantity));
    }
    
    public void confirm() {
        if (orderLines.isEmpty()) {
            throw new IllegalStateException("Cannot confirm empty order");
        }
        status = OrderStatus.CONFIRMED;
    }
}

// Value Object
@Embeddable
public class Money {
    private final BigDecimal amount;
    private final String currency;
    
    public Money(BigDecimal amount, String currency) {
        this.amount = amount;
        this.currency = 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);
    }
}

Bounded Contexts

Pro Tip: Bounded contexts help manage complexity by defining clear boundaries between different parts of the system.

Context Mapping


// Order Context
@BoundedContext(name = "OrderManagement")
public class OrderContext {
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private CustomerContext customerContext;
    
    public Order createOrder(CustomerId customerId, List orderLines) {
        // Verify customer exists in Customer Context
        if (!customerContext.exists(customerId)) {
            throw new CustomerNotFoundException(customerId);
        }
        
        Order order = new Order(customerId, orderLines);
        return orderRepository.save(order);
    }
}

// Customer Context
@BoundedContext(name = "CustomerManagement")
public class CustomerContext {
    @Autowired
    private CustomerRepository customerRepository;
    
    public boolean exists(CustomerId customerId) {
        return customerRepository.existsById(customerId);
    }
}

Aggregates

Note: Aggregates ensure consistency boundaries within the domain model.

Aggregate Implementation


@AggregateRoot
@Entity
public class Order {
    @Id
    private OrderId id;
    
    @Version
    private long version;
    
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    private List orderLines;
    
    public void addOrderLine(Product product, int quantity) {
        if (orderLines.size() >= 10) {
            throw new OrderLineLimitExceededException();
        }
        
        OrderLine orderLine = new OrderLine(product, quantity);
        orderLines.add(orderLine);
    }
    
    public void removeOrderLine(OrderLineId orderLineId) {
        orderLines.removeIf(line -> line.getId().equals(orderLineId));
    }
    
    public Money calculateTotal() {
        return orderLines.stream()
            .map(OrderLine::getSubtotal)
            .reduce(Money.ZERO, Money::add);
    }
}

Value Objects

Pro Tip: Value objects are immutable and represent descriptive aspects of the domain.

Value Object Implementation


@Embeddable
public class Address {
    private final String street;
    private final String city;
    private final String country;
    private final String postalCode;
    
    public Address(String street, String city, String country, String postalCode) {
        this.street = street;
        this.city = city;
        this.country = country;
        this.postalCode = postalCode;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Address address = (Address) o;
        return street.equals(address.street) &&
               city.equals(address.city) &&
               country.equals(address.country) &&
               postalCode.equals(address.postalCode);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(street, city, country, postalCode);
    }
}

Domain Events

Note: Domain events capture important business occurrences that other parts of the system might be interested in.

Domain Event Implementation


public class OrderConfirmedEvent extends DomainEvent {
    private final OrderId orderId;
    private final CustomerId customerId;
    private final Money totalAmount;
    
    public OrderConfirmedEvent(OrderId orderId, CustomerId customerId, Money totalAmount) {
        super();
        this.orderId = orderId;
        this.customerId = customerId;
        this.totalAmount = totalAmount;
    }
}

@Entity
public class Order {
    @Transient
    private List domainEvents = new ArrayList<>();
    
    public void confirm() {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Order is not in draft status");
        }
        status = OrderStatus.CONFIRMED;
        domainEvents.add(new OrderConfirmedEvent(id, customerId, calculateTotal()));
    }
    
    public List getDomainEvents() {
        return Collections.unmodifiableList(domainEvents);
    }
    
    public void clearDomainEvents() {
        domainEvents.clear();
    }
}

Event Sourcing

Pro Tip: Event sourcing stores all changes to an application state as a sequence of events.

Event Sourcing Implementation


@Entity
public class OrderEvent {
    @Id
    @GeneratedValue
    private Long id;
    
    private OrderId orderId;
    private String eventType;
    private String eventData;
    private LocalDateTime timestamp;
}

@Service
public class OrderEventSourcing {
    @Autowired
    private OrderEventRepository eventRepository;
    
    public Order reconstructOrder(OrderId orderId) {
        List events = eventRepository.findByOrderIdOrderByTimestamp(orderId);
        Order order = new Order(orderId);
        
        for (OrderEvent event : events) {
            applyEvent(order, event);
        }
        
        return order;
    }
    
    private void applyEvent(Order order, OrderEvent event) {
        switch (event.getEventType()) {
            case "ORDER_CREATED":
                applyOrderCreated(order, event);
                break;
            case "ORDER_LINE_ADDED":
                applyOrderLineAdded(order, event);
                break;
            case "ORDER_CONFIRMED":
                applyOrderConfirmed(order, event);
                break;
        }
    }
}

CQRS Pattern

Note: CQRS separates read and write operations for better scalability and performance.

CQRS Implementation


// Command
public class CreateOrderCommand {
    private final CustomerId customerId;
    private final List orderLines;
}

// Query
public class OrderSummaryQuery {
    private final OrderId orderId;
}

// Command Handler
@Service
public class CreateOrderCommandHandler {
    @Autowired
    private OrderRepository orderRepository;
    
    public OrderId handle(CreateOrderCommand command) {
        Order order = new Order(command.getCustomerId());
        command.getOrderLines().forEach(line ->
            order.addOrderLine(line.getProductId(), line.getQuantity())
        );
        return orderRepository.save(order).getId();
    }
}

// Query Handler
@Service
public class OrderSummaryQueryHandler {
    @Autowired
    private OrderSummaryRepository summaryRepository;
    
    public OrderSummary handle(OrderSummaryQuery query) {
        return summaryRepository.findById(query.getOrderId())
            .orElseThrow(() -> new OrderNotFoundException(query.getOrderId()));
    }
}

Best Practices

Pro Tip: Following DDD best practices ensures maintainable and scalable domain models.

Best Practices Summary

  • Keep aggregates small and focused
  • Use value objects for immutable concepts
  • Implement proper domain events
  • Maintain clear bounded contexts
  • Use ubiquitous language consistently
  • Implement proper validation
  • Handle concurrency properly
  • Use appropriate persistence strategies

Conclusion

Domain-Driven Design provides a powerful approach to building complex software systems. By understanding and implementing advanced DDD patterns, developers can create more maintainable and scalable applications that better reflect the business domain.