Skip to content

refactor: ITransactionWriteRepository — eliminate catch-to-check in command handlers (Phase 3.4)#51

Merged
jorge07 merged 1 commit intojorge07:masterfrom
josecarlospeer-cloud:feat/write-repository-exists
Feb 22, 2026
Merged

refactor: ITransactionWriteRepository — eliminate catch-to-check in command handlers (Phase 3.4)#51
jorge07 merged 1 commit intojorge07:masterfrom
josecarlospeer-cloud:feat/write-repository-exists

Conversation

@josecarlospeer-cloud
Copy link
Contributor

Problem

Every command handler that needed to verify aggregate existence was using exception control flow:

try {
    await eventStore.load(id);
    // exists — conflict
} catch (AggregateRootNotFoundException) {
    // doesn't exist — proceed
}

Violation: Command handlers orchestrate; they should not query to make decisions using exceptions as flow control.

Source: Vernon (IDDD), p. 356. Also M2 in the billing-api evolution roadmap.

Consequence: The pattern is scattered across every command handler, and the EventStore (infrastructure) is injected directly into Application layer code.


Fix

ITransactionWriteRepository (Domain):

exists(id: TransactionId): Promise<boolean>;
load(id: TransactionId): Promise<Transaction>;
save(transaction: Transaction): Promise<void>;

EventStoreTransactionRepository (Infrastructure): wraps EventSourcing.EventStore — the catch-to-check lives in exists() exactly once.

All four command handlers (Create, Confirm, Fail, Refund) now inject ITransactionWriteRepository via infrastructure.transaction.writeRepository and use clean readable checks:

if (await this.writeModel.exists(id)) throw new ConflictException(...)
if (!await this.writeModel.exists(id)) throw new NotFoundException(...)

Test double: InMemoryWriteRepository shares the same InMemoryEventStore as the existing write-side test double, so handlers and assertions see the same state.


Tests

41/41 pass.

…ommand handlers (Phase 3.4)

Phase 3.4 from the evolution roadmap.

Violation: Create handler (and Confirm/Fail/Refund) used exception
control flow to check aggregate existence:

  try { await eventStore.load(id) } catch (AggregateRootNotFoundException) {}

This mixes queries with writes and scatters the catch-to-check pattern
across every command handler that needs to verify existence first.

Source: Vernon (IDDD), p. 356 — "Command handlers orchestrate; they do
not query to make decisions."

Fix:

ITransactionWriteRepository (Domain):
  - exists(id): Promise<boolean>
  - load(id): Promise<Transaction>
  - save(transaction): Promise<void>

EventStoreTransactionRepository (Infrastructure):
  - Wraps EventSourcing.EventStore<Transaction>
  - Encapsulates the catch-to-check in exists() — one place, tested once

All command handlers (Create, Confirm, Fail, Refund) now inject
ITransactionWriteRepository via 'infrastructure.transaction.writeRepository'
and call exists() for clean, readable flow:

  if (await this.writeModel.exists(id)) throw ConflictException
  if (!await this.writeModel.exists(id)) throw NotFoundException

Create handler: error counter retained for infrastructure save failures.

Test doubles:
  InMemoryWriteRepository implements ITransactionWriteRepository;
  shares the same InMemoryEventStore as InMemoryTransactionRepository
  so both write-side handlers and test assertions see the same state.

41/41 tests pass.
@jorge07 jorge07 merged commit 63191dd into jorge07:master Feb 22, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants