Domain-Driven Design is an approach to software development that focuses on modeling software to match the business domain. This guide covers DDD fundamentals with practical Java implementations.
1. Core DDD Concepts and Building Blocks
Domain Model Foundation
import java.time.LocalDateTime;
import java.util.*;
// Value Object - Immutable and identified by its attributes
public final class Money implements ValueObject {
private final double amount;
private final String currency;
public Money(double amount, String currency) {
if (amount < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
if (currency == null || currency.trim().isEmpty()) {
throw new IllegalArgumentException("Currency cannot be empty");
}
this.amount = amount;
this.currency = currency.toUpperCase();
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add different currencies");
}
return new Money(this.amount + other.amount, this.currency);
}
public Money subtract(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot subtract different currencies");
}
return new Money(this.amount - other.amount, this.currency);
}
// Value objects are equal if all attributes are equal
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return Double.compare(money.amount, amount) == 0 &&
currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
@Override
public String toString() {
return String.format("%.2f %s", amount, currency);
}
// Getters
public double getAmount() { return amount; }
public String getCurrency() { return currency; }
}
// Entity - Has a unique identity and lifecycle
public class Customer implements Entity<CustomerId> {
private CustomerId id;
private String name;
private Email email;
private CustomerStatus status;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public Customer(CustomerId id, String name, Email email) {
this.id = Objects.requireNonNull(id, "Customer ID cannot be null");
this.name = validateName(name);
this.email = Objects.requireNonNull(email, "Email cannot be null");
this.status = CustomerStatus.ACTIVE;
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
private String validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Customer name cannot be empty");
}
if (name.length() < 2 || name.length() > 100) {
throw new IllegalArgumentException("Customer name must be between 2 and 100 characters");
}
return name.trim();
}
public void changeName(String newName) {
this.name = validateName(newName);
this.updatedAt = LocalDateTime.now();
}
public void changeEmail(Email newEmail) {
this.email = Objects.requireNonNull(newEmail, "Email cannot be null");
this.updatedAt = LocalDateTime.now();
}
public void deactivate() {
this.status = CustomerStatus.INACTIVE;
this.updatedAt = LocalDateTime.now();
}
public void activate() {
this.status = CustomerStatus.ACTIVE;
this.updatedAt = LocalDateTime.now();
}
// Entities are equal if their IDs are equal
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Customer customer = (Customer) o;
return id.equals(customer.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
// Getters
@Override
public CustomerId getId() { return id; }
public String getName() { return name; }
public Email getEmail() { return email; }
public CustomerStatus getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
// Domain Service - Stateless operation that doesn't fit in an Entity or Value Object
public interface CustomerRegistrationService {
Customer registerNewCustomer(String name, Email email, String password);
void sendWelcomeEmail(Customer customer);
}
// Repository interface - Collection-like interface for accessing domain objects
public interface CustomerRepository {
Customer findById(CustomerId id);
List<Customer> findByName(String name);
Customer findByEmail(Email email);
void save(Customer customer);
void delete(CustomerId id);
boolean existsByEmail(Email email);
}
// Supporting classes
public class CustomerId implements ValueObject {
private final String value;
public CustomerId(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Customer ID cannot be empty");
}
this.value = value.trim();
}
public static CustomerId generate() {
return new CustomerId("CUST-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomerId that = (CustomerId) o;
return value.equals(that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}
public class Email implements ValueObject {
private final String value;
public Email(String value) {
if (!isValidEmail(value)) {
throw new IllegalArgumentException("Invalid email address: " + value);
}
this.value = value.toLowerCase().trim();
}
private boolean isValidEmail(String email) {
if (email == null || email.trim().isEmpty()) return false;
String emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$";
return email.matches(emailRegex);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Email email = (Email) o;
return value.equals(email.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}
enum CustomerStatus {
ACTIVE, INACTIVE, SUSPENDED
}
// Marker interfaces
interface ValueObject {}
interface Entity<T> {
T getId();
}
2. Aggregate Root and Domain Events
Order Management Domain
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.*;
// Domain Event
interface DomainEvent {
LocalDateTime occurredOn();
String getEventType();
}
class OrderCreatedEvent implements DomainEvent {
private final OrderId orderId;
private final CustomerId customerId;
private final Money totalAmount;
private final LocalDateTime occurredOn;
public OrderCreatedEvent(OrderId orderId, CustomerId customerId, Money totalAmount) {
this.orderId = orderId;
this.customerId = customerId;
this.totalAmount = totalAmount;
this.occurredOn = LocalDateTime.now();
}
public OrderId getOrderId() { return orderId; }
public CustomerId getCustomerId() { return customerId; }
public Money getTotalAmount() { return totalAmount; }
@Override
public LocalDateTime occurredOn() { return occurredOn; }
@Override
public String getEventType() { return "ORDER_CREATED"; }
}
class OrderShippedEvent implements DomainEvent {
private final OrderId orderId;
private final LocalDateTime shippedDate;
private final LocalDateTime occurredOn;
public OrderShippedEvent(OrderId orderId, LocalDateTime shippedDate) {
this.orderId = orderId;
this.shippedDate = shippedDate;
this.occurredOn = LocalDateTime.now();
}
public OrderId getOrderId() { return orderId; }
public LocalDateTime getShippedDate() { return shippedDate; }
@Override
public LocalDateTime occurredOn() { return occurredOn; }
@Override
public String getEventType() { return "ORDER_SHIPPED"; }
}
// Aggregate Root - Order
public class Order implements AggregateRoot<OrderId> {
private OrderId id;
private CustomerId customerId;
private OrderStatus status;
private Address shippingAddress;
private Money totalAmount;
private List<OrderLine> orderLines;
private List<DomainEvent> domainEvents;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// Factory method for creating new orders
public static Order create(CustomerId customerId, Address shippingAddress) {
OrderId orderId = OrderId.generate();
Order order = new Order(orderId, customerId, shippingAddress);
// Record domain event
order.recordEvent(new OrderCreatedEvent(orderId, customerId, new Money(0, "USD")));
return order;
}
private Order(OrderId id, CustomerId customerId, Address shippingAddress) {
this.id = Objects.requireNonNull(id, "Order ID cannot be null");
this.customerId = Objects.requireNonNull(customerId, "Customer ID cannot be null");
this.shippingAddress = Objects.requireNonNull(shippingAddress, "Shipping address cannot be null");
this.status = OrderStatus.DRAFT;
this.totalAmount = new Money(0, "USD");
this.orderLines = new ArrayList<>();
this.domainEvents = new ArrayList<>();
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
// Business logic methods
public void addItem(Product product, int quantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot add items to order in status: " + status);
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
// Check if product already exists in order
Optional<OrderLine> existingLine = orderLines.stream()
.filter(line -> line.getProductId().equals(product.getId()))
.findFirst();
if (existingLine.isPresent()) {
existingLine.get().increaseQuantity(quantity);
} else {
OrderLine newLine = new OrderLine(OrderLineId.generate(), product.getId(),
product.getPrice(), quantity);
orderLines.add(newLine);
}
recalculateTotal();
this.updatedAt = LocalDateTime.now();
}
public void removeItem(ProductId productId) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot remove items from order in status: " + status);
}
boolean removed = orderLines.removeIf(line -> line.getProductId().equals(productId));
if (removed) {
recalculateTotal();
this.updatedAt = LocalDateTime.now();
}
}
public void updateItemQuantity(ProductId productId, int newQuantity) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot update items in order in status: " + status);
}
if (newQuantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
orderLines.stream()
.filter(line -> line.getProductId().equals(productId))
.findFirst()
.ifPresent(line -> {
line.updateQuantity(newQuantity);
recalculateTotal();
this.updatedAt = LocalDateTime.now();
});
}
public void submit() {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Order cannot be submitted from status: " + status);
}
if (orderLines.isEmpty()) {
throw new IllegalStateException("Cannot submit empty order");
}
this.status = OrderStatus.SUBMITTED;
this.updatedAt = LocalDateTime.now();
}
public void ship() {
if (status != OrderStatus.SUBMITTED) {
throw new IllegalStateException("Order cannot be shipped from status: " + status);
}
this.status = OrderStatus.SHIPPED;
this.updatedAt = LocalDateTime.now();
// Record domain event
recordEvent(new OrderShippedEvent(id, LocalDateTime.now()));
}
public void cancel() {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
throw new IllegalStateException("Cannot cancel order in status: " + status);
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = LocalDateTime.now();
}
private void recalculateTotal() {
Money total = orderLines.stream()
.map(OrderLine::getLineTotal)
.reduce(new Money(0, "USD"), Money::add);
this.totalAmount = total;
}
// Domain event methods
private void recordEvent(DomainEvent event) {
this.domainEvents.add(event);
}
public List<DomainEvent> getDomainEvents() {
return new ArrayList<>(domainEvents);
}
public void clearEvents() {
this.domainEvents.clear();
}
// Validation methods
public boolean canBeModified() {
return status == OrderStatus.DRAFT;
}
public boolean isShippable() {
return status == OrderStatus.SUBMITTED;
}
// Getters
@Override
public OrderId getId() { return id; }
public CustomerId getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public Address getShippingAddress() { return shippingAddress; }
public Money getTotalAmount() { return totalAmount; }
public List<OrderLine> getOrderLines() { return new ArrayList<>(orderLines); }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
// Order Line Entity (part of Order aggregate)
class OrderLine implements Entity<OrderLineId> {
private OrderLineId id;
private ProductId productId;
private Money unitPrice;
private int quantity;
public OrderLine(OrderLineId id, ProductId productId, Money unitPrice, int quantity) {
this.id = Objects.requireNonNull(id);
this.productId = Objects.requireNonNull(productId);
this.unitPrice = Objects.requireNonNull(unitPrice);
this.quantity = validateQuantity(quantity);
}
private int validateQuantity(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
return quantity;
}
public void increaseQuantity(int additionalQuantity) {
this.quantity += validateQuantity(additionalQuantity);
}
public void updateQuantity(int newQuantity) {
this.quantity = validateQuantity(newQuantity);
}
public Money getLineTotal() {
return new Money(unitPrice.getAmount() * quantity, unitPrice.getCurrency());
}
@Override
public OrderLineId getId() { return id; }
public ProductId getProductId() { return productId; }
public Money getUnitPrice() { return unitPrice; }
public int getQuantity() { return quantity; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderLine orderLine = (OrderLine) o;
return id.equals(orderLine.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
// Supporting classes
class OrderId implements ValueObject {
private final String value;
public OrderId(String value) {
this.value = validateValue(value);
}
public static OrderId generate() {
return new OrderId("ORD-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase());
}
private String validateValue(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Order ID cannot be empty");
}
return value.trim();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderId orderId = (OrderId) o;
return value.equals(orderId.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}
class OrderLineId implements ValueObject {
private final String value;
public OrderLineId(String value) {
this.value = validateValue(value);
}
public static OrderLineId generate() {
return new OrderLineId("OL-" + UUID.randomUUID().toString().substring(0, 6).toUpperCase());
}
private String validateValue(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("OrderLine ID cannot be empty");
}
return value.trim();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OrderLineId that = (OrderLineId) o;
return value.equals(that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}
class ProductId implements ValueObject {
private final String value;
public ProductId(String value) {
this.value = validateValue(value);
}
private String validateValue(String value) {
if (value == null || value.trim().isEmpty()) {
throw new IllegalArgumentException("Product ID cannot be empty");
}
return value.trim();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ProductId that = (ProductId) o;
return value.equals(that.value);
}
@Override
public int hashCode() {
return Objects.hash(value);
}
@Override
public String toString() {
return value;
}
}
class Address implements ValueObject {
private final String street;
private final String city;
private final String state;
private final String zipCode;
private final String country;
public Address(String street, String city, String state, String zipCode, String country) {
this.street = validateStreet(street);
this.city = validateCity(city);
this.state = validateState(state);
this.zipCode = validateZipCode(zipCode);
this.country = validateCountry(country);
}
private String validateStreet(String street) {
if (street == null || street.trim().isEmpty()) {
throw new IllegalArgumentException("Street cannot be empty");
}
return street.trim();
}
private String validateCity(String city) {
if (city == null || city.trim().isEmpty()) {
throw new IllegalArgumentException("City cannot be empty");
}
return city.trim();
}
private String validateState(String state) {
if (state == null || state.trim().isEmpty()) {
throw new IllegalArgumentException("State cannot be empty");
}
return state.trim();
}
private String validateZipCode(String zipCode) {
if (zipCode == null || zipCode.trim().isEmpty()) {
throw new IllegalArgumentException("Zip code cannot be empty");
}
return zipCode.trim();
}
private String validateCountry(String country) {
if (country == null || country.trim().isEmpty()) {
throw new IllegalArgumentException("Country cannot be empty");
}
return country.trim();
}
@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) &&
state.equals(address.state) &&
zipCode.equals(address.zipCode) &&
country.equals(address.country);
}
@Override
public int hashCode() {
return Objects.hash(street, city, state, zipCode, country);
}
@Override
public String toString() {
return String.format("%s, %s, %s %s, %s", street, city, state, zipCode, country);
}
}
enum OrderStatus {
DRAFT, SUBMITTED, SHIPPED, DELIVERED, CANCELLED
}
// Product entity (simplified)
class Product implements Entity<ProductId> {
private ProductId id;
private String name;
private String description;
private Money price;
private int stockQuantity;
public Product(ProductId id, String name, String description, Money price, int stockQuantity) {
this.id = Objects.requireNonNull(id);
this.name = validateName(name);
this.description = description;
this.price = Objects.requireNonNull(price);
this.stockQuantity = stockQuantity;
}
private String validateName(String name) {
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Product name cannot be empty");
}
return name.trim();
}
public void updatePrice(Money newPrice) {
this.price = Objects.requireNonNull(newPrice);
}
public void updateStock(int newStock) {
this.stockQuantity = newStock;
}
@Override
public ProductId getId() { return id; }
public String getName() { return name; }
public String getDescription() { return description; }
public Money getPrice() { return price; }
public int getStockQuantity() { return stockQuantity; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Product product = (Product) o;
return id.equals(product.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
interface AggregateRoot<T> extends Entity<T> {
// Marker interface for aggregate roots
}
3. Domain Services and Repositories
Domain Services Implementation
import java.util.List;
import java.util.Optional;
// Domain Service - Order Management
public interface OrderService {
Order createOrder(CustomerId customerId, Address shippingAddress);
void addItemToOrder(OrderId orderId, Product product, int quantity);
void submitOrder(OrderId orderId);
void cancelOrder(OrderId orderId);
OrderSummary getOrderSummary(OrderId orderId);
}
// Implementation of Order Service
public class OrderServiceImpl implements OrderService {
private final OrderRepository orderRepository;
private final ProductRepository productRepository;
private final DomainEventPublisher eventPublisher;
public OrderServiceImpl(OrderRepository orderRepository,
ProductRepository productRepository,
DomainEventPublisher eventPublisher) {
this.orderRepository = orderRepository;
this.productRepository = productRepository;
this.eventPublisher = eventPublisher;
}
@Override
public Order createOrder(CustomerId customerId, Address shippingAddress) {
Order order = Order.create(customerId, shippingAddress);
orderRepository.save(order);
return order;
}
@Override
public void addItemToOrder(OrderId orderId, Product product, int quantity) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
// Check product availability
if (product.getStockQuantity() < quantity) {
throw new InsufficientStockException(
"Insufficient stock for product: " + product.getName() +
". Available: " + product.getStockQuantity() + ", Requested: " + quantity
);
}
order.addItem(product, quantity);
orderRepository.save(order);
}
@Override
public void submitOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
order.submit();
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
}
@Override
public void cancelOrder(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
order.cancel();
orderRepository.save(order);
// Publish domain events
publishDomainEvents(order);
}
@Override
public OrderSummary getOrderSummary(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
return new OrderSummary(
order.getId(),
order.getCustomerId(),
order.getStatus(),
order.getTotalAmount(),
order.getOrderLines().size(),
order.getCreatedAt()
);
}
private void publishDomainEvents(Order order) {
order.getDomainEvents().forEach(eventPublisher::publish);
order.clearEvents();
}
}
// Repository interfaces
public interface OrderRepository {
Optional<Order> findById(OrderId id);
List<Order> findByCustomerId(CustomerId customerId);
List<Order> findByStatus(OrderStatus status);
void save(Order order);
void delete(OrderId id);
boolean exists(OrderId id);
}
public interface ProductRepository {
Optional<Product> findById(ProductId id);
List<Product> findByName(String name);
List<Product> findByPriceRange(Money minPrice, Money maxPrice);
void save(Product product);
void updateStock(ProductId productId, int newStock);
}
// Domain Event Publisher
public interface DomainEventPublisher {
void publish(DomainEvent event);
}
// Value Objects for service results
public class OrderSummary implements ValueObject {
private final OrderId orderId;
private final CustomerId customerId;
private final OrderStatus status;
private final Money totalAmount;
private final int itemCount;
private final LocalDateTime createdAt;
public OrderSummary(OrderId orderId, CustomerId customerId, OrderStatus status,
Money totalAmount, int itemCount, LocalDateTime createdAt) {
this.orderId = orderId;
this.customerId = customerId;
this.status = status;
this.totalAmount = totalAmount;
this.itemCount = itemCount;
this.createdAt = createdAt;
}
// Getters
public OrderId getOrderId() { return orderId; }
public CustomerId getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public Money getTotalAmount() { return totalAmount; }
public int getItemCount() { return itemCount; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
// Custom exceptions
class OrderNotFoundException extends RuntimeException {
public OrderNotFoundException(String message) {
super(message);
}
}
class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String message) {
super(message);
}
}
// Inventory Management Domain Service
public interface InventoryService {
void reserveStock(ProductId productId, int quantity);
void releaseStock(ProductId productId, int quantity);
void updateStock(ProductId productId, int newQuantity);
int getAvailableStock(ProductId productId);
}
public class InventoryServiceImpl implements InventoryService {
private final ProductRepository productRepository;
public InventoryServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public void reserveStock(ProductId productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException("Product not found: " + productId));
if (product.getStockQuantity() < quantity) {
throw new InsufficientStockException(
"Cannot reserve " + quantity + " items. Available: " + product.getStockQuantity()
);
}
product.updateStock(product.getStockQuantity() - quantity);
productRepository.save(product);
}
@Override
public void releaseStock(ProductId productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException("Product not found: " + productId));
product.updateStock(product.getStockQuantity() + quantity);
productRepository.save(product);
}
@Override
public void updateStock(ProductId productId, int newQuantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException("Product not found: " + productId));
if (newQuantity < 0) {
throw new IllegalArgumentException("Stock quantity cannot be negative");
}
product.updateStock(newQuantity);
productRepository.save(product);
}
@Override
public int getAvailableStock(ProductId productId) {
return productRepository.findById(productId)
.map(Product::getStockQuantity)
.orElse(0);
}
}
class ProductNotFoundException extends RuntimeException {
public ProductNotFoundException(String message) {
super(message);
}
}
4. Application Service and Factories
Application Layer Implementation
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
// Application Service - Coordinates domain objects for use cases
@Service
@Transactional
public class OrderApplicationService {
private final OrderService orderService;
private final InventoryService inventoryService;
private final ProductRepository productRepository;
private final CustomerRepository customerRepository;
private final DomainEventPublisher eventPublisher;
public OrderApplicationService(OrderService orderService,
InventoryService inventoryService,
ProductRepository productRepository,
CustomerRepository customerRepository,
DomainEventPublisher eventPublisher) {
this.orderService = orderService;
this.inventoryService = inventoryService;
this.productRepository = productRepository;
this.customerRepository = customerRepository;
this.eventPublisher = eventPublisher;
}
// Use case: Create new order
public OrderId createOrder(CreateOrderCommand command) {
// Validate customer exists
Customer customer = customerRepository.findById(command.getCustomerId())
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + command.getCustomerId()));
// Create order
Order order = orderService.createOrder(command.getCustomerId(), command.getShippingAddress());
return order.getId();
}
// Use case: Add item to order
public void addItemToOrder(AddItemCommand command) {
// Validate product exists and get current data
Product product = productRepository.findById(command.getProductId())
.orElseThrow(() -> new ProductNotFoundException("Product not found: " + command.getProductId()));
// Reserve stock
inventoryService.reserveStock(command.getProductId(), command.getQuantity());
try {
// Add item to order
orderService.addItemToOrder(command.getOrderId(), product, command.getQuantity());
} catch (Exception e) {
// Release reserved stock if operation fails
inventoryService.releaseStock(command.getProductId(), command.getQuantity());
throw e;
}
}
// Use case: Submit order
public void submitOrder(OrderId orderId) {
Order order = orderService.submitOrder(orderId);
// Additional business logic after order submission
// (e.g., notify shipping department, update analytics, etc.)
}
// Use case: Cancel order
public void cancelOrder(CancelOrderCommand command) {
Order order = orderService.cancelOrder(command.getOrderId());
// Release reserved stock
order.getOrderLines().forEach(line -> {
inventoryService.releaseStock(line.getProductId(), line.getQuantity());
});
}
// Use case: Get order details
@Transactional(readOnly = true)
public OrderDetails getOrderDetails(OrderId orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException("Order not found: " + orderId));
Customer customer = customerRepository.findById(order.getCustomerId())
.orElseThrow(() -> new CustomerNotFoundException("Customer not found: " + order.getCustomerId()));
return OrderDetailsFactory.create(order, customer);
}
}
// Command objects (input DTOs)
class CreateOrderCommand {
private final CustomerId customerId;
private final Address shippingAddress;
public CreateOrderCommand(CustomerId customerId, Address shippingAddress) {
this.customerId = customerId;
this.shippingAddress = shippingAddress;
}
public CustomerId getCustomerId() { return customerId; }
public Address getShippingAddress() { return shippingAddress; }
}
class AddItemCommand {
private final OrderId orderId;
private final ProductId productId;
private final int quantity;
public AddItemCommand(OrderId orderId, ProductId productId, int quantity) {
this.orderId = orderId;
this.productId = productId;
this.quantity = quantity;
}
public OrderId getOrderId() { return orderId; }
public ProductId getProductId() { return productId; }
public int getQuantity() { return quantity; }
}
class CancelOrderCommand {
private final OrderId orderId;
private final String reason;
public CancelOrderCommand(OrderId orderId, String reason) {
this.orderId = orderId;
this.reason = reason;
}
public OrderId getOrderId() { return orderId; }
public String getReason() { return reason; }
}
// Factory for creating complex objects
class OrderDetailsFactory {
public static OrderDetails create(Order order, Customer customer) {
return new OrderDetails(
order.getId(),
customer.getName(),
customer.getEmail(),
order.getStatus(),
order.getTotalAmount(),
order.getShippingAddress(),
order.getOrderLines(),
order.getCreatedAt(),
order.getUpdatedAt()
);
}
}
// Output DTO
class OrderDetails {
private final OrderId orderId;
private final String customerName;
private final Email customerEmail;
private final OrderStatus status;
private final Money totalAmount;
private final Address shippingAddress;
private final List<OrderLine> orderLines;
private final LocalDateTime createdAt;
private final LocalDateTime updatedAt;
public OrderDetails(OrderId orderId, String customerName, Email customerEmail,
OrderStatus status, Money totalAmount, Address shippingAddress,
List<OrderLine> orderLines, LocalDateTime createdAt, LocalDateTime updatedAt) {
this.orderId = orderId;
this.customerName = customerName;
this.customerEmail = customerEmail;
this.status = status;
this.totalAmount = totalAmount;
this.shippingAddress = shippingAddress;
this.orderLines = new ArrayList<>(orderLines);
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// Getters
public OrderId getOrderId() { return orderId; }
public String getCustomerName() { return customerName; }
public Email getCustomerEmail() { return customerEmail; }
public OrderStatus getStatus() { return status; }
public Money getTotalAmount() { return totalAmount; }
public Address getShippingAddress() { return shippingAddress; }
public List<OrderLine> getOrderLines() { return new ArrayList<>(orderLines); }
public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
}
class CustomerNotFoundException extends RuntimeException {
public CustomerNotFoundException(String message) {
super(message);
}
}
5. Demo Client Code
Complete DDD System Demonstration
public class DDDSystemDemo {
public static void main(String[] args) {
// Setup (in real app, this would be done by Dependency Injection)
CustomerRepository customerRepository = new InMemoryCustomerRepository();
OrderRepository orderRepository = new InMemoryOrderRepository();
ProductRepository productRepository = new InMemoryProductRepository();
DomainEventPublisher eventPublisher = new ConsoleEventPublisher();
OrderService orderService = new OrderServiceImpl(orderRepository, productRepository, eventPublisher);
InventoryService inventoryService = new InventoryServiceImpl(productRepository);
OrderApplicationService orderAppService = new OrderApplicationService(
orderService, inventoryService, productRepository, customerRepository, eventPublisher
);
// Create sample data
CustomerId customerId = CustomerId.generate();
Customer customer = new Customer(customerId, "John Doe", new Email("[email protected]"));
customerRepository.save(customer);
ProductId product1Id = new ProductId("PROD-001");
Product product1 = new Product(product1Id, "Laptop", "High-performance laptop",
new Money(999.99, "USD"), 10);
productRepository.save(product1);
ProductId product2Id = new ProductId("PROD-002");
Product product2 = new Product(product2Id, "Mouse", "Wireless mouse",
new Money(29.99, "USD"), 50);
productRepository.save(product2);
// Demo order creation and processing
System.out.println("=== DDD System Demo ===");
// 1. Create order
Address shippingAddress = new Address("123 Main St", "New York", "NY", "10001", "USA");
CreateOrderCommand createCmd = new CreateOrderCommand(customerId, shippingAddress);
OrderId orderId = orderAppService.createOrder(createCmd);
System.out.println("Created order: " + orderId);
// 2. Add items to order
AddItemCommand addItem1 = new AddItemCommand(orderId, product1Id, 1);
orderAppService.addItemToOrder(addItem1);
AddItemCommand addItem2 = new AddItemCommand(orderId, product2Id, 2);
orderAppService.addItemToOrder(addItem2);
// 3. Submit order
orderAppService.submitOrder(orderId);
System.out.println("Order submitted successfully");
// 4. Get order details
OrderDetails orderDetails = orderAppService.getOrderDetails(orderId);
System.out.println("\n=== Order Details ===");
System.out.println("Customer: " + orderDetails.getCustomerName());
System.out.println("Total: " + orderDetails.getTotalAmount());
System.out.println("Status: " + orderDetails.getStatus());
System.out.println("Items: " + orderDetails.getOrderLines().size());
// 5. Check inventory
int remainingStock = inventoryService.getAvailableStock(product1Id);
System.out.println("Remaining laptop stock: " + remainingStock);
}
}
// Simple in-memory implementations for demo
class InMemoryCustomerRepository implements CustomerRepository {
private final Map<CustomerId, Customer> store = new HashMap<>();
@Override
public Customer findById(CustomerId id) {
return store.get(id);
}
@Override
public List<Customer> findByName(String name) {
return store.values().stream()
.filter(c -> c.getName().equalsIgnoreCase(name))
.collect(Collectors.toList());
}
@Override
public Customer findByEmail(Email email) {
return store.values().stream()
.filter(c -> c.getEmail().equals(email))
.findFirst()
.orElse(null);
}
@Override
public void save(Customer customer) {
store.put(customer.getId(), customer);
}
@Override
public void delete(CustomerId id) {
store.remove(id);
}
@Override
public boolean existsByEmail(Email email) {
return store.values().stream().anyMatch(c -> c.getEmail().equals(email));
}
}
class InMemoryOrderRepository implements OrderRepository {
private final Map<OrderId, Order> store = new HashMap<>();
@Override
public Optional<Order> findById(OrderId id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Order> findByCustomerId(CustomerId customerId) {
return store.values().stream()
.filter(o -> o.getCustomerId().equals(customerId))
.collect(Collectors.toList());
}
@Override
public List<Order> findByStatus(OrderStatus status) {
return store.values().stream()
.filter(o -> o.getStatus() == status)
.collect(Collectors.toList());
}
@Override
public void save(Order order) {
store.put(order.getId(), order);
}
@Override
public void delete(OrderId id) {
store.remove(id);
}
@Override
public boolean exists(OrderId id) {
return store.containsKey(id);
}
}
class InMemoryProductRepository implements ProductRepository {
private final Map<ProductId, Product> store = new HashMap<>();
@Override
public Optional<Product> findById(ProductId id) {
return Optional.ofNullable(store.get(id));
}
@Override
public List<Product> findByName(String name) {
return store.values().stream()
.filter(p -> p.getName().equalsIgnoreCase(name))
.collect(Collectors.toList());
}
@Override
public List<Product> findByPriceRange(Money minPrice, Money maxPrice) {
return store.values().stream()
.filter(p -> p.getPrice().getAmount() >= minPrice.getAmount() &&
p.getPrice().getAmount() <= maxPrice.getAmount() &&
p.getPrice().getCurrency().equals(minPrice.getCurrency()))
.collect(Collectors.toList());
}
@Override
public void save(Product product) {
store.put(product.getId(), product);
}
@Override
public void updateStock(ProductId productId, int newStock) {
Product product = store.get(productId);
if (product != null) {
product.updateStock(newStock);
}
}
}
class ConsoleEventPublisher implements DomainEventPublisher {
@Override
public void publish(DomainEvent event) {
System.out.println("📢 Domain Event Published: " + event.getEventType() +
" at " + event.occurredOn());
}
}
Key DDD Concepts Covered
- Ubiquitous Language: Using domain terms in code
- Bounded Contexts: Clear boundaries between different parts of the system
- Entities: Objects with identity and lifecycle
- Value Objects: Immutable objects identified by their attributes
- Aggregates: Clusters of associated objects treated as a unit
- Domain Events: Capture things that happened in the domain
- Repositories: Abstraction for persistence
- Domain Services: Operations that don't fit in Entities/VO
- Application Services: Coordinate domain objects for use cases
- Factories: Complex object creation logic
This comprehensive DDD implementation demonstrates how to structure a Java application following Domain-Driven Design principles, focusing on the business domain and maintaining clear separation of concerns.