Skip to content

Persistence

Repository Port

  • [Required] Feature から必要な永続化操作は Features/{Area}/{UseCase}/I{Area}{UseCase}Repository.cs に定義する。
csharp
public interface ICustomerSignupRepository
{
    Task<bool> EmailExistsAsync(EmailAddress email, CancellationToken ct);
}
  • [Required] Feature 専用の port として小さく定義する。
  • [Required] 汎用 CRUD repository を作らない。
  • [Required] interface 名は I{Area}{UseCase}Repository にする。
  • [Required] Infrastructure 側の実装名は port と対応する用途名にする。
  • [Contextual] 同じ Infrastructure repository class が複数の use case port を実装してよい。ただし各 port の contract は use case に必要な操作だけに絞る。
  • [Contextual] repository は use case の transaction boundary を開始・所有しない。単一永続化操作を atomic にするため provider transaction を内部実装として実行することはできるが、複数操作を束ねる transaction が必要な場合は use case / application service 側にユースケース固有の Transaction 抽象を置く。
  • [Required] 単純な作成 use case では、Handler が永続化前に重複確認や domain object 作成を行い、repository port は EmailExistsAsync(...)SaveAsync(entity, ct) のように必要な操作だけを持つ。
  • [Contextual] 永続化側で一意制約違反や採番結果など、use case が判断すべき結果が返る場合だけ {Area}{UseCase}PersistenceResult を使う。
  • [Contextual] 複数の永続化操作を一貫した transaction にする必要がある場合だけ、use case 専用の Transaction と TransactionScope interface を追加する。

例:

csharp
public interface ICustomerSignupRepository
{
    Task<bool> EmailExistsAsync(EmailAddress email, CancellationToken ct);

    Task SaveAsync(Customer customer, CancellationToken ct);
}

Transaction and Transaction Scope

  • [Required] Transaction boundary は repository 単位ではなく use case の実行単位として扱う。 Repository はオブジェクトのコレクションをエミュレートする port であり、DB transaction 制御の owner ではない。

  • [Required] ここでいう owner は use case 全体の開始・commit・rollback を決める責務を指す。Repository が単一 aggregate の read-modify-write を atomic にするため provider transaction を内部で実行することとは区別する。 Repository 実装が transaction を開始すると、複数 aggregate / 複数 repository を同じ整合性で更新する use case で transaction owner が曖昧になり、片方の repository に他 aggregate の更新責務が寄りやすくなる。

  • [Recommended] 原則として、1 transaction で更新する aggregate は 1 つにする。

  • [Contextual] 複数 aggregate の同期的な整合性が必要に見える場合は、先に aggregate boundary を見直す。

  • [Required] 即時整合性が不要なら、domain event と結果整合性で別 aggregate を更新する。

  • [Contextual] それでも同期的に複数 aggregate を更新する必要がある場合は、use case / application service がユースケース固有の Transaction を開始し、その TransactionScope 内で必要な操作を実行する。

  • [Required] I{Area}{UseCase}TransactionExecuteAsync だけを持ち、実行中の操作をI{Area}{UseCase}TransactionScopeへ渡すfeature-local portにする。

  • [Contextual] TransactionScopeは、そのユースケースのtransaction内で許可する読み書き操作だけを持つ。空のmarker contextにせず、別のrepositoryへcontextを引き回さない。

  • [Required] SDK / provider固有のdatabase、transaction、query型をFeatures側へ漏らさない。Infrastructure実装はprovider transactionを保持するScopeを生成し、実行時型判定やダウンキャストを使わない。

  • [Recommended] TransactionとTransactionScopeはユースケースごとに定義し、同じaggregateを扱うことだけを理由に共通化しない。操作と整合性境界が同一であることが確認できた場合だけ共有を検討する。

  • [Recommended] AsyncLocal や framework context などによる暗黙的な transaction 伝搬は、scope がコード上で追いにくくなるため原則避ける。採用する場合は利便性より可読性を優先して明示的に理由を残す。

  • [Required] Repository 内の transaction callback に業務判断を入れない。参加可否、満員、フェーズ制約、スコア更新などの判断と状態遷移は handler / domain model / domain service 側に置く。

  • [Required] 単一 aggregate に閉じた atomic read-modify-write が必要な場合でも、まず handler / domain model で状態遷移を表現できないか検討する。

  • [Contextual] providerの競合制御など実装上transactionが必要な場合はrepositoryがproviderの詳細を隠してよいが、repositoryがuse caseのtransaction boundaryを代表したり、業務判断をInfrastructureへ移したりしない。

単一aggregateのatomic read-modify-writeは次の順で選ぶ。

  • [Required] 競合を許容でき、providerのconditional updateまたはversion checkで保存時に検出できる場合は、Handlerがread、domain method、saveを行い、repositoryは競合をuse caseが判断できるpersistence resultとして返す。
  • [Contextual] provider transaction callbackの再実行中に同じaggregateを再読込する必要がある場合は、repositoryにUpdateAsyncのような業務callbackを渡さない。use case固有のTransaction / TransactionScopeを使い、scopeからdomain modelを取得し、Handlerがdomain methodを呼び、scopeへ保存する。
  • [Contextual] providerが単一documentのatomic transformやcompare-and-setを提供し、操作自体に業務判断を含まない場合は、repositoryの単一操作として隠してよい。
  • [Required] どの方式でも、満員、phase、権限、状態遷移などの業務判断はdomain modelまたはHandlerに残し、Infrastructure callbackへ移さない。