Unit of Workは、複数の永続化変更をまとめ、成功時にcommit、失敗時にrollbackするパターンです。

Transactionの単位をそろえる
注文の状態変更と在庫の減算を別々に保存すると、失敗時に片方だけが残ります。同じDBで一体なら、1つのtransactionへまとめます。
begin
Orderを確定
Stockを減算
PaymentRecordを追加
commit
失敗 → rollback
Unit of Workは、ユースケースへtransaction境界を提供します。
interface UnitOfWork {
run<T>(work: (repos: Repositories) => Promise<T>): Promise<T>;
}
await unitOfWork.run(async ({ orders, stocks }) => {
const order = await orders.findById(orderId);
const stock = await stocks.findByProductId(order.productId);
order.confirm();
stock.allocate(order.quantity);
await orders.save(order);
await stocks.save(stock);
});
runは成功時にcommit、例外時にrollbackし、同じconnectionを各Repositoryへ共有します。
Repositoryとの役割分担
| 要素 | 主な役割 |
|---|---|
| Repository | 個別のAggregateを取得・保存する |
| Unit of Work | 複数Repositoryの変更を同じ作業単位にまとめる |
| DB Transaction | DB上でcommit・rollbackを実現する |
Repositoryごとにtransactionを開始すると、全変更をまとめて戻せません。Unit of Workへ業務ルールも書きません。
ORMに変更追跡やtransaction callbackがあれば、別classを重ねる必要はありません。
外部APIはrollbackできない
DB transaction中に決済APIを呼び、あとでDBをrollbackしても、外部で成立した決済は自動では戻りません。メール送信やmessage publishも同じです。
DB rollback ≠ 決済取消 ≠ 送信済みメールの取消
異なるシステムでは、Outbox、idempotency key、再試行、補償処理を検討します。Unit of Workだけでは解決しません。
境界を決める注意点
- transaction中に利用者入力や遅い外部APIを待たない
- deadlock時に安全に再試行できるか確認する
- 例外を握りつぶして誤ってcommitしない
読み取りや1回の保存だけなら、明示的なUnit of Workは不要な場合があります。
まとめ
- Unit of Workは複数変更を1つのtransaction単位へまとめる
- 成功時にcommitし、失敗時にrollbackする
- Repositoryは個別の保存、Unit of Workは全体の境界を担当する
- DB外の決済・メール・messageはrollbackできない
個別保存はリポジトリパターン、複数システムは分散トランザクションを参照してください。