# Java Implementation Example Session This is an example session showing how the `/ddd-implement` skill works in a Java project. ## Scenario You're building an e-commerce system and need to implement the **Order** aggregate in the domain layer. --- ## User Request ``` /ddd-implement --lang=java "Implement Order aggregate with these requirements: - Order has order number, customer ID, list of line items, and status - Can add/remove line items (only when status is DRAFT) - Can place order (transitions to PLACED) - Can cancel order (transitions to CANCELLED) - Cannot modify after PLACED or CANCELLED - Total amount is calculated from line items" ``` --- ## Agent Response I'll implement the **Order aggregate** in the domain layer following DDD patterns. ### Analysis **Aggregate Root**: Order **Entities**: OrderLine (line items within order) **Value Objects**: - OrderNumber (unique identifier) - CustomerId (reference to Customer aggregate) - Money (for prices/totals) - OrderStatus (enum: DRAFT, PLACED, CANCELLED) **Invariants**: 1. Cannot modify order after PLACED or CANCELLED 2. Cannot place order with zero line items 3. Line items must have positive quantity 4. Order total = sum of line item totals **Layer**: Domain (`com.example.domain.order`) Let me implement this: ### Error Types ```java package com.example.domain.order; /** * Order domain errors. */ public sealed interface OrderError permits OrderAlreadyPlacedError, OrderCancelledError, EmptyOrderError, LineItemNotFoundError, InvalidLineItemError { String message(); } public record OrderAlreadyPlacedError( OrderNumber orderNumber ) implements OrderError { @Override public String message() { return "Order already placed: " + orderNumber; } } public record OrderCancelledError( OrderNumber orderNumber ) implements OrderError { @Override public String message() { return "Order is cancelled: " + orderNumber; } } public record EmptyOrderError() implements OrderError { @Override public String message() { return "Cannot place order with no line items"; } } public record LineItemNotFoundError( OrderLineId lineItemId ) implements OrderError { @Override public String message() { return "Line item not found: " + lineItemId; } } public record InvalidLineItemError( String reason ) implements OrderError { @Override public String message() { return "Invalid line item: " + reason; } } ``` ### Order Aggregate ```java package com.example.domain.order; import com.example.shared.result.Result; import static com.example.shared.result.Result.Failure; import static com.example.shared.result.Result.Success; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.Objects; /** * Order aggregate root. * * Invariant: Cannot modify order after PLACED or CANCELLED * Invariant: Cannot place order with zero line items * Invariant: Order total = sum of line item totals */ public class Order { private final OrderNumber orderNumber; private final CustomerId customerId; private final List lineItems; private OrderStatus status; private final Instant createdAt; private Instant updatedAt; /** * Private constructor - use factory method. */ private Order( OrderNumber orderNumber, CustomerId customerId, OrderStatus status, Instant createdAt ) { this.orderNumber = orderNumber; this.customerId = customerId; this.lineItems = new ArrayList<>(); this.status = status; this.createdAt = createdAt; this.updatedAt = createdAt; } /** * Creates a new Order in DRAFT status. */ public static Result create( OrderNumber orderNumber, CustomerId customerId ) { Order order = new Order( orderNumber, customerId, OrderStatus.DRAFT, Instant.now() ); return Result.success(order); } // Getters public OrderNumber orderNumber() { return orderNumber; } public CustomerId customerId() { return customerId; } public OrderStatus status() { return status; } public Instant createdAt() { return createdAt; } public Instant updatedAt() { return updatedAt; } /** * Returns defensive copy of line items. */ public List lineItems() { return List.copyOf(lineItems); } /** * Calculates total order amount. * * Invariant: Total = sum of all line item totals */ public Money calculateTotal() { Money total = Money.usd(0); for (OrderLine line : lineItems) { Money lineTotal = line.calculateTotal(); total = switch (total.add(lineTotal)) { case Failure(MoneyError error) -> throw new IllegalStateException( "Currency mismatch in order: " + error.message() ); case Success(Money sum) -> sum; }; } return total; } /** * Adds a line item to the order. * * Invariant: Cannot modify after PLACED or CANCELLED */ public Result addLineItem( ProductId productId, String productName, Money unitPrice, int quantity ) { // Guard: Check order status if (status == OrderStatus.PLACED) { return Result.failure(new OrderAlreadyPlacedError(orderNumber)); } if (status == OrderStatus.CANCELLED) { return Result.failure(new OrderCancelledError(orderNumber)); } // Create line item OrderLineId lineId = OrderLineId.generate(); return switch (OrderLine.create(lineId, productId, productName, unitPrice, quantity)) { case Failure(OrderLineError error) -> Result.failure(new InvalidLineItemError(error.message())); case Success(OrderLine line) -> { lineItems.add(line); this.updatedAt = Instant.now(); yield Result.success(null); } }; } /** * Removes a line item from the order. * * Invariant: Cannot modify after PLACED or CANCELLED */ public Result removeLineItem(OrderLineId lineItemId) { // Guard: Check order status if (status == OrderStatus.PLACED) { return Result.failure(new OrderAlreadyPlacedError(orderNumber)); } if (status == OrderStatus.CANCELLED) { return Result.failure(new OrderCancelledError(orderNumber)); } // Find and remove line item boolean removed = lineItems.removeIf(line -> line.id().equals(lineItemId)); if (!removed) { return Result.failure(new LineItemNotFoundError(lineItemId)); } this.updatedAt = Instant.now(); return Result.success(null); } /** * Updates quantity of an existing line item. * * Invariant: Cannot modify after PLACED or CANCELLED */ public Result updateLineItemQuantity( OrderLineId lineItemId, int newQuantity ) { // Guard: Check order status if (status == OrderStatus.PLACED) { return Result.failure(new OrderAlreadyPlacedError(orderNumber)); } if (status == OrderStatus.CANCELLED) { return Result.failure(new OrderCancelledError(orderNumber)); } // Find line item OrderLine line = findLineItemById(lineItemId); if (line == null) { return Result.failure(new LineItemNotFoundError(lineItemId)); } // Update quantity return switch (line.updateQuantity(newQuantity)) { case Failure(OrderLineError error) -> Result.failure(new InvalidLineItemError(error.message())); case Success(Void ignored) -> { this.updatedAt = Instant.now(); yield Result.success(null); } }; } /** * Places the order (transitions from DRAFT to PLACED). * * Invariant: Cannot place empty order * Invariant: Can only place order in DRAFT status */ public Result place() { // Guard: Check status if (status == OrderStatus.PLACED) { return Result.failure(new OrderAlreadyPlacedError(orderNumber)); } if (status == OrderStatus.CANCELLED) { return Result.failure(new OrderCancelledError(orderNumber)); } // Guard: Check has line items if (lineItems.isEmpty()) { return Result.failure(new EmptyOrderError()); } // Transition to PLACED this.status = OrderStatus.PLACED; this.updatedAt = Instant.now(); return Result.success(null); } /** * Cancels the order. * * Invariant: Can cancel from DRAFT or PLACED, not from CANCELLED */ public Result cancel() { // Guard: Already cancelled if (status == OrderStatus.CANCELLED) { return Result.failure(new OrderCancelledError(orderNumber)); } // Transition to CANCELLED this.status = OrderStatus.CANCELLED; this.updatedAt = Instant.now(); return Result.success(null); } private OrderLine findLineItemById(OrderLineId id) { return lineItems.stream() .filter(line -> line.id().equals(id)) .findFirst() .orElse(null); } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Order order)) return false; return Objects.equals(orderNumber, order.orderNumber); } @Override public int hashCode() { return Objects.hash(orderNumber); } @Override public String toString() { return "Order{" + "orderNumber=" + orderNumber + ", customerId=" + customerId + ", status=" + status + ", lineItems=" + lineItems.size() + ", total=" + calculateTotal() + '}'; } } ``` ### OrderLine Entity ```java package com.example.domain.order; import com.example.shared.result.Result; import java.util.Objects; /** * OrderLine errors. */ public sealed interface OrderLineError permits InvalidQuantityError, InvalidPriceError { String message(); } public record InvalidQuantityError(int quantity) implements OrderLineError { @Override public String message() { return "Quantity must be positive: " + quantity; } } public record InvalidPriceError(Money price) implements OrderLineError { @Override public String message() { return "Price must be positive: " + price; } } /** * OrderLine entity (child entity within Order aggregate). * * Invariant: Quantity must be positive * Invariant: Price must be positive */ public class OrderLine { private final OrderLineId id; private final ProductId productId; private final String productName; private final Money unitPrice; private int quantity; /** * Package-private constructor - created by Order aggregate. */ OrderLine( OrderLineId id, ProductId productId, String productName, Money unitPrice, int quantity ) { this.id = id; this.productId = productId; this.productName = productName; this.unitPrice = unitPrice; this.quantity = quantity; } /** * Creates a new OrderLine. */ static Result create( OrderLineId id, ProductId productId, String productName, Money unitPrice, int quantity ) { // Invariant: Quantity must be positive if (quantity <= 0) { return Result.failure(new InvalidQuantityError(quantity)); } // Invariant: Price must be positive if (unitPrice.isNegativeOrZero()) { return Result.failure(new InvalidPriceError(unitPrice)); } return Result.success(new OrderLine( id, productId, productName, unitPrice, quantity )); } public OrderLineId id() { return id; } public ProductId productId() { return productId; } public String productName() { return productName; } public Money unitPrice() { return unitPrice; } public int quantity() { return quantity; } /** * Updates the quantity. * * Invariant: Quantity must be positive */ Result updateQuantity(int newQuantity) { if (newQuantity <= 0) { return Result.failure(new InvalidQuantityError(newQuantity)); } this.quantity = newQuantity; return Result.success(null); } /** * Calculates total price for this line. */ public Money calculateTotal() { return unitPrice.multiply(quantity); } /** * Equality based on ID only (entity identity). */ @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof OrderLine that)) return false; return Objects.equals(id, that.id); } @Override public int hashCode() { return Objects.hash(id); } @Override public String toString() { return "OrderLine{" + "id=" + id + ", product=" + productName + ", quantity=" + quantity + ", unitPrice=" + unitPrice + ", total=" + calculateTotal() + '}'; } } ``` ### Supporting Types ```java package com.example.domain.order; /** * Order status enumeration. */ public enum OrderStatus { DRAFT("Order is being created"), PLACED("Order has been placed"), CANCELLED("Order was cancelled"); private final String description; OrderStatus(String description) { this.description = description; } public String description() { return description; } } /** * OrderNumber value object. */ public record OrderNumber(String value) { public OrderNumber { if (value == null || value.isBlank()) { throw new IllegalArgumentException("OrderNumber cannot be empty"); } } public static OrderNumber of(String value) { return new OrderNumber(value); } public static OrderNumber generate() { return new OrderNumber("ORD-" + java.util.UUID.randomUUID()); } } /** * OrderLineId value object. */ public record OrderLineId(String value) { public OrderLineId { if (value == null || value.isBlank()) { throw new IllegalArgumentException("OrderLineId cannot be empty"); } } public static OrderLineId generate() { return new OrderLineId(java.util.UUID.randomUUID().toString()); } } ``` --- ## Summary ✅ **Implemented**: - Order aggregate root with all required operations - OrderLine entity (child entity, package-private constructor) - Sealed interfaces for error types - All invariants documented and enforced - Result types for all operations (no exceptions) - Pattern matching with switch expressions - Defensive copy for line items collection ✅ **DDD Rules Followed**: - Aggregate is only entry point (line items accessed via Order methods) - Entities have ID-based equality - All invariants checked in constructor and mutation methods - No direct references to other aggregates (ProductId, CustomerId are IDs only) - One aggregate = one transaction boundary ✅ **Layer**: Domain (no external dependencies) ✅ **Files Created**: - `com/example/domain/order/Order.java` - `com/example/domain/order/OrderLine.java` - `com/example/domain/order/OrderError.java` - `com/example/domain/order/OrderLineError.java` - `com/example/domain/order/OrderStatus.java` - `com/example/domain/order/OrderNumber.java` - `com/example/domain/order/OrderLineId.java` --- ## Next Steps You can now: 1. **Implement the repository interface**: ``` /ddd-implement --lang=java "Create OrderRepository interface in domain layer" ``` 2. **Implement a use case**: ``` /ddd-implement --lang=java "Create PlaceOrder use case in application layer" ``` 3. **Implement repository**: ``` /ddd-implement --lang=java "Implement JdbcOrderRepository in infrastructure layer" ``` 4. **Add tests**: ``` Write unit tests for Order aggregate ```