DDD Bounded Contexts in Java: Complete Implementation Guide

Learn how to implement Domain-Driven Design (DDD) Bounded Contexts in Java with practical examples and modern architectures.

Table of Contents

  1. Bounded Contexts Fundamentals
  2. Strategic Design Patterns
  3. E-Commerce Case Study
  4. Context Mapping
  5. Implementation Architecture
  6. Communication Patterns
  7. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper