DDD における timestamp(createdAt / updatedAt)の扱い方 ― ドメインと永続化の分離
DDD(ドメイン駆動設計)でエンティティを設計していると、つい反射的にcreatedAt や updatedAtを持たせてしまいがちです。
しかし、それらは本当にドメインの関心ごとなのでしょうか?
こんにちは、しがないエンジニアの k_y16 です。
本記事では、timestamp(createdAt / updatedAt)を
ドメインから分離すべき理由 と、責務の分離をどう実現するか
をまとめます。
createdAt / updatedAt は何を表しているのか?
「申込」を例にすると、そこには2種類の "日時" が存在します。
ドメインの事実
-
appliedAt
→ ビジネス的な意味を持つ。「この申込はいつ行われたのか?」
永続化の都合(システムログ)
-
createdAt / updatedAt
→ 「DB の行がいつ INSERT / UPDATE されたか」という技術情報。
見た目は似ていますが、本質はまったく異なります。
timestamp をドメインロジックに使う危険性
次のように createdAt をドメインルールに使ってしまうと危険です。
if (application.createdAt < cutoffDate) {
// 古い申込なので〜
}
この瞬間、以下の問題が発生します。
- バッチでデータを再投入するとロジックの意味が変わる
- 監査でコピーしただけで「新しい扱い」になる
- DB の挙動がビジネスルールに影響する
つまり 永続化の都合がドメインを侵食する 状態になります。
結論:timestamp はドメインの関心ごとではない
- ドメインが扱うべきは 意味のある日時(appliedAt など)
- createdAt / updatedAt は システムログでありドメイン知識ではない
したがって timestamp は エンティティの本質部分から切り離すべき
という結論に至りました。
timestamp の扱い方:2つのパターン
パターン1:シンプルに Entity から排除する
DDD では "モデルはドメインの言葉で語られるべき" と言われます。
createdAt / updatedAt はドメイン語彙には属さないため、潔く排除します。
export class Application {
readonly id: ApplicationId | null
readonly appliedAt: Date
}
このアプローチのメリット:
- モデルの純度が高い
- テストが書きやすい
- 永続化方式の変更に強い
- 不要な責務が入り込まない
もっとも DDD 的に純粋な選択肢です。
パターン2:システム画面(UI)で必要になった場合だけ付与する
実務では、管理画面や一覧画面で timestamp が必要になるケースがあります。
- 登録日順にソートしたい
- 更新日時を表示したい
このような UI / アプリケーション層の都合が生じたときに限り、エンティティに "付加情報" として timestampを持たせます。
export type AuditInfo = {
readonly createdAt: Date
readonly updatedAt: Date
}
export class Application {
readonly id: ApplicationId | null
readonly appliedAt: Date
readonly audit?: AuditInfo // 永続化済み & UI で必要なときだけ付く
}
このパターンのポイント
- ドメインロジックは audit に依存しない
- audit を付与するのは リポジトリ / アプリケーション層
- 必要ないケースでは audit を付けない(= パターン1のまま)
これは「逃げ」ではなく、DDD 的に許容されるメタ情報として audit を扱い、timestamp のためだけにクエリサービスや DTOを増やすコストを避ける現実解 だと考えています。
なお、この判断についての私見(IMO)
Repository や ReadModel
の境界についてはさまざまな流派がありますが、個人的には次のように捉えています。
- Repository は「ドメインの一貫した状態」を返すのが基本
- 検索・JOIN・ページングは UI 都合 が強く、厳密にやるならReadModel/クエリサービス向き
- ただし audit を Entity にオプションで持たせておけば、Repositoryの戻り値だけで UI の要求も自然に満たせる
- Entity に持たない場合は、UI の都合のためだけに
- ReadModel
- DTO
- QueryServiceを追加する必要があり、timestampのためだけにコードが増えてしまうことがある
そのためパターン2は、
ドメイン純度と現場の実装負荷のバランスを取った現実的な選択肢
として「十分アリ」だと思っています。
この発想に至った背景
最初の違和感は、
- 永続化前の createdAt / updatedAt が null になる型
- その null が「業務の未決定」ではなく「DB保存前の技術都合」であること
ここから、
- ドメインの日時はビジネス語彙として意味づけるべき
- timestamp はログでありビジネス知識ではない
- ログとドメインは分離したほうがよい
という整理が自然と進みました。
まとめ
- createdAt / updatedAt は 永続化の都合(システムログ)
- ドメインが扱うのは ビジネス的意味を持つ日時のみ
- timestamp はエンティティから排除するのが基本方針
- ただし UI が要求する場合は audit として 許容範囲内で付与
- パターン2は、ドメイン純度と開発効率を両立する現実解
timestamp は便利ですが、 "便利だから入れる" ではなく "意味のある情報だけ残す" という DDD の基本に立ち返ると、モデルが驚くほどスッキリします。
ドメインの純度を保つためにも、timestampの扱いは一度見直してみる価値があります。
スペースを簡単に貸し借りできるサービス「スペースマーケット」のエンジニアによる公式ブログです。 弊社採用技術スタックはこちら -> whatweuse.dev/company/spacemarket
Discussion