1
0
Fork 0
mirror of https://github.com/s-frick/effigenix.git synced 2026-03-28 19:59:57 +01:00
effigenix/bin/.claude/skills/ddd-model/rules/degraded-state-pattern.md
2026-02-18 23:25:12 +01:00

178 lines
5.1 KiB
Markdown

# Degraded State Pattern
## Problem
When domain invariants evolve in production systems, old data may violate new business rules:
```java
// Version 1: Email optional
public class Customer {
private EmailAddress email; // nullable
}
// Version 2: Email becomes required (new invariant)
public static Result<CustomerError, Customer> create(...) {
if (email == null) {
return Result.failure(new EmailRequiredError()); // ❌ Old data breaks!
}
}
```
**Without a migration strategy, existing production data cannot be loaded.**
## Solution: Degraded State Pattern
Allow entities to exist in a "degraded state" where some invariants are temporarily violated. The entity:
1. **Can be loaded** from persistence despite missing new required fields
2. **Is flagged** as degraded with clear indication of missing fields
3. **Blocks certain operations** until brought to valid state
4. **Provides path to recovery** through explicit update operations
## When to Use
**Use when**:
- Adding new required fields to existing entities
- Tightening validation rules on production data
- Migrating data that doesn't meet current standards
- Gradual rollout of stricter business rules
**Don't use when**:
- Creating new entities (always enforce current invariants)
- Data corruption (fix at persistence layer instead)
- Temporary technical failures (use retry/circuit breaker instead)
## Implementation Pattern
### 1. Dual Factory Methods
```java
public class Customer {
private final CustomerId id;
private final String name;
private EmailAddress email; // New required field
private final boolean isDegraded;
/**
* Creates NEW customer (strict validation).
* Enforces all current invariants.
*/
public static Result<CustomerError, Customer> create(
CustomerId id,
String name,
EmailAddress email
) {
// Strict validation
if (email == null) {
return Result.failure(new EmailRequiredError(
"Email is required for new customers"
));
}
return Result.success(new Customer(id, name, email, false));
}
/**
* Reconstructs customer from persistence (lenient).
* Allows loading old data that doesn't meet current invariants.
*/
public static Customer fromPersistence(
CustomerId id,
String name,
EmailAddress email // Can be null for old data
) {
boolean isDegraded = (email == null);
if (isDegraded) {
log.warn("Customer loaded in degraded state: id={}, missing=email", id);
}
return new Customer(id, name, email, isDegraded);
}
private Customer(CustomerId id, String name, EmailAddress email, boolean isDegraded) {
this.id = id;
this.name = name;
this.email = email;
this.isDegraded = isDegraded;
}
}
```
### 2. Operation Gating
Block operations that require valid state:
```java
public Result<CustomerError, Order> placeOrder(OrderDetails details) {
// Guard: Cannot place order in degraded state
if (isDegraded) {
return Result.failure(new CustomerDegradedError(
"Please complete your profile (add email) before placing orders",
List.of("email")
));
}
// Normal business logic
Order order = new Order(details, this.email);
return Result.success(order);
}
```
### 3. Recovery Path
Provide explicit operations to exit degraded state:
```java
public Result<CustomerError, Void> updateEmail(EmailAddress newEmail) {
if (newEmail == null) {
return Result.failure(new EmailRequiredError("Email cannot be null"));
}
this.email = newEmail;
this.isDegraded = false; // Exit degraded state
log.info("Customer email updated, exited degraded state: id={}", id);
return Result.success(null);
}
public boolean isDegraded() {
return isDegraded;
}
public List<String> getMissingFields() {
List<String> missing = new ArrayList<>();
if (email == null) {
missing.add("email");
}
return missing;
}
```
## Best Practices
**Do**:
- Use dual factory methods (`create()` strict, `fromPersistence()` lenient)
- Log when entities load in degraded state
- Provide clear error messages with missing fields
- Allow read operations and deposits (recovery paths)
- Block critical operations until valid
- Provide explicit recovery operations
- Monitor degraded entity count in production
**Don't**:
- Allow new entities to be created in degraded state
- Silently accept degraded state without logging
- Block all operations (allow recovery paths)
- Forget to provide user-facing recovery UI
- Leave entities degraded indefinitely (migrate!)
- Use degraded state for temporary failures
## Summary
The Degraded State Pattern enables:
-**Zero-downtime schema evolution**
-**Gradual migration of invariants**
-**Clear user communication** about incomplete data
-**Explicit recovery paths** to valid state
-**Production safety** during schema changes
Use it when domain rules evolve and existing production data doesn't meet new standards.