mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 14:09:34 +01:00
docs: and skills
This commit is contained in:
parent
e4f0665086
commit
ccd4ee534a
25 changed files with 10412 additions and 0 deletions
767
bin/.claude/skills/ddd-model/languages/java/style-guide.md
Normal file
767
bin/.claude/skills/ddd-model/languages/java/style-guide.md
Normal file
|
|
@ -0,0 +1,767 @@
|
|||
# Java 21+ Style Guide for DDD
|
||||
|
||||
This guide covers Java conventions and modern language features for Domain-Driven Design implementations.
|
||||
|
||||
## Records vs Classes for Value Objects
|
||||
|
||||
### Use Records for Value Objects
|
||||
|
||||
Records are perfect for immutable value objects with validation:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Use record for simple value object.
|
||||
* Automatically generates equals, hashCode, toString.
|
||||
*/
|
||||
public record Money(
|
||||
java.math.BigDecimal amount,
|
||||
String currency
|
||||
) {
|
||||
/**
|
||||
* Compact constructor performs validation.
|
||||
*/
|
||||
public Money {
|
||||
if (amount == null) {
|
||||
throw new IllegalArgumentException("Amount cannot be null");
|
||||
}
|
||||
if (currency == null || currency.isBlank()) {
|
||||
throw new IllegalArgumentException("Currency cannot be empty");
|
||||
}
|
||||
if (amount.signum() < 0) {
|
||||
throw new IllegalArgumentException("Amount cannot be negative");
|
||||
}
|
||||
// Canonicalize to 2 decimal places
|
||||
amount = amount.setScale(2, java.math.RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Static factory for common currencies.
|
||||
*/
|
||||
public static Money usd(long cents) {
|
||||
return new Money(
|
||||
java.math.BigDecimal.valueOf(cents).scaleByPowerOfTen(-2),
|
||||
"USD"
|
||||
);
|
||||
}
|
||||
|
||||
public static Money eur(long cents) {
|
||||
return new Money(
|
||||
java.math.BigDecimal.valueOf(cents).scaleByPowerOfTen(-2),
|
||||
"EUR"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MustXxx variant for tests (panics on error).
|
||||
*/
|
||||
public static Money mustUsd(String amount) {
|
||||
try {
|
||||
return usd(Long.parseLong(amount));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new AssertionError("Invalid money for test: " + amount, e);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isNegativeOrZero() {
|
||||
return amount.signum() <= 0;
|
||||
}
|
||||
|
||||
public boolean isPositive() {
|
||||
return amount.signum() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain operation: add money (must be same currency).
|
||||
*/
|
||||
public Result<MoneyError, Money> add(Money other) {
|
||||
if (!currency.equals(other.currency)) {
|
||||
return Result.failure(
|
||||
new CurrencyMismatchError(currency, other.currency)
|
||||
);
|
||||
}
|
||||
return Result.success(
|
||||
new Money(amount.add(other.amount), currency)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Domain operation: multiply by factor.
|
||||
*/
|
||||
public Money multiply(int factor) {
|
||||
if (factor < 0) {
|
||||
throw new IllegalArgumentException("Factor cannot be negative");
|
||||
}
|
||||
return new Money(
|
||||
amount.multiply(java.math.BigDecimal.valueOf(factor)),
|
||||
currency
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formatted display.
|
||||
*/
|
||||
public String formatted() {
|
||||
return String.format("%s %s", currency, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed interface MoneyError permits CurrencyMismatchError {
|
||||
String message();
|
||||
}
|
||||
|
||||
public record CurrencyMismatchError(
|
||||
String from,
|
||||
String to
|
||||
) implements MoneyError {
|
||||
@Override
|
||||
public String message() {
|
||||
return String.format("Currency mismatch: %s vs %s", from, to);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Use Classes for Aggregates/Entities
|
||||
|
||||
Keep mutable aggregates and entities as classes for encapsulation:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Aggregate root - use class for mutability.
|
||||
* Package-private constructor forces use of factory.
|
||||
*/
|
||||
public class Account {
|
||||
private final AccountId id;
|
||||
private Money balance;
|
||||
private AccountStatus status;
|
||||
private final java.time.Instant createdAt;
|
||||
private java.time.Instant updatedAt;
|
||||
|
||||
/**
|
||||
* Private constructor - use factory method.
|
||||
*/
|
||||
private Account(
|
||||
AccountId id,
|
||||
Money initialBalance,
|
||||
AccountStatus status,
|
||||
java.time.Instant createdAt
|
||||
) {
|
||||
this.id = id;
|
||||
this.balance = initialBalance;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method in aggregate.
|
||||
*/
|
||||
public static Result<AccountError, Account> create(
|
||||
AccountId id,
|
||||
Money initialBalance
|
||||
) {
|
||||
if (initialBalance.isNegative()) {
|
||||
return Result.failure(
|
||||
new InvalidBalanceError(initialBalance)
|
||||
);
|
||||
}
|
||||
|
||||
Account account = new Account(
|
||||
id,
|
||||
initialBalance,
|
||||
AccountStatus.ACTIVE,
|
||||
java.time.Instant.now()
|
||||
);
|
||||
|
||||
return Result.success(account);
|
||||
}
|
||||
|
||||
// ✅ Getters with accessor method names (not get prefix)
|
||||
public AccountId id() { return id; }
|
||||
public Money balance() { return balance; }
|
||||
public AccountStatus status() { return status; }
|
||||
|
||||
/**
|
||||
* Invariant: Cannot withdraw from closed account
|
||||
* Invariant: Cannot withdraw more than balance
|
||||
*/
|
||||
public Result<AccountError, Void> withdraw(Money amount) {
|
||||
// Guard: Check status
|
||||
if (status == AccountStatus.CLOSED) {
|
||||
return Result.failure(new AccountClosedError(id));
|
||||
}
|
||||
|
||||
// Guard: Check amount
|
||||
if (amount.isNegativeOrZero()) {
|
||||
return Result.failure(new InvalidAmountError(amount));
|
||||
}
|
||||
|
||||
// Guard: Check balance
|
||||
if (balance.amount().compareTo(amount.amount()) < 0) {
|
||||
return Result.failure(
|
||||
new InsufficientFundsError(amount, balance)
|
||||
);
|
||||
}
|
||||
|
||||
// Execute state change
|
||||
this.balance = new Money(
|
||||
balance.amount().subtract(amount.amount()),
|
||||
balance.currency()
|
||||
);
|
||||
this.updatedAt = java.time.Instant.now();
|
||||
|
||||
return Result.success(null);
|
||||
}
|
||||
|
||||
// Equality based on ID (entity identity)
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (!(o instanceof Account account)) return false;
|
||||
return Objects.equals(id, account.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Account{" +
|
||||
"id=" + id +
|
||||
", balance=" + balance +
|
||||
", status=" + status +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sealed Interfaces for Type Hierarchies
|
||||
|
||||
Use sealed interfaces for error types and domain concepts:
|
||||
|
||||
```java
|
||||
/**
|
||||
* Sealed interface - only permitted implementations.
|
||||
*/
|
||||
public sealed interface AccountError permits
|
||||
AccountClosedError,
|
||||
InsufficientFundsError,
|
||||
InvalidAmountError,
|
||||
AccountNotFoundError {
|
||||
String message();
|
||||
}
|
||||
|
||||
/**
|
||||
* Only this class can implement AccountError.
|
||||
*/
|
||||
public record AccountClosedError(AccountId id) implements AccountError {
|
||||
@Override
|
||||
public String message() {
|
||||
return "Account closed: " + id;
|
||||
}
|
||||
}
|
||||
|
||||
// More implementations...
|
||||
|
||||
/**
|
||||
* Sealed with final subclasses.
|
||||
*/
|
||||
public sealed interface OrderStatus permits
|
||||
DraftStatus,
|
||||
PlacedStatus,
|
||||
ShippedStatus,
|
||||
DeliveredStatus,
|
||||
CancelledStatus {
|
||||
String description();
|
||||
}
|
||||
|
||||
public final record DraftStatus() implements OrderStatus {
|
||||
@Override
|
||||
public String description() {
|
||||
return "Draft - being composed";
|
||||
}
|
||||
}
|
||||
|
||||
public final record PlacedStatus(java.time.Instant placedAt) implements OrderStatus {
|
||||
@Override
|
||||
public String description() {
|
||||
return "Placed on " + placedAt;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Pattern Matching
|
||||
|
||||
### Instance Checking
|
||||
|
||||
```java
|
||||
// ❌ Old style
|
||||
public void process(OrderError error) {
|
||||
if (error instanceof InvalidAmountError) {
|
||||
InvalidAmountError e = (InvalidAmountError) error;
|
||||
System.out.println(e.amount());
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ New style - pattern matching
|
||||
public void process(OrderError error) {
|
||||
if (error instanceof InvalidAmountError e) {
|
||||
System.out.println(e.amount());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Switch with Pattern Matching
|
||||
|
||||
```java
|
||||
public String formatError(OrderError error) {
|
||||
return switch (error) {
|
||||
case InvalidAmountError e ->
|
||||
"Invalid amount: " + e.amount();
|
||||
case LineItemNotFoundError e ->
|
||||
"Line item not found: " + e.lineItemId();
|
||||
case OrderAlreadyPlacedError e ->
|
||||
"Order already placed: " + e.orderNumber();
|
||||
case OrderCancelledError e ->
|
||||
"Order cancelled: " + e.orderNumber();
|
||||
case EmptyOrderError ->
|
||||
"Cannot place order with no items";
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pattern matching with Result.
|
||||
*/
|
||||
public void handleResult(Result<OrderError, Order> result) {
|
||||
switch (result) {
|
||||
case Success(Order order) -> {
|
||||
System.out.println("Order created: " + order.orderNumber());
|
||||
}
|
||||
case Failure(InvalidAmountError e) -> {
|
||||
logger.warn("Invalid amount in order: " + e.amount());
|
||||
}
|
||||
case Failure(EmptyOrderError) -> {
|
||||
logger.warn("User attempted to place empty order");
|
||||
}
|
||||
case Failure(OrderError e) -> {
|
||||
logger.error("Unexpected order error: " + e.message());
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Record Pattern Matching
|
||||
|
||||
```java
|
||||
// Record patterns (Java 21+)
|
||||
public void processTransfer(Object obj) {
|
||||
if (obj instanceof Transfer(
|
||||
Money amount,
|
||||
AccountId from,
|
||||
AccountId to
|
||||
)) {
|
||||
System.out.println("Transfer " + amount + " from " + from + " to " + to);
|
||||
}
|
||||
}
|
||||
|
||||
// In switch expressions
|
||||
public String describeTransfer(Object obj) {
|
||||
return switch (obj) {
|
||||
case Transfer(Money amount, AccountId from, AccountId to) ->
|
||||
String.format("Transfer %s from %s to %s", amount, from, to);
|
||||
case Withdrawal(Money amount, AccountId account) ->
|
||||
String.format("Withdrawal %s from %s", amount, account);
|
||||
default -> "Unknown operation";
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Static Imports
|
||||
|
||||
Use static imports for readability:
|
||||
|
||||
```java
|
||||
// ❌ Verbose without static import
|
||||
Result<AccountError, Account> account = Result.success(newAccount);
|
||||
|
||||
// ✅ With static imports
|
||||
import static com.example.shared.result.Result.success;
|
||||
import static com.example.shared.result.Result.failure;
|
||||
|
||||
Result<AccountError, Account> account = success(newAccount);
|
||||
Result<AccountError, Void> error = failure(new AccountClosedError(id));
|
||||
```
|
||||
|
||||
### Import Aliases
|
||||
|
||||
```java
|
||||
// For classes with same name in different packages
|
||||
import com.example.domain.account.AccountError;
|
||||
import com.example.application.account.AccountError as AppAccountError;
|
||||
```
|
||||
|
||||
## Private Constructors + Public Static Factories
|
||||
|
||||
### Aggregate Pattern
|
||||
|
||||
```java
|
||||
/**
|
||||
* Aggregates use private constructor + public factory.
|
||||
* Ensures validation always happens.
|
||||
*/
|
||||
public class Order {
|
||||
private final OrderNumber number;
|
||||
private final CustomerId customerId;
|
||||
private final List<OrderLine> lineItems;
|
||||
private OrderStatus status;
|
||||
|
||||
/**
|
||||
* Private - access only via factory.
|
||||
*/
|
||||
private Order(
|
||||
OrderNumber number,
|
||||
CustomerId customerId,
|
||||
OrderStatus status
|
||||
) {
|
||||
this.number = number;
|
||||
this.customerId = customerId;
|
||||
this.lineItems = new ArrayList<>();
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new order (in DRAFT status).
|
||||
*/
|
||||
public static Result<OrderError, Order> create(
|
||||
OrderNumber number,
|
||||
CustomerId customerId
|
||||
) {
|
||||
// Validation before construction
|
||||
if (number == null) {
|
||||
return Result.failure(new InvalidOrderNumberError());
|
||||
}
|
||||
if (customerId == null) {
|
||||
return Result.failure(new InvalidCustomerIdError());
|
||||
}
|
||||
|
||||
Order order = new Order(number, customerId, OrderStatus.DRAFT);
|
||||
return Result.success(order);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reconstruct from database (internal use).
|
||||
* Skips validation since data is from trusted source.
|
||||
*/
|
||||
static Order reconstruct(
|
||||
OrderNumber number,
|
||||
CustomerId customerId,
|
||||
List<OrderLine> lineItems,
|
||||
OrderStatus status
|
||||
) {
|
||||
Order order = new Order(number, customerId, status);
|
||||
order.lineItems.addAll(lineItems);
|
||||
return order;
|
||||
}
|
||||
|
||||
/**
|
||||
* For tests - panics on error.
|
||||
*/
|
||||
public static Order mustCreate(String number, String customerId) {
|
||||
return create(
|
||||
new OrderNumber(number),
|
||||
new CustomerId(customerId)
|
||||
).orElseThrow(e ->
|
||||
new AssertionError("Failed to create test order: " + e.message())
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Value Object Pattern
|
||||
|
||||
```java
|
||||
/**
|
||||
* Value object - sealed to prevent subclassing.
|
||||
*/
|
||||
public final record Money(
|
||||
java.math.BigDecimal amount,
|
||||
String currency
|
||||
) {
|
||||
/**
|
||||
* Compact constructor validates.
|
||||
*/
|
||||
public Money {
|
||||
if (amount == null || currency == null) {
|
||||
throw new IllegalArgumentException("Cannot be null");
|
||||
}
|
||||
if (amount.signum() < 0) {
|
||||
throw new IllegalArgumentException("Amount cannot be negative");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for USD (common case).
|
||||
*/
|
||||
public static Money usd(long cents) {
|
||||
return new Money(
|
||||
java.math.BigDecimal.valueOf(cents, 2),
|
||||
"USD"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory for arbitrary currency.
|
||||
*/
|
||||
public static Result<MoneyError, Money> of(String amount, String currency) {
|
||||
try {
|
||||
return Result.success(
|
||||
new Money(new java.math.BigDecimal(amount), currency)
|
||||
);
|
||||
} catch (NumberFormatException e) {
|
||||
return Result.failure(new InvalidMoneyFormatError(amount));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must-variant for tests.
|
||||
*/
|
||||
public static Money mustUsd(long cents) {
|
||||
return usd(cents);
|
||||
}
|
||||
|
||||
public static Money mustOf(String amount, String currency) {
|
||||
return of(amount, currency)
|
||||
.orElseThrow(e ->
|
||||
new AssertionError("Test money construction failed: " + e.message())
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Package-Private for Entities
|
||||
|
||||
Hide child entities from outside aggregates:
|
||||
|
||||
```java
|
||||
package com.example.domain.order;
|
||||
|
||||
/**
|
||||
* Aggregate root - public.
|
||||
*/
|
||||
public class Order {
|
||||
private final List<OrderLine> lineItems;
|
||||
|
||||
/**
|
||||
* Public factory - creates Order and its children.
|
||||
*/
|
||||
public static Result<OrderError, Order> create(...) {
|
||||
return Result.success(new Order(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* Package-private - only Order and other aggregate classes access this.
|
||||
*/
|
||||
public Result<OrderError, Void> addLineItem(...) {
|
||||
// Internal validation
|
||||
OrderLine line = new OrderLine(...); // Package-private constructor
|
||||
lineItems.add(line);
|
||||
return Result.success(null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entity - package-private constructor.
|
||||
* Only Order aggregate can create it.
|
||||
*/
|
||||
public class OrderLine {
|
||||
private final OrderLineId id;
|
||||
private final ProductId productId;
|
||||
private int quantity;
|
||||
|
||||
/**
|
||||
* Package-private - only Order can use.
|
||||
*/
|
||||
OrderLine(
|
||||
OrderLineId id,
|
||||
ProductId productId,
|
||||
String name,
|
||||
Money unitPrice,
|
||||
int quantity
|
||||
) {
|
||||
this.id = id;
|
||||
this.productId = productId;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Package-private factory - used by Order.
|
||||
*/
|
||||
static OrderLine create(
|
||||
OrderLineId id,
|
||||
ProductId productId,
|
||||
String name,
|
||||
Money unitPrice,
|
||||
int quantity
|
||||
) {
|
||||
if (quantity <= 0) {
|
||||
throw new IllegalArgumentException("Quantity must be positive");
|
||||
}
|
||||
return new OrderLine(id, productId, name, unitPrice, quantity);
|
||||
}
|
||||
|
||||
// ✅ Package-private accessor (no public getter for modification)
|
||||
int getQuantity() { return quantity; }
|
||||
|
||||
/**
|
||||
* Package-private mutation - only Order calls this.
|
||||
*/
|
||||
void updateQuantity(int newQuantity) {
|
||||
if (newQuantity <= 0) {
|
||||
throw new IllegalArgumentException("Quantity must be positive");
|
||||
}
|
||||
this.quantity = newQuantity;
|
||||
}
|
||||
}
|
||||
|
||||
// Outside the package - cannot access OrderLine directly
|
||||
Order order = Order.create(...).success();
|
||||
// ❌ OrderLine line = new OrderLine(...); // Compile error
|
||||
// ❌ order.lineItems().get(0); // No public getter
|
||||
|
||||
// ✅ Access through Order aggregate only
|
||||
order.addLineItem(...);
|
||||
order.removeLineItem(...);
|
||||
```
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
| Element | Convention | Example |
|
||||
|---------|-----------|---------|
|
||||
| Class | PascalCase | `Account`, `Order`, `Transfer` |
|
||||
| Record | PascalCase | `Money`, `OrderNumber`, `CustomerId` |
|
||||
| Interface | PascalCase | `Repository`, `AccountError`, `OrderStatus` |
|
||||
| Variable | camelCase | `accountId`, `initialBalance`, `orderNumber` |
|
||||
| Constant | UPPER_SNAKE_CASE | `MAX_AMOUNT`, `DEFAULT_CURRENCY` |
|
||||
| Method | camelCase (no get/set prefix) | `balance()`, `withdraw()`, `transfer()` |
|
||||
| Enum | PascalCase values | `ACTIVE`, `DRAFT`, `PLACED` |
|
||||
|
||||
## Accessor Methods (Property-Style)
|
||||
|
||||
```java
|
||||
// ✅ Preferred in DDD - property-style accessors
|
||||
public class Account {
|
||||
private Money balance;
|
||||
|
||||
public Money balance() { // Property name, not getBalance()
|
||||
return balance;
|
||||
}
|
||||
|
||||
public AccountId id() { // Not getId()
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Avoid getter/setter naming in domain
|
||||
public class Account {
|
||||
public Money getBalance() { ... } // Too verbose for domain
|
||||
public void setBalance(Money m) { ... } // Never use setters
|
||||
}
|
||||
```
|
||||
|
||||
## Immutability
|
||||
|
||||
```java
|
||||
// ✅ Immutable record
|
||||
public record Money(
|
||||
java.math.BigDecimal amount,
|
||||
String currency
|
||||
) {
|
||||
// No setters, fields are final automatically
|
||||
}
|
||||
|
||||
// ✅ Defensive copy for mutable collection
|
||||
public class Order {
|
||||
private List<OrderLine> lineItems;
|
||||
|
||||
public List<OrderLine> lineItems() {
|
||||
return List.copyOf(lineItems); // Returns unmodifiable copy
|
||||
}
|
||||
|
||||
public void addLineItem(OrderLine line) {
|
||||
lineItems.add(line); // Internal modification only
|
||||
}
|
||||
}
|
||||
|
||||
// ❌ Avoid direct exposure of mutable collections
|
||||
public class Order {
|
||||
public List<OrderLine> getLineItems() { // Caller could modify!
|
||||
return lineItems;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
```java
|
||||
/**
|
||||
* Order aggregate root.
|
||||
*
|
||||
* Represents a customer's order with multiple line items.
|
||||
* State transitions: DRAFT -> PLACED -> SHIPPED -> DELIVERED
|
||||
* Or: DRAFT/PLACED -> CANCELLED
|
||||
*
|
||||
* Invariant: Cannot modify order after PLACED
|
||||
* Invariant: Cannot place order with zero line items
|
||||
* Invariant: Order total = sum of line item totals
|
||||
*
|
||||
* @see OrderLine
|
||||
* @see OrderRepository
|
||||
*/
|
||||
public class Order {
|
||||
|
||||
/**
|
||||
* Creates new order in DRAFT status.
|
||||
*
|
||||
* @param number unique order identifier
|
||||
* @param customerId customer who placed the order
|
||||
* @return new Order wrapped in Result
|
||||
* @throws nothing - errors are in Result type
|
||||
*/
|
||||
public static Result<OrderError, Order> create(
|
||||
OrderNumber number,
|
||||
CustomerId customerId
|
||||
) {
|
||||
...
|
||||
}
|
||||
|
||||
/**
|
||||
* Places the order (transitions DRAFT -> PLACED).
|
||||
*
|
||||
* Invariant: Can only place DRAFT orders
|
||||
* Invariant: Order must have at least one line item
|
||||
*
|
||||
* @return success if placed, failure with reason if not
|
||||
*/
|
||||
public Result<OrderError, Void> place() {
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices Summary
|
||||
|
||||
1. **Use records** for value objects, IDs, errors
|
||||
2. **Use classes** for aggregates and entities
|
||||
3. **Private constructors** on aggregates, **public factories** for validation
|
||||
4. **Sealed interfaces** for closed type hierarchies (errors, statuses)
|
||||
5. **Pattern matching** instead of instanceof + casting
|
||||
6. **Static imports** for Result.success/failure and common factories
|
||||
7. **Property-style accessors** (method name = property name)
|
||||
8. **No setters** in domain objects (return Result instead)
|
||||
9. **Defensive copies** for mutable collections
|
||||
10. **Package-private** for child entities (enforce aggregate boundaries)
|
||||
11. **Final records** by default
|
||||
12. **Compact constructors** for validation
|
||||
13. **Document invariants** in Javadoc
|
||||
14. **Use Result<E, T>** instead of throwing exceptions
|
||||
Loading…
Add table
Add a link
Reference in a new issue