Learn how to implement Domain-Driven Design (DDD) Bounded Contexts in Java with practical examples and modern architectures.
Table of Contents
- Bounded Contexts Fundamentals
- Strategic Design Patterns
- E-Commerce Case Study
- Context Mapping
- Implementation Architecture
- Communication Patterns
- Testing Strategies
Bounded Contexts Fundamentals
What are Bounded Contexts?
Bounded Contexts define explicit boundaries within which a particular domain model applies. Each context has its own:
- Ubiquitous Language
- Domain Models
- Business Rules
- Data Persistence
Key Principles:
- Explicit Boundaries: Clear separation between contexts
- Autonomy: Each context can evolve independently
- Integration: Well-defined communication patterns
- Consistency Boundaries: Transactions within single context
Strategic Design Patterns
1. Core Domain Models
// Shared Kernel - Common types used across contexts
public class SharedKernel {
// Common value objects
public record Money(BigDecimal amount, Currency currency) {
public Money {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("Amount cannot be negative");
}
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount.add(other.amount), this.currency);
}
}
public enum Currency {
USD, EUR, GBP, JPY
}
public record Address(
String street,
String city,
String state,
String zipCode,
String country
) {
public Address {
if (street == null || street.isBlank()) {
throw new IllegalArgumentException("Street is required");
}
if (city == null || city.isBlank()) {
throw new IllegalArgumentException("City is required");
}
}
}
// Common identifiers
public record CustomerId(String value) {
public CustomerId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Customer ID cannot be empty");
}
}
}
public record OrderId(String value) {
public OrderId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Order ID cannot be empty");
}
}
}
}
E-Commerce Case Study
2. Bounded Context: Identity & Access
// Identity Context - Manages users, authentication, and authorization
public class IdentityContext {
// Core Domain Models
public record UserId(String value) {
public UserId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("User ID cannot be empty");
}
}
}
public enum UserStatus {
PENDING_VERIFICATION, ACTIVE, SUSPENDED, DELETED
}
public enum UserRole {
CUSTOMER, SELLER, ADMIN, SUPPORT
}
// Aggregate Root
public static class User {
private final UserId id;
private String email;
private String passwordHash;
private UserStatus status;
private Set<UserRole> roles;
private final Instant createdAt;
private Instant updatedAt;
public User(UserId id, String email, String passwordHash, Set<UserRole> roles) {
this.id = id;
this.email = email;
this.passwordHash = passwordHash;
this.roles = new HashSet<>(roles);
this.status = UserStatus.PENDING_VERIFICATION;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
validate();
}
// Domain methods
public void verifyEmail() {
if (this.status != UserStatus.PENDING_VERIFICATION) {
throw new IllegalStateException("User is not pending verification");
}
this.status = UserStatus.ACTIVE;
this.updatedAt = Instant.now();
}
public void suspend() {
this.status = UserStatus.SUSPENDED;
this.updatedAt = Instant.now();
}
public void addRole(UserRole role) {
this.roles.add(role);
this.updatedAt = Instant.now();
}
public boolean hasRole(UserRole role) {
return this.roles.contains(role);
}
private void validate() {
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Invalid email address");
}
if (passwordHash == null || passwordHash.isBlank()) {
throw new IllegalArgumentException("Password hash is required");
}
if (roles.isEmpty()) {
throw new IllegalArgumentException("User must have at least one role");
}
}
// Getters
public UserId getId() { return id; }
public String getEmail() { return email; }
public UserStatus getStatus() { return status; }
public Set<UserRole> getRoles() { return Set.copyOf(roles); }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
// Domain Service
public interface PasswordHasher {
String hash(String plainPassword);
boolean verify(String plainPassword, String hash);
}
public static class AuthenticationService {
private final PasswordHasher passwordHasher;
private final UserRepository userRepository;
public AuthenticationService(PasswordHasher passwordHasher, UserRepository userRepository) {
this.passwordHasher = passwordHasher;
this.userRepository = userRepository;
}
public User authenticate(String email, String plainPassword) {
var user = userRepository.findByEmail(email)
.orElseThrow(() -> new AuthenticationException("Invalid credentials"));
if (user.getStatus() != UserStatus.ACTIVE) {
throw new AuthenticationException("User account is not active");
}
if (!passwordHasher.verify(plainPassword, user.passwordHash)) {
throw new AuthenticationException("Invalid credentials");
}
return user;
}
}
// Repository interface
public interface UserRepository {
Optional<User> findById(UserId id);
Optional<User> findByEmail(String email);
void save(User user);
boolean existsByEmail(String email);
}
}
3. Bounded Context: Catalog
// Catalog Context - Manages products, categories, and inventory
public class CatalogContext {
// Value Objects
public record ProductId(String value) {
public ProductId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Product ID cannot be empty");
}
}
}
public record CategoryId(String value) {
public CategoryId {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Category ID cannot be empty");
}
}
}
public record StockQuantity(int value) {
public StockQuantity {
if (value < 0) {
throw new IllegalArgumentException("Stock quantity cannot be negative");
}
}
public StockQuantity add(StockQuantity other) {
return new StockQuantity(this.value + other.value);
}
public StockQuantity subtract(StockQuantity other) {
int result = this.value - other.value;
if (result < 0) {
throw new IllegalArgumentException("Insufficient stock");
}
return new StockQuantity(result);
}
}
// Entities
public static class Product {
private final ProductId id;
private String name;
private String description;
private SharedKernel.Money price;
private CategoryId categoryId;
private StockQuantity stockQuantity;
private boolean active;
private final Instant createdAt;
private Instant updatedAt;
public Product(ProductId id, String name, SharedKernel.Money price,
CategoryId categoryId, StockQuantity initialStock) {
this.id = id;
this.name = name;
this.price = price;
this.categoryId = categoryId;
this.stockQuantity = initialStock;
this.active = true;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
validate();
}
// Domain methods
public void updatePrice(SharedKernel.Money newPrice) {
this.price = newPrice;
this.updatedAt = Instant.now();
}
public void updateStock(StockQuantity newStock) {
this.stockQuantity = newStock;
this.updatedAt = Instant.now();
}
public void increaseStock(StockQuantity quantity) {
this.stockQuantity = this.stockQuantity.add(quantity);
this.updatedAt = Instant.now();
}
public void decreaseStock(StockQuantity quantity) {
this.stockQuantity = this.stockQuantity.subtract(quantity);
this.updatedAt = Instant.now();
}
public void deactivate() {
this.active = false;
this.updatedAt = Instant.now();
}
public void activate() {
this.active = true;
this.updatedAt = Instant.now();
}
public boolean isAvailable() {
return active && stockQuantity.value() > 0;
}
private void validate() {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Product name is required");
}
if (price == null) {
throw new IllegalArgumentException("Product price is required");
}
if (categoryId == null) {
throw new IllegalArgumentException("Category is required");
}
}
// Getters
public ProductId getId() { return id; }
public String getName() { return name; }
public SharedKernel.Money getPrice() { return price; }
public CategoryId getCategoryId() { return categoryId; }
public StockQuantity getStockQuantity() { return stockQuantity; }
public boolean isActive() { return active; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
public static class Category {
private final CategoryId id;
private String name;
private String description;
private CategoryId parentCategoryId;
private final Instant createdAt;
private Instant updatedAt;
public Category(CategoryId id, String name) {
this.id = id;
this.name = name;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
validate();
}
private void validate() {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Category name is required");
}
}
// Getters and setters
public CategoryId getId() { return id; }
public String getName() { return name; }
public String getDescription() { return description; }
public CategoryId getParentCategoryId() { return parentCategoryId; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public void setName(String name) {
this.name = name;
this.updatedAt = Instant.now();
}
public void setDescription(String description) {
this.description = description;
this.updatedAt = Instant.now();
}
public void setParentCategory(CategoryId parentCategoryId) {
this.parentCategoryId = parentCategoryId;
this.updatedAt = Instant.now();
}
}
// Domain Service
public static class ProductSearchService {
private final ProductRepository productRepository;
public ProductSearchService(ProductRepository productRepository) {
this.productRepository = productRepository;
}
public List<Product> searchActiveProducts(String query, CategoryId categoryId) {
return productRepository.findActiveProducts(query, categoryId);
}
public List<Product> findProductsInStock() {
return productRepository.findProductsInStock();
}
}
// Repository interfaces
public interface ProductRepository {
Optional<Product> findById(ProductId id);
List<Product> findActiveProducts(String query, CategoryId categoryId);
List<Product> findProductsInStock();
List<Product> findByCategory(CategoryId categoryId);
void save(Product product);
}
public interface CategoryRepository {
Optional<Category> findById(CategoryId id);
List<Category> findAll();
List<Category> findSubcategories(CategoryId parentId);
void save(Category category);
}
}
4. Bounded Context: Ordering
// Ordering Context - Manages orders, payments, and fulfillment
public class OrderingContext {
// Value Objects
public record OrderItem(
CatalogContext.ProductId productId,
String productName,
SharedKernel.Money unitPrice,
int quantity,
SharedKernel.Money subtotal
) {
public OrderItem {
if (productId == null) {
throw new IllegalArgumentException("Product ID is required");
}
if (productName == null || productName.isBlank()) {
throw new IllegalArgumentException("Product name is required");
}
if (unitPrice == null) {
throw new IllegalArgumentException("Unit price is required");
}
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be positive");
}
if (subtotal == null) {
throw new IllegalArgumentException("Subtotal is required");
}
}
public static OrderItem create(CatalogContext.ProductId productId,
String productName,
SharedKernel.Money unitPrice,
int quantity) {
SharedKernel.Money subtotal = new SharedKernel.Money(
unitPrice.amount().multiply(BigDecimal.valueOf(quantity)),
unitPrice.currency()
);
return new OrderItem(productId, productName, unitPrice, quantity, subtotal);
}
}
public enum OrderStatus {
DRAFT, PENDING_PAYMENT, CONFIRMED, PROCESSING, SHIPPED, DELIVERED, CANCELLED
}
public enum PaymentStatus {
PENDING, PROCESSING, COMPLETED, FAILED, REFUNDED
}
// Aggregate Root
public static class Order {
private final SharedKernel.OrderId id;
private final SharedKernel.CustomerId customerId;
private OrderStatus status;
private final List<OrderItem> items;
private SharedKernel.Money totalAmount;
private SharedKernel.Address shippingAddress;
private SharedKernel.Address billingAddress;
private PaymentStatus paymentStatus;
private final Instant createdAt;
private Instant updatedAt;
public Order(SharedKernel.OrderId id, SharedKernel.CustomerId customerId,
SharedKernel.Address shippingAddress, SharedKernel.Address billingAddress) {
this.id = id;
this.customerId = customerId;
this.status = OrderStatus.DRAFT;
this.items = new ArrayList<>();
this.totalAmount = new SharedKernel.Money(BigDecimal.ZERO, SharedKernel.Currency.USD);
this.shippingAddress = shippingAddress;
this.billingAddress = billingAddress;
this.paymentStatus = PaymentStatus.PENDING;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
validate();
}
// Domain methods
public void addItem(OrderItem item) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify order in status: " + status);
}
items.add(item);
recalculateTotal();
this.updatedAt = Instant.now();
}
public void removeItem(OrderItem item) {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Cannot modify order in status: " + status);
}
items.remove(item);
recalculateTotal();
this.updatedAt = Instant.now();
}
public void submit() {
if (status != OrderStatus.DRAFT) {
throw new IllegalStateException("Order is not in draft status");
}
if (items.isEmpty()) {
throw new IllegalStateException("Cannot submit empty order");
}
this.status = OrderStatus.PENDING_PAYMENT;
this.updatedAt = Instant.now();
}
public void confirmPayment() {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("Order is not pending payment");
}
this.status = OrderStatus.CONFIRMED;
this.paymentStatus = PaymentStatus.COMPLETED;
this.updatedAt = Instant.now();
}
public void cancel() {
if (status == OrderStatus.SHIPPED || status == OrderStatus.DELIVERED) {
throw new IllegalStateException("Cannot cancel shipped or delivered order");
}
this.status = OrderStatus.CANCELLED;
this.updatedAt = Instant.now();
}
public void markAsShipped() {
if (status != OrderStatus.CONFIRMED) {
throw new IllegalStateException("Order must be confirmed before shipping");
}
this.status = OrderStatus.SHIPPED;
this.updatedAt = Instant.now();
}
public void markAsDelivered() {
if (status != OrderStatus.SHIPPED) {
throw new IllegalStateException("Order must be shipped before delivery");
}
this.status = OrderStatus.DELIVERED;
this.updatedAt = Instant.now();
}
private void recalculateTotal() {
BigDecimal total = items.stream()
.map(item -> item.subtotal().amount())
.reduce(BigDecimal.ZERO, BigDecimal::add);
this.totalAmount = new SharedKernel.Money(total, totalAmount.currency());
}
private void validate() {
if (customerId == null) {
throw new IllegalArgumentException("Customer ID is required");
}
if (shippingAddress == null) {
throw new IllegalArgumentException("Shipping address is required");
}
if (billingAddress == null) {
throw new IllegalArgumentException("Billing address is required");
}
}
// Getters
public SharedKernel.OrderId getId() { return id; }
public SharedKernel.CustomerId getCustomerId() { return customerId; }
public OrderStatus getStatus() { return status; }
public List<OrderItem> getItems() { return List.copyOf(items); }
public SharedKernel.Money getTotalAmount() { return totalAmount; }
public SharedKernel.Address getShippingAddress() { return shippingAddress; }
public SharedKernel.Address getBillingAddress() { return billingAddress; }
public PaymentStatus getPaymentStatus() { return paymentStatus; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
}
// Domain Service
public static class OrderPricingService {
private final CatalogContext.ProductRepository productRepository;
public OrderPricingService(CatalogContext.ProductRepository productRepository) {
this.productRepository = productRepository;
}
public SharedKernel.Money calculateOrderTotal(List<OrderItem> items) {
BigDecimal total = BigDecimal.ZERO;
SharedKernel.Currency currency = null;
for (OrderItem item : items) {
if (currency == null) {
currency = item.unitPrice().currency();
} else if (!currency.equals(item.unitPrice().currency())) {
throw new IllegalArgumentException("Mixed currencies in order items");
}
total = total.add(item.subtotal().amount());
}
if (currency == null) {
currency = SharedKernel.Currency.USD; // Default currency
}
return new SharedKernel.Money(total, currency);
}
public void validateProductAvailability(OrderItem item) {
var product = productRepository.findById(item.productId())
.orElseThrow(() -> new IllegalArgumentException("Product not found"));
if (!product.isActive()) {
throw new IllegalArgumentException("Product is not active");
}
if (product.getStockQuantity().value() < item.quantity()) {
throw new IllegalArgumentException("Insufficient stock for product: " + product.getName());
}
}
}
// Repository interface
public interface OrderRepository {
Optional<Order> findById(SharedKernel.OrderId id);
List<Order> findByCustomerId(SharedKernel.CustomerId customerId);
List<Order> findByStatus(OrderStatus status);
void save(Order order);
}
}
5. Bounded Context: Shipping
// Shipping Context - Manages logistics, tracking, and delivery
public class ShippingContext {
// Value Objects
public record TrackingNumber(String value) {
public TrackingNumber {
if (value == null || value.isBlank()) {
throw new IllegalArgumentException("Tracking number cannot be empty");
}
}
}
public record PackageWeight(double value, WeightUnit unit) {
public PackageWeight {
if (value <= 0) {
throw new IllegalArgumentException("Weight must be positive");
}
}
public enum WeightUnit {
GRAMS, KILOGRAMS, POUNDS, OUNCES
}
}
public record PackageDimensions(double length, double width, double height, LengthUnit unit) {
public PackageDimensions {
if (length <= 0 || width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive");
}
}
public enum LengthUnit {
CENTIMETERS, INCHES
}
}
public enum ShippingStatus {
PENDING, LABEL_CREATED, IN_TRANSIT, OUT_FOR_DELIVERY, DELIVERED, FAILED
}
// Aggregate Root
public static class Shipment {
private final SharedKernel.OrderId orderId;
private final TrackingNumber trackingNumber;
private ShippingStatus status;
private final PackageWeight weight;
private final PackageDimensions dimensions;
private SharedKernel.Address origin;
private SharedKernel.Address destination;
private String carrier;
private String serviceLevel;
private final Instant createdAt;
private Instant updatedAt;
private Instant estimatedDelivery;
private Instant actualDelivery;
public Shipment(SharedKernel.OrderId orderId, TrackingNumber trackingNumber,
PackageWeight weight, PackageDimensions dimensions,
SharedKernel.Address origin, SharedKernel.Address destination,
String carrier, String serviceLevel) {
this.orderId = orderId;
this.trackingNumber = trackingNumber;
this.status = ShippingStatus.PENDING;
this.weight = weight;
this.dimensions = dimensions;
this.origin = origin;
this.destination = destination;
this.carrier = carrier;
this.serviceLevel = serviceLevel;
this.createdAt = Instant.now();
this.updatedAt = Instant.now();
validate();
}
// Domain methods
public void createShippingLabel() {
if (status != ShippingStatus.PENDING) {
throw new IllegalStateException("Shipment is not pending");
}
this.status = ShippingStatus.LABEL_CREATED;
this.updatedAt = Instant.now();
}
public void markAsInTransit() {
if (status != ShippingStatus.LABEL_CREATED) {
throw new IllegalStateException("Shipping label must be created first");
}
this.status = ShippingStatus.IN_TRANSIT;
this.updatedAt = Instant.now();
}
public void markAsOutForDelivery() {
if (status != ShippingStatus.IN_TRANSIT) {
throw new IllegalStateException("Shipment must be in transit first");
}
this.status = ShippingStatus.OUT_FOR_DELIVERY;
this.updatedAt = Instant.now();
}
public void markAsDelivered() {
if (status != ShippingStatus.OUT_FOR_DELIVERY) {
throw new IllegalStateException("Shipment must be out for delivery first");
}
this.status = ShippingStatus.DELIVERED;
this.actualDelivery = Instant.now();
this.updatedAt = Instant.now();
}
public void markAsFailed(String reason) {
this.status = ShippingStatus.FAILED;
this.updatedAt = Instant.now();
}
public void updateTracking(String carrier, String trackingUrl) {
this.carrier = carrier;
this.updatedAt = Instant.now();
}
private void validate() {
if (orderId == null) {
throw new IllegalArgumentException("Order ID is required");
}
if (origin == null) {
throw new IllegalArgumentException("Origin address is required");
}
if (destination == null) {
throw new IllegalArgumentException("Destination address is required");
}
if (carrier == null || carrier.isBlank()) {
throw new IllegalArgumentException("Carrier is required");
}
}
// Getters
public SharedKernel.OrderId getOrderId() { return orderId; }
public TrackingNumber getTrackingNumber() { return trackingNumber; }
public ShippingStatus getStatus() { return status; }
public PackageWeight getWeight() { return weight; }
public PackageDimensions getDimensions() { return dimensions; }
public SharedKernel.Address getOrigin() { return origin; }
public SharedKernel.Address getDestination() { return destination; }
public String getCarrier() { return carrier; }
public String getServiceLevel() { return serviceLevel; }
public Instant getCreatedAt() { return createdAt; }
public Instant getUpdatedAt() { return updatedAt; }
public Instant getEstimatedDelivery() { return estimatedDelivery; }
public Instant getActualDelivery() { return actualDelivery; }
}
// Domain Service
public static class ShippingCostCalculator {
private static final Map<String, BigDecimal> CARRIER_RATES = Map.of(
"UPS", new BigDecimal("0.15"),
"FEDEX", new BigDecimal("0.18"),
"USPS", new BigDecimal("0.12")
);
public SharedKernel.Money calculateCost(PackageWeight weight,
PackageDimensions dimensions,
String carrier,
String serviceLevel,
int distanceMiles) {
BigDecimal baseRate = CARRIER_RATES.getOrDefault(carrier, new BigDecimal("0.15"));
// Simple calculation: base rate * weight * distance
double weightInPounds = convertToPounds(weight);
BigDecimal cost = baseRate
.multiply(BigDecimal.valueOf(weightInPounds))
.multiply(BigDecimal.valueOf(distanceMiles));
// Add service level premium
if ("EXPRESS".equals(serviceLevel)) {
cost = cost.multiply(new BigDecimal("1.5"));
} else if ("OVERNIGHT".equals(serviceLevel)) {
cost = cost.multiply(new BigDecimal("2.0"));
}
return new SharedKernel.Money(cost, SharedKernel.Currency.USD);
}
private double convertToPounds(PackageWeight weight) {
return switch (weight.unit()) {
case GRAMS -> weight.value() / 453.592;
case KILOGRAMS -> weight.value() * 2.20462;
case POUNDS -> weight.value();
case OUNCES -> weight.value() / 16;
};
}
}
// Repository interface
public interface ShipmentRepository {
Optional<Shipment> findByOrderId(SharedKernel.OrderId orderId);
Optional<Shipment> findByTrackingNumber(TrackingNumber trackingNumber);
List<Shipment> findByStatus(ShippingStatus status);
void save(Shipment shipment);
}
}
Context Mapping
6. Context Integration Patterns
// Anti-Corruption Layer (ACL) for context integration
public class ContextIntegration {
// ACL for Identity Context integration
public static class IdentityContextACL {
private final IdentityContext.UserRepository userRepository;
public IdentityContextACL(IdentityContext.UserRepository userRepository) {
this.userRepository = userRepository;
}
public CustomerInfo getCustomerInfo(SharedKernel.CustomerId customerId) {
var userId = new IdentityContext.UserId(customerId.value());
var user = userRepository.findById(userId)
.orElseThrow(() -> new CustomerNotFoundException(customerId));
return new CustomerInfo(
customerId,
user.getEmail(),
user.getStatus() == IdentityContext.UserStatus.ACTIVE,
user.getRoles()
);
}
public void validateCustomerActive(SharedKernel.CustomerId customerId) {
var info = getCustomerInfo(customerId);
if (!info.active()) {
throw new CustomerNotActiveException(customerId);
}
}
public record CustomerInfo(
SharedKernel.CustomerId customerId,
String email,
boolean active,
Set<IdentityContext.UserRole> roles
) {}
}
// ACL for Catalog Context integration
public static class CatalogContextACL {
private final CatalogContext.ProductRepository productRepository;
public CatalogContextACL(CatalogContext.ProductRepository productRepository) {
this.productRepository = productRepository;
}
public ProductInfo getProductInfo(CatalogContext.ProductId productId) {
var product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
return new ProductInfo(
productId,
product.getName(),
product.getPrice(),
product.getStockQuantity(),
product.isActive()
);
}
public void reserveProduct(CatalogContext.ProductId productId, int quantity) {
var product = productRepository.findById(productId)
.orElseThrow(() -> new ProductNotFoundException(productId));
if (!product.isAvailable()) {
throw new ProductNotAvailableException(productId);
}
if (product.getStockQuantity().value() < quantity) {
throw new InsufficientStockException(productId, quantity, product.getStockQuantity().value());
}
product.decreaseStock(new CatalogContext.StockQuantity(quantity));
productRepository.save(product);
}
public record ProductInfo(
CatalogContext.ProductId productId,
String name,
SharedKernel.Money price,
CatalogContext.StockQuantity stock,
boolean active
) {}
}
// Domain Events for inter-context communication
public interface DomainEvent {
String eventId();
Instant occurredOn();
String eventType();
}
public record OrderCreatedEvent(
String eventId,
Instant occurredOn,
SharedKernel.OrderId orderId,
SharedKernel.CustomerId customerId,
SharedKernel.Money totalAmount,
List<OrderItem> items
) implements DomainEvent {
public OrderCreatedEvent(SharedKernel.OrderId orderId, SharedKernel.CustomerId customerId,
SharedKernel.Money totalAmount, List<OrderItem> items) {
this(UUID.randomUUID().toString(), Instant.now(), orderId, customerId, totalAmount, items);
}
@Override
public String eventType() {
return "ORDER_CREATED";
}
}
public record PaymentCompletedEvent(
String eventId,
Instant occurredOn,
SharedKernel.OrderId orderId,
SharedKernel.CustomerId customerId,
SharedKernel.Money amount
) implements DomainEvent {
public PaymentCompletedEvent(SharedKernel.OrderId orderId, SharedKernel.CustomerId customerId,
SharedKernel.Money amount) {
this(UUID.randomUUID().toString(), Instant.now(), orderId, customerId, amount);
}
@Override
public String eventType() {
return "PAYMENT_COMPLETED";
}
}
public record ProductStockUpdatedEvent(
String eventId,
Instant occurredOn,
CatalogContext.ProductId productId,
CatalogContext.StockQuantity newStock,
CatalogContext.StockQuantity oldStock
) implements DomainEvent {
public ProductStockUpdatedEvent(CatalogContext.ProductId productId,
CatalogContext.StockQuantity newStock,
CatalogContext.StockQuantity oldStock) {
this(UUID.randomUUID().toString(), Instant.now(), productId, newStock, oldStock);
}
@Override
public String eventType() {
return "PRODUCT_STOCK_UPDATED";
}
}
// Event handlers for cross-context integration
public static class OrderEventsHandler {
private final ShippingContext.ShipmentRepository shipmentRepository;
private final CatalogContextACL catalogACL;
public OrderEventsHandler(ShippingContext.ShipmentRepository shipmentRepository,
CatalogContextACL catalogACL) {
this.shipmentRepository = shipmentRepository;
this.catalogACL = catalogACL;
}
public void handleOrderCreated(OrderCreatedEvent event) {
// Reserve products in catalog
for (var item : event.items()) {
catalogACL.reserveProduct(item.productId(), item.quantity());
}
// Create shipment in shipping context
var shipment = new ShippingContext.Shipment(
event.orderId(),
new ShippingContext.TrackingNumber(generateTrackingNumber()),
calculatePackageWeight(event.items()),
calculatePackageDimensions(event.items()),
getWarehouseAddress(),
getCustomerAddress(event.customerId()), // Would need address service
"UPS",
"GROUND"
);
shipmentRepository.save(shipment);
}
public void handlePaymentCompleted(PaymentCompletedEvent event) {
// Update order status and trigger shipment
var shipment = shipmentRepository.findByOrderId(event.orderId())
.orElseThrow(() -> new ShipmentNotFoundException(event.orderId()));
shipment.createShippingLabel();
shipmentRepository.save(shipment);
}
private ShippingContext.PackageWeight calculatePackageWeight(List<OrderItem> items) {
// Simplified calculation
double totalWeight = items.size() * 0.5; // 0.5kg per item
return new ShippingContext.PackageWeight(totalWeight,
ShippingContext.PackageWeight.WeightUnit.KILOGRAMS);
}
private ShippingContext.PackageDimensions calculatePackageDimensions(List<OrderItem> items) {
// Simplified calculation
return new ShippingContext.PackageDimensions(30.0, 20.0, 15.0,
ShippingContext.PackageDimensions.LengthUnit.CENTIMETERS);
}
private SharedKernel.Address getWarehouseAddress() {
return new SharedKernel.Address(
"123 Warehouse St", "Commerce City", "CO", "80022", "US"
);
}
private SharedKernel.Address getCustomerAddress(SharedKernel.CustomerId customerId) {
// Would integrate with customer profile context
return new SharedKernel.Address(
"456 Customer Ave", "Denver", "CO", "80202", "US"
);
}
private String generateTrackingNumber() {
return "TRK" + System.currentTimeMillis() +
ThreadLocalRandom.current().nextInt(1000, 9999);
}
}
}
Implementation Architecture
7. Spring Boot Application Structure
// Application layer for Identity Context
@RestController
@RequestMapping("/api/identity")
public class IdentityController {
private final IdentityContext.AuthenticationService authenticationService;
private final IdentityContext.UserRepository userRepository;
public IdentityController(IdentityContext.AuthenticationService authenticationService,
IdentityContext.UserRepository userRepository) {
this.authenticationService = authenticationService;
this.userRepository = userRepository;
}
@PostMapping("/register")
public ResponseEntity<UserResponse> register(@RequestBody RegisterRequest request) {
// Validate request
if (userRepository.existsByEmail(request.email())) {
throw new UserAlreadyExistsException(request.email());
}
// Create user
var userId = new IdentityContext.UserId(UUID.randomUUID().toString());
var user = new IdentityContext.User(
userId,
request.email(),
hashPassword(request.password()),
Set.of(IdentityContext.UserRole.CUSTOMER)
);
userRepository.save(user);
return ResponseEntity.status(HttpStatus.CREATED)
.body(UserResponse.fromDomain(user));
}
@PostMapping("/authenticate")
public ResponseEntity<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
var user = authenticationService.authenticate(request.email(), request.password());
var token = generateAuthToken(user);
return ResponseEntity.ok(new AuthenticationResponse(token, UserResponse.fromDomain(user)));
}
// DTOs
public record RegisterRequest(String email, String password, String confirmPassword) {}
public record AuthenticationRequest(String email, String password) {}
public record UserResponse(String userId, String email, String status, Set<String> roles) {
public static UserResponse fromDomain(IdentityContext.User user) {
return new UserResponse(
user.getId().value(),
user.getEmail(),
user.getStatus().name(),
user.getRoles().stream()
.map(Enum::name)
.collect(Collectors.toSet())
);
}
}
public record AuthenticationResponse(String token, UserResponse user) {}
private String hashPassword(String plainPassword) {
// Implementation would use proper password hashing
return "hashed_" + plainPassword;
}
private String generateAuthToken(IdentityContext.User user) {
// Implementation would use JWT or similar
return "token_" + user.getId().value();
}
}
// Application layer for Ordering Context
@RestController
@RequestMapping("/api/orders")
public class OrderController {
private final OrderingContext.OrderRepository orderRepository;
private final OrderingContext.OrderPricingService pricingService;
private final ContextIntegration.IdentityContextACL identityACL;
private final ContextIntegration.CatalogContextACL catalogACL;
private final ContextIntegration.OrderEventsHandler eventsHandler;
public OrderController(OrderingContext.OrderRepository orderRepository,
OrderingContext.OrderPricingService pricingService,
ContextIntegration.IdentityContextACL identityACL,
ContextIntegration.CatalogContextACL catalogACL,
ContextIntegration.OrderEventsHandler eventsHandler) {
this.orderRepository = orderRepository;
this.pricingService = pricingService;
this.identityACL = identityACL;
this.catalogACL = catalogACL;
this.eventsHandler = eventsHandler;
}
@PostMapping
public ResponseEntity<OrderResponse> createOrder(@RequestBody CreateOrderRequest request) {
// Validate customer
identityACL.validateCustomerActive(request.customerId());
// Create order
var orderId = new SharedKernel.OrderId(UUID.randomUUID().toString());
var order = new OrderingContext.Order(
orderId,
request.customerId(),
request.shippingAddress(),
request.billingAddress()
);
// Add items
for (var itemRequest : request.items()) {
var productInfo = catalogACL.getProductInfo(itemRequest.productId());
var orderItem = OrderingContext.OrderItem.create(
itemRequest.productId(),
productInfo.name(),
productInfo.price(),
itemRequest.quantity()
);
order.addItem(orderItem);
}
// Submit order
order.submit();
orderRepository.save(order);
// Publish domain event
var event = new ContextIntegration.OrderCreatedEvent(
orderId,
request.customerId(),
order.getTotalAmount(),
order.getItems()
);
eventsHandler.handleOrderCreated(event);
return ResponseEntity.status(HttpStatus.CREATED)
.body(OrderResponse.fromDomain(order));
}
@PostMapping("/{orderId}/payment")
public ResponseEntity<OrderResponse> processPayment(@PathVariable String orderId,
@RequestBody PaymentRequest request) {
var order = orderRepository.findById(new SharedKernel.OrderId(orderId))
.orElseThrow(() -> new OrderNotFoundException(orderId));
// Process payment (simplified)
order.confirmPayment();
orderRepository.save(order);
// Publish domain event
var event = new ContextIntegration.PaymentCompletedEvent(
order.getId(),
order.getCustomerId(),
order.getTotalAmount()
);
eventsHandler.handlePaymentCompleted(event);
return ResponseEntity.ok(OrderResponse.fromDomain(order));
}
// DTOs
public record CreateOrderRequest(
SharedKernel.CustomerId customerId,
SharedKernel.Address shippingAddress,
SharedKernel.Address billingAddress,
List<OrderItemRequest> items
) {}
public record OrderItemRequest(CatalogContext.ProductId productId, int quantity) {}
public record PaymentRequest(String paymentMethod, String transactionId) {}
public record OrderResponse(
String orderId,
String customerId,
String status,
List<OrderItemResponse> items,
MoneyResponse totalAmount,
AddressResponse shippingAddress,
AddressResponse billingAddress,
String paymentStatus,
Instant createdAt
) {
public static OrderResponse fromDomain(OrderingContext.Order order) {
return new OrderResponse(
order.getId().value(),
order.getCustomerId().value(),
order.getStatus().name(),
order.getItems().stream()
.map(OrderItemResponse::fromDomain)
.collect(Collectors.toList()),
MoneyResponse.fromDomain(order.getTotalAmount()),
AddressResponse.fromDomain(order.getShippingAddress()),
AddressResponse.fromDomain(order.getBillingAddress()),
order.getPaymentStatus().name(),
order.getCreatedAt()
);
}
}
public record OrderItemResponse(
String productId,
String productName,
MoneyResponse unitPrice,
int quantity,
MoneyResponse subtotal
) {
public static OrderItemResponse fromDomain(OrderingContext.OrderItem item) {
return new OrderItemResponse(
item.productId().value(),
item.productName(),
MoneyResponse.fromDomain(item.unitPrice()),
item.quantity(),
MoneyResponse.fromDomain(item.subtotal())
);
}
}
public record MoneyResponse(BigDecimal amount, String currency) {
public static MoneyResponse fromDomain(SharedKernel.Money money) {
return new MoneyResponse(money.amount(), money.currency().name());
}
}
public record AddressResponse(String street, String city, String state,
String zipCode, String country) {
public static AddressResponse fromDomain(SharedKernel.Address address) {
return new AddressResponse(
address.street(), address.city(), address.state(),
address.zipCode(), address.country()
);
}
}
}
Communication Patterns
8. Asynchronous Communication with Events
// Event publishing and consumption
@Component
public class DomainEventPublisher {
private final ApplicationEventPublisher eventPublisher;
public DomainEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void publish(ContextIntegration.DomainEvent event) {
eventPublisher.publishEvent(event);
}
}
// Event listeners for cross-context integration
@Component
public class CrossContextEventListeners {
private final ContextIntegration.OrderEventsHandler orderEventsHandler;
private final CatalogContext.ProductRepository productRepository;
public CrossContextEventListeners(ContextIntegration.OrderEventsHandler orderEventsHandler,
CatalogContext.ProductRepository productRepository) {
this.orderEventsHandler = orderEventsHandler;
this.productRepository = productRepository;
}
@EventListener
public void handleOrderCreated(ContextIntegration.OrderCreatedEvent event) {
orderEventsHandler.handleOrderCreated(event);
}
@EventListener
public void handlePaymentCompleted(ContextIntegration.PaymentCompletedEvent event) {
orderEventsHandler.handlePaymentCompleted(event);
}
@EventListener
public void handleProductStockUpdated(ContextIntegration.ProductStockUpdatedEvent event) {
// Update read models or trigger other actions
System.out.println("Product stock updated: " + event.productId().value() +
" from " + event.oldStock().value() +
" to " + event.newStock().value());
}
}
// Message-based integration (Kafka example)
@Component
public class KafkaEventPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
private final ObjectMapper objectMapper;
public KafkaEventPublisher(KafkaTemplate<String, Object> kafkaTemplate,
ObjectMapper objectMapper) {
this.kafkaTemplate = kafkaTemplate;
this.objectMapper = objectMapper;
}
public void publishToKafka(ContextIntegration.DomainEvent event) {
try {
String topic = "domain-events-" + event.eventType().toLowerCase();
String message = objectMapper.writeValueAsString(event);
kafkaTemplate.send(topic, event.eventId(), message)
.addCallback(
result -> logger.info("Event published successfully: {}", event.eventId()),
failure -> logger.error("Failed to publish event: {}", event.eventId(), failure)
);
} catch (Exception e) {
logger.error("Error publishing event to Kafka", e);
}
}
}
@Component
public class KafkaEventConsumer {
private final ContextIntegration.OrderEventsHandler orderEventsHandler;
public KafkaEventConsumer(ContextIntegration.OrderEventsHandler orderEventsHandler) {
this.orderEventsHandler = orderEventsHandler;
}
@KafkaListener(topics = "domain-events-order-created")
public void consumeOrderCreated(String message) {
try {
var event = objectMapper.readValue(message, ContextIntegration.OrderCreatedEvent.class);
orderEventsHandler.handleOrderCreated(event);
} catch (Exception e) {
logger.error("Error processing OrderCreated event", e);
}
}
}
Testing Strategies
9. Comprehensive Testing
// Unit tests for domain models
class OrderTests {
@Test
void shouldCreateOrderInDraftStatus() {
// Given
var orderId = new SharedKernel.OrderId("order-123");
var customerId = new SharedKernel.CustomerId("customer-456");
var address = new SharedKernel.Address("123 Main St", "City", "State", "12345", "US");
// When
var order = new OrderingContext.Order(orderId, customerId, address, address);
// Then
assertEquals(OrderingContext.OrderStatus.DRAFT, order.getStatus());
assertEquals(0, order.getItems().size());
assertEquals(BigDecimal.ZERO, order.getTotalAmount().amount());
}
@Test
void shouldAddItemToOrder() {
// Given
var order = createTestOrder();
var item = OrderingContext.OrderItem.create(
new CatalogContext.ProductId("product-1"),
"Test Product",
new SharedKernel.Money(new BigDecimal("29.99"), SharedKernel.Currency.USD),
2
);
// When
order.addItem(item);
// Then
assertEquals(1, order.getItems().size());
assertEquals(new BigDecimal("59.98"), order.getTotalAmount().amount());
}
@Test
void shouldNotAddItemToSubmittedOrder() {
// Given
var order = createTestOrder();
order.submit();
var item = OrderingContext.OrderItem.create(
new CatalogContext.ProductId("product-1"),
"Test Product",
new SharedKernel.Money(new BigDecimal("29.99"), SharedKernel.Currency.USD),
1
);
// When & Then
assertThrows(IllegalStateException.class, () -> order.addItem(item));
}
private OrderingContext.Order createTestOrder() {
var orderId = new SharedKernel.OrderId("order-123");
var customerId = new SharedKernel.CustomerId("customer-456");
var address = new SharedKernel.Address("123 Main St", "City", "State", "12345", "US");
return new OrderingContext.Order(orderId, customerId, address, address);
}
}
// Integration tests for context mapping
@SpringBootTest
class ContextIntegrationTests {
@Autowired
private OrderingContext.OrderRepository orderRepository;
@Autowired
private ContextIntegration.IdentityContextACL identityACL;
@Autowired
private ContextIntegration.CatalogContextACL catalogACL;
@Test
void shouldCreateOrderWithValidCustomerAndProducts() {
// Given
var customerId = new SharedKernel.CustomerId("valid-customer");
var productId = new CatalogContext.ProductId("valid-product");
// When & Then
assertDoesNotThrow(() -> {
identityACL.validateCustomerActive(customerId);
catalogACL.reserveProduct(productId, 1);
});
}
}
// Test doubles for isolated testing
class TestDoubles {
static class InMemoryUserRepository implements IdentityContext.UserRepository {
private final Map<IdentityContext.UserId, IdentityContext.User> users = new HashMap<>();
@Override
public Optional<IdentityContext.User> findById(IdentityContext.UserId id) {
return Optional.ofNullable(users.get(id));
}
@Override
public Optional<IdentityContext.User> findByEmail(String email) {
return users.values().stream()
.filter(user -> user.getEmail().equals(email))
.findFirst();
}
@Override
public void save(IdentityContext.User user) {
users.put(user.getId(), user);
}
@Override
public boolean existsByEmail(String email) {
return users.values().stream()
.anyMatch(user -> user.getEmail().equals(email));
}
}
static class InMemoryOrderRepository implements OrderingContext.OrderRepository {
private final Map<SharedKernel.OrderId, OrderingContext.Order> orders = new HashMap<>();
@Override
public Optional<OrderingContext.Order> findById(SharedKernel.OrderId id) {
return Optional.ofNullable(orders.get(id));
}
@Override
public List<OrderingContext.Order> findByCustomerId(SharedKernel.CustomerId customerId) {
return orders.values().stream()
.filter(order -> order.getCustomerId().equals(customerId))
.collect(Collectors.toList());
}
@Override
public List<OrderingContext.Order> findByStatus(OrderingContext.OrderStatus status) {
return orders.values().stream()
.filter(order -> order.getStatus() == status)
.collect(Collectors.toList());
}
@Override
public void save(OrderingContext.Order order) {
orders.put(order.getId(), order);
}
}
}
This comprehensive DDD Bounded Contexts implementation provides:
- Clear context boundaries with explicit domain models
- Strategic design patterns including ACL and domain events
- Practical e-commerce case study with multiple contexts
- Spring Boot integration for modern applications
- Testing strategies for maintainable code
- Communication patterns for context integration
The architecture ensures each bounded context can evolve independently while maintaining clear integration points and preserving domain integrity.