refactor: ITransactionWriteRepository — eliminate catch-to-check in command handlers (Phase 3.4)#51
Merged
jorge07 merged 1 commit intojorge07:masterfrom Feb 22, 2026
Conversation
…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
approved these changes
Feb 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Every command handler that needed to verify aggregate existence was using exception control flow:
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):EventStoreTransactionRepository(Infrastructure): wrapsEventSourcing.EventStore— the catch-to-check lives inexists()exactly once.All four command handlers (Create, Confirm, Fail, Refund) now inject
ITransactionWriteRepositoryviainfrastructure.transaction.writeRepositoryand use clean readable checks:Test double:
InMemoryWriteRepositoryshares the sameInMemoryEventStoreas the existing write-side test double, so handlers and assertions see the same state.Tests
41/41 pass.