mirror of
https://github.com/s-frick/effigenix.git
synced 2026-03-28 10:19:35 +01:00
5.1 KiB
5.1 KiB
Degraded State Pattern
Problem
When domain invariants evolve in production systems, old data may violate new business rules:
// 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:
- Can be loaded from persistence despite missing new required fields
- Is flagged as degraded with clear indication of missing fields
- Blocks certain operations until brought to valid state
- 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
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:
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:
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.