mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 22:39:58 +01:00
178 lines
5.1 KiB
Markdown
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.
|