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

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:

  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

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.