Imported from rito528/dotfiles (
config/agents/skills/domain-design-safety-discipline/SKILL.md). Install upstream withnpx skills add rito528/dotfiles --skill domain-design-safety-discipline. Copyright stays with the author.
安全なドメイン設計の規律
基本方針
正しい使い方が自然になり、誤った使い方が書きにくい設計を優先する。
呼び出し側の注意力、コメント、ドキュメント、テストだけに依存しない。暗黙の順序、事前チェック、検証済みであることは、可能な限り型、関数、戻り値、境界で表現する。
型と境界を変える前の確認
型と境界の齟齬は、実装後に直すほど高くつく。型・境界・責務を追加・削除・変更する前に確認する。
集約は強整合性の単位で決める
集約は、意味のあるデータのまとまりであり、強整合性が要求される単位である。
- 集約境界を決める前に、強整合性が必要な操作を列挙し、そこから境界を導く
- 保存の都合から境界を導かない。別テーブルや別 map に分けて保存できることは、整合性の単位を分けてよい理由にならない。保存方法と整合性境界は別の問い
役割に必要な情報を持てるか
概念に役割(判定、検証、変換、状態遷移の起点など)を担わせる前に、その概念が役割の遂行に必要な情報を自分の境界内で持てるか確認する。
持てない場合、その場で代替案を出し始めず、選択肢を並べて比較する: 必要な情報を持つ別概念を立てる / 境界を引き直す / 役割を担う仕組みの側を変える / 仕様上の制約として記録して許容する。
名前と意味を分ける
同じ名前は同じ意味を保証しない。操作や型を「何が、何に対して、何を起こすか」の自然言語で言い直し、同名同士で意味が一致するか確認する。
- 同じ操作名が、対象や粒度の違う操作(対象そのものへの操作と、その配下の要素への操作など)を指していないか
- 追加・改名する型は、新しいドメイン概念か、既存概念の別名か
型の役割を 1 つに言い切る
型を追加・削除・改名する前に、その型の役割を 1 つ選ぶ: 永続化される対象 / 操作の前提を表す値 / 判定のためだけの一時的な入力 / 処理途中の作業データ / 層境界の受け渡し用。
複数に当てはまる、またはどれとも言えないなら、概念が混ざっている。判定のためだけの一時的な値を domain / repository 境界に出す場合は、設計として妥当か疑う。
正しい使い方が自然になる API
避けるもの:
- 特定の順番で呼ばないと壊れる API
- 呼び出し前に別の関数でチェックが必要な API
- 不正な状態を作れてしまう struct / enum
boolや primitive の組み合わせから意味を推測させる API- コメントで注意しないと危険な public method
考えること:
- 事前チェックを生成関数や constructor に寄せられないか
- 戻り値で次に可能な操作を制限できないか
- enum の variant で状態を分け、不正な組み合わせを消せないか
- private field と public constructor で不変条件を守れないか
Always-valid domain model
ドメイン上ありえない状態は、可能な限り型の上で作れないようにする。
- 検証を handler や usecase に散らさない
- 値オブジェクト、集約、生成関数、domain service に不変条件を寄せる
String/Vec/ primitive 型をそのまま使う前に、ドメイン制約を持つ型にできないか考える- 「作った後にチェックする」より、「チェック済みの値しか作れない」形を優先する
- ただし、意味の薄いラッパー型や、1 箇所でしか価値のない型を増やしすぎない
判断基準:
- その型は不正状態を実際に消しているか
- その型名はドメイン語彙として意味があるか
- その検証は複数箇所へ散らばる可能性があるか
- テストや呼び出し側の注意を減らしているか
型の配置と凝集度
型をどこに定義するかは、型の種類ではなく、ドメイン上の所有者で決める。
値オブジェクトや ID 型は、それを構成要素として持つ集約と同じモジュールに置く。UserId は User 集約の一部であり、OrderId は Order 集約の一部である。「identifier である」という形の類似は、まとめて置く理由にならない。
避けるもの:
- ID 型を
identifiersのような 1 つのファイルへまとめて定義する types/values/newtypesなど、型の種類だけを共通点にしたモジュール- 他の集約から参照されるという理由だけで、型を共有モジュールへ移す
- 小さな型が集約のそばに並ぶのを嫌って、整理のために種類別ファイルへ寄せる
考えること:
- その型は、どの集約・どの概念の一部として説明できるか
- 集約間の参照は ID で行い、ID 型の定義は所有する集約側に置いたまま、参照する側が import できないか
- モジュールを分けるなら、同じ理由で変更されるものが近くに集まる分け方になっているか
- 依存の向きが気になる場合も、共有モジュールへ逃がす前に、所有者のそばに置いたまま解決できないか
ドメイン語彙で説明できるか確認する
設計判断や条件分岐を置く前に、それをドメインの言葉で説明できるか確認する。
技術的な都合の言葉だけで説明している場合、ドメイン上の概念やルールを見落としている可能性がある。status、flag、type、mode などの汎用的な名前に寄せる前に、その区別がドメイン上は何を意味するのかを考える。
考えること:
- その条件は、業務上・プロダクト上は何と呼ばれるものか
- 利用者、運用者、ドメインの専門家に説明するとき、どの語を使うか
- 技術的な状態名ではなく、ドメイン上の状態や段階として名前を付けられないか
- 「更新する」「無効にする」「許可する」ではなく、ドメイン上は何が起きているのか
- 実装都合の分類が先に立って、ドメイン上の重要な区別を潰していないか
- ドメインの言葉で説明できない場合、それは本当に domain 層へ置くべき概念か
可視性・操作可能性
可視性や操作可能性は、最初から application 層の手続きとして扱うものとは限らない。
「誰が見えてよいか」「誰が操作してよいか」「どの状態なら操作できるか」は、単なる外側の制御ではなく、ドメイン仕様であることが多い。handler や usecase に条件分岐を書く前に、それがドメインの言葉で説明できるルールかを確認する。
避けるもの:
- handler / controller だけで、可視性や操作可否の判断を完結させる
- usecase の末端で、取得結果を削るだけにする
- presentation 層に「見えてよいか」「操作してよいか」の判断を漏らす
- 可視性や操作可否の漏れをテストだけで防ごうとする
- 取得後に filter するだけで、本来取得してはいけないデータを扱う
考えること:
- そのルールは application 層の制御か、ドメイン上の制約か
- ドメイン仕様なら、集約、domain service、policy、query model のどこで表現するのが自然か
- 操作可能な状態と操作不能な状態を、型や戻り値で区別できないか
- repository / query の段階で可視性条件を表現すべきか
- 操作の前提条件を呼び出し側の記憶に頼らない API にできないか
ドメイン語彙で操作を表す
ドメイン上の操作は、できるだけドメインの言葉で表現する。
望ましい例:
post_messagesubmit_answerpublisharchiveinvite_membermark_as_read
避けたい例:
update_xxx_fieldset_statusmodify_recordapply_flag
外側の usecase に手続きが散らばっている場合、その操作を集約や domain service のメソッドとして表現できないか考える。
ただし、巨大な集約を無理にすべてロードしてまで、自然なメソッドの形にしない。整合性境界と性能上の境界は分けて考える。
層境界と型の流出
境界を越える型を混ぜない。
- infra の型を domain / application に漏らさない
- DB entity、HTTP request / response、外部 API DTO は境界で変換する
- usecase を presentation の都合に合わせたレスポンスの形へ寄せすぎない
- presentation 層にドメイン判断、可視性判断、永続化都合を漏らさない
- mapping は面倒でも、境界を明確にするために分離する
判断基準:
- この型はどの層の都合を表しているか
- 外部 API や DB schema の変更が domain に波及しないか
- presentation の表示都合が usecase や domain に入り込んでいないか
- mapping を省いたことで、境界が曖昧になっていないか
差分レビュー
実装後、差分に対して行う。差分と要求だけを渡された第三者でも同じ結果になるように、対象を列挙してから、対象ごとの問いに答える形で進める。誰がこのレビューを行うかは、このスキルでは決めない。実装の進め方を管理する側に従う。
手順:
- 差分から対象を列挙する: 新規・変更した型 / 新規・変更した public 関数・メソッド / 層境界をまたぐ箇所 / 追加・変更した集約境界。
- 対象ごとに下の問いに答える。「問題なし」で済ませず、根拠を 1 行で言い切る。言い切れない対象は、本文の該当節へ戻って設計から見直す。
- 問題が見つかった対象は修正し、修正した対象にもう一度この手順を適用する。
型ごとの問い:
- 所有者: どの集約・どの概念の一部として説明できるか。定義は所有者と同じモジュールに置かれているか
- 役割: 「型の役割を 1 つに言い切る」の 5 分類のどれか 1 つで言い切れるか。役割の遂行に必要な情報を自分の境界内で持てているか。判定のためだけの一時的な値が domain / repository 境界に出ていないか
- 不変条件: どこで守られているか。生成関数・constructor・private field のどこでも守られていないなら、不正な状態を作れる型になっていないか。検証が handler / usecase 側に書かれていないか
- 存在意義: その型は不正状態を実際に消しているか。意味の薄いラッパー型を増やしていないか
public 関数・メソッドごとの問い:
- 呼び出し側が順序や事前チェックを覚える必要があるか。あるなら「正しい使い方が自然になる API」へ戻る
- 名前はドメイン語彙か。
set_/update_/modify_系の名前なら、ドメイン上は何が起きているのかを言い直す - 同名の操作が、対象や粒度の違う別の意味を指していないか
層境界をまたぐ箇所ごとの問い:
- 越えている型は、どの層の都合を表しているか。DB entity / HTTP request・response / 外部 API DTO が domain / application に入っていないか
- 可視性・操作可否の判断はどの層にあるか。handler / presentation だけで完結していないか。ドメイン仕様なら domain 側で表現されているか
集約境界を追加・変更した場合の問い:
- 境界は強整合性が必要な操作から導いたか。保存の都合や性能上の都合から導いていないか
機械的に確認できる項目(対象の列挙とあわせて確認する):
identifiers/types/valuesのような、型の種類でまとめたファイルを新設していないか- domain 層のシグネチャに
String/bool/ primitive の引数・戻り値が増えていないか status/flag/type/modeのような汎用名のフィールドや分岐が増えていないか。増えているなら、ドメイン上の何の区別かを言い直す