1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 10:29:35 +01:00
effigenix/.claude/skills/ddd-implement/examples/java-example.md
Sebastian Frick ec9114aa0a feat: add Spring Boot ERP application with user management domain
Implement DDD-based architecture with domain, application, infrastructure,
and API layers. Includes user/role management with authentication,
RBAC permissions, audit logging, Liquibase migrations, and test suite.
2026-02-17 19:33:24 +01:00

637 lines
16 KiB
Markdown

# 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<OrderLine> 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<OrderError, Order> 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<OrderLine> 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<OrderError, Void> 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<OrderError, Void> 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<OrderError, Void> 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<OrderError, Void> 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<OrderError, Void> 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<OrderLineError, OrderLine> 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<OrderLineError, Void> 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
```