# 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 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 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 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 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 getMissingFields() { List 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.