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

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.
Table of Contents
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.