# The problem: waiting on third parties inside a transaction
An HTTP handler often wraps a whole request in a transaction via an injected executor. That pattern works when the request's work talks only to the database. It breaks down when the use case must call an external service (payment gateway, file store, long-running generator) before it writes.
If you open a transaction and then await an external call, the connection and any row locks remain held for the duration of that external wait. The result is lower throughput: pooled connections busy waiting, locked rows blocking other requests.
# Why moving the scope matters
The simple fix is: put only database operations inside the transaction. But the controller that opens the original executor cannot make that decision because it doesn't know which part of the use case is the write and which part is the wait. That knowledge lives in the use case.
If the use case directly depends on the concrete transaction executor (for example, a TypeOrmTransactionExecutor), the application layer becomes coupled to the ORM. That coupling makes the use case harder to test and violates the goal of keeping domain code persistence-agnostic.
# The solution: a UnitOfWork domain port
Declare a minimal UnitOfWork interface in the domain. The contract in the article is short and focused:
- UnitOfWork exposes run (props: UnitOfWorkProps): Promise and should join an existing unit in the current async context when present.
This interface gives use cases a collaborator that can run a block as a single unit without tying them to TypeORM, MongoDB, or any implementation detail.
# How this changes use-case code
Instead of the controller wrapping a whole handler, the use case receives a UnitOfWork and invokes unit.run around the database-writing portion. The external call (the wait) happens outside the unit. That pattern keeps transactions short and scoped to DB operations while letting the use case decide where to start and stop the unit.
# Verifying the abstraction: two implementations
To know whether the UnitOfWork generalizes, implement the interface for multiple engines and run the same use-case tests against each. The article shows implementations over PostgreSQL (TypeORM) and MongoDB. Running the same tests on both is the only reliable check that the contract is useful rather than just renaming the same executor.
Tests also reveal differences between engines that the interface cannot hide. Those differences matter for correctness and performance and should inform whether the abstraction is safe to trust in a given context.
# Practical takeaways
- Keep transactions short: never await slow external services while holding a DB transaction.
- Push transaction-scope decisions down into the use case, but avoid injecting concrete ORM executors there.
- Declare a tiny UnitOfWork interface in the domain and implement it per persistence engine.
- Validate the abstraction by running the same tests against each implementation and inspect mismatches.
The pattern aligns use-case responsibility with transaction scope, restores testability, and surfaces engine differences through concrete tests rather than hiding them behind a single executor implementation.