Skip to content

Add IdempotentLedger decorator for automatic duplicate handling #4

@mnavarrocarter

Description

@mnavarrocarter

Problem

Currently, the ledger throws ConstraintViolation exceptions with AccountAlreadyExists and TransferAlreadyExists error codes when attempting to create duplicate accounts or transfers. While this is correct behavior, it requires users to manually handle these errors in their application code for idempotent operations.

For retry scenarios and distributed systems, users need to write boilerplate code like:

try {
    $ledger->execute($createAccount);
} catch (ConstraintViolation $e) {
    if ($e->errorCode === ErrorCode::AccountAlreadyExists) {
        // Already exists, that's fine
        return;
    }
    throw $e;
}

Proposed Solution

Add an IdempotentLedger decorator that wraps the standard ledger and automatically suppresses AccountAlreadyExists and TransferAlreadyExists errors. This gives users the choice of how to handle duplicates:

  • Default behavior: Throw on duplicates (current behavior)
  • Idempotent behavior: Silently ignore duplicates (new decorator)

Implementation

final class IdempotentLedger implements Ledger
{
    public function __construct(
        private readonly Ledger $ledger,
    ) {}

    public function execute(Command ...$commands): void
    {
        try {
            $this->ledger->execute(...$commands);
        } catch (ConstraintViolation $e) {
            // Suppress duplicate errors
            if ($e->errorCode === ErrorCode::AccountAlreadyExists ||
                $e->errorCode === ErrorCode::TransferAlreadyExists) {
                return;
            }
            throw $e;
        }
    }
}

Usage

// Standard ledger - throws on duplicates
$ledger = new StandardLedger($accounts, $transfers, $accountBalances);

// Idempotent ledger - silently ignores duplicates
$idempotentLedger = new IdempotentLedger($ledger);

// Safe to retry without error handling
$idempotentLedger->execute($createAccount);
$idempotentLedger->execute($createAccount);  // No error!

Benefits

  1. Explicit choice: Users choose between strict (default) and idempotent behavior
  2. Clean code: No boilerplate error handling for retry scenarios
  3. Decorator pattern: Follows existing patterns in the codebase
  4. Backward compatible: Doesn't change existing behavior
  5. Composable: Can be combined with TransactionalLedger

Acceptance Criteria

  • Create IdempotentLedger class implementing Ledger interface
  • Suppress AccountAlreadyExists errors
  • Suppress TransferAlreadyExists errors
  • All other errors should propagate normally
  • Add unit tests covering:
    • Duplicate account creation (should not throw)
    • Duplicate transfer creation (should not throw)
    • Other constraint violations (should throw)
    • Multiple commands with some duplicates
  • Add documentation example in "Working with the Library" guide
  • Update error handling section to mention this decorator as an option

Open Questions

  1. Should the decorator log when it suppresses an error? (for debugging/monitoring)
  2. Should it be configurable which error codes to suppress?
  3. Should it work with batched commands (multiple commands in one execute call)?

Related

  • Documentation already updated to show manual handling pattern
  • This would be an optional convenience for users who want automatic idempotency

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions