RESTful APIデータモデリングにおけるDDD:集約と値オブジェクトのAPI表現
はじめに
RESTful APIの設計において、どのようなデータ構造をリソースとして公開するかは非常に重要な判断です。システムの内部構造やビジネスロジックと密接に関わる部分であり、その設計の良し悪しがAPIの使いやすさ、保守性、そしてシステム全体の健全性に大きく影響します。特に、システムが複雑になるにつれて、データ構造の設計に迷うことは少なくありません。
ここでは、ドメイン駆動設計(Domain-Driven Design; DDD)の概念をRESTful APIのデータモデリングに応用する考え方について解説します。DDDは複雑なビジネスドメインを扱うための設計手法であり、その中で定義される「集約」や「値オブジェクト」といった概念は、APIリソースの適切な粒度や構造を検討する上で非常に参考になります。
ドメイン駆動設計(DDD)における主要な概念(APIモデリングの視点から)
まず、RESTful APIのデータモデリングに関連するDDDの主要な概念を簡単に整理します。
- ドメイン (Domain): システムが解決しようとする対象領域、すなわちビジネスそのものです。
- ドメインモデル (Domain Model): ドメインにおける概念やビジネスルールを表現したモデルです。ここにビジネスの複雑性が凝縮されます。
- エンティティ (Entity): システム内で識別子(ID)によって区別されるオブジェクトです。時間経過や属性の変化に関わらず、そのIDによって同一性が保たれます(例: 顧客、注文)。
- 値オブジェクト (Value Object): その属性によってのみ定義されるオブジェクトで、識別子を持ちません。属性の組み合わせが同じであれば、同じ値オブジェクトとみなされます。不変(Immutable)であることが推奨されます(例: 住所、金額、期間)。
- 集約 (Aggregate): 関連するエンティティや値オブジェクトをまとめた境界単位です。集約内のオブジェクトは一貫性の境界を共有し、外部からは集約ルート(Aggregate Root)と呼ばれる特定のエンティティを通してのみアクセスや変更が行われます。集約は、データの不整合を防ぐための強力な概念です(例: 注文とその注文明細の集まりを「注文集約」とする)。
- 境界づけられたコンテキスト (Bounded Context): 特定のドメインモデルが明確に定義され、適用される境界です。異なる境界づけられたコンテキストでは、同じ概念(例えば「顧客」)でも異なる意味やモデルを持つことがあります。
ドメインモデルからAPIリソースへのマッピング
DDDで設計されたドメインモデルをRESTful APIのリソースとして公開する際、以下のマッピングが基本的な考え方となります。
-
集約をAPIリソースの単位とする: 最も基本的なアプローチは、DDDの集約をAPIのリソース単位として扱うことです。集約ルートをリソースの識別子とし、集約全体をリソース表現の対象とします。これにより、APIを通じてデータの整合性を保ちやすくなります。
- 例: 顧客集約 (
Customer
とそのAddress
、ContactInfo
など) →customers/{customerId}
というリソース。 - 例: 注文集約 (
Order
とそのOrderItems
、ShippingInfo
など) →orders/{orderId}
というリソース。
この考え方によれば、APIクライアントは集約ルートのリソースを通じて、その集約内の全ての関連データにアクセスしたり、集約全体に対する操作(生成、更新、削除)を行ったりすることが一般的になります。
- 例: 顧客集約 (
-
値オブジェクトのAPI表現: 値オブジェクトは識別子を持たないため、単独のリソースとして公開されることは稀です。通常は、それを所有するエンティティや集約の一部として、ネストしたオブジェクト構造で表現されます。
- 例:
Address
という値オブジェクト(street
,city
,zipCode
などの属性を持つ)は、Customer
リソースやOrder
リソースのJSON表現内で以下のようにネストして表現されます。
json { "customerId": "...", "name": "...", "shippingAddress": { "street": "123 Main St", "city": "Anytown", "zipCode": "12345" }, ... }
値オブジェクトは不変であるというDDDの性質をAPIにも反映させる場合、値オブジェクト全体を一度に更新する(新しい値オブジェクトで置き換える)設計が自然です。部分的な属性更新(PATCH)は、値オブジェクトの属性ごとではなく、包含するエンティティや集約の一部として扱われます。 - 例:
-
エンティティのAPI表現: 集約ルート以外のエンティティ(集約内部のエンティティ)は、単独で独立したリソースとするか、あるいは集約リソースのサブリソースとして表現するかを検討します。
-
独立したリソースとして公開しない場合: 集約リソースのJSON表現の一部としてネストして表現されます。集約の整合性を保つ操作は、集約ルートを通じてのみ行われます。 例: 注文集約内の
OrderItem
は、orders/{orderId}
リソースのGETレスポンス内で、items
というリストとして表現される。json { "orderId": "...", "orderDate": "...", "items": [ { "itemId": "...", // OrderItemのIDだが、単独リソースではない "productCode": "...", "quantity": 2, "price": 1000 }, ... ], ... }
-
サブリソースとして公開する場合: 集約内の特定のエンティティに対して直接アクセスしたり、操作を行いたい場合にサブリソースとして公開することがあります。これは、そのエンティティが集約の外部から比較的独立して扱われる必要がある場合や、そのエンティティのリストが非常に大きい場合に検討されます。しかし、集約の整合性ルールを破らないように注意が必要です。 例:
orders/{orderId}/items/{itemId}
のように、特定の注文明細に直接アクセスする。ただし、注文明細の削除や数量変更などの操作が、注文集約全体のビジネスルール(例: 合計金額の再計算、在庫引き当ての解除など)と連携して行われる必要があります。サブリソースとするか否かの判断は、そのエンティティがどれだけ独立したライフサイクルを持つか、そしてそのエンティティに対する操作が集約全体の整合性にどの程度影響するかによって変わります。一般的には、集約内のエンティティは集約ルートを通じて操作される方が、データの整合性を保ちやすいため推奨されます。
-
具体的な設計パターンと考慮事項
1. 集約単位でのリソース設計
最もDDDの考え方を反映したシンプルなパターンです。
- GET /customers/{customerId}: 顧客集約全体の情報を取得します。住所や連絡先などの値オブジェクトも含まれます。
- POST /customers: 新しい顧客集約を作成します。リクエストボディには顧客の初期情報(名前、住所など)を含めます。
- PUT /customers/{customerId}: 特定の顧客集約全体を更新します。リクエストボディには更新後の顧客集約全体の情報を指定します。住所のような値オブジェクトを変更する場合も、新しい住所オブジェクト全体を渡す設計がDDDの不変性とも整合します。
- DELETE /customers/{customerId}: 特定の顧客集約を削除します。
このパターンは、集約の境界がAPIの操作単位と一致するため、ドメインロジックとAPI実装の間のマッピングが明確になりやすいメリットがあります。
2. 集約内の部分的な変更(PATCH)
RESTful APIでは部分更新のためにPATCHメソッドがよく使われます。DDDの視点からPATCHを考える場合、以下の点に注意が必要です。
-
値オブジェクトの更新: DDDでは値オブジェクトは不変です。したがって、例えば顧客の住所を変更する場合、PATCHリクエストで住所オブジェクトの一部属性だけを変更しようとするのではなく、新しい住所オブジェクト全体を渡して古いものと置き換える形式が望ましいです。 例: ```json PATCH /customers/{customerId} Content-Type: application/merge-patch+json
{ "shippingAddress": { "street": "456 Oak Ave", "city": "Newtown" } } ``` このようなPATCHリクエストは、API実装側で適切に解釈し、新しい住所オブジェクトを構築して古い住所と置き換える処理を行います。
-
集約内部のエンティティの更新: 集約内のエンティティ(例:
OrderItem
)をPATCHで更新する場合、その操作が集約全体の一貫性ルールに違反しないことを保証する必要があります。例えば、注文明細の数量を変更した場合、集約ルートである注文オブジェクトの合計金額を再計算する必要があるかもしれません。API実装は、このような集約内の不変条件やビジネスルールを適用する必要があります。
3. コマンドとしての操作
RESTful APIはリソース指向ですが、ビジネスロジックによっては、特定のリソースに対する「操作」や「アクション」を表現したい場合があります(例: 注文を確定する、商品をカートに追加する)。DDDではこれを「ドメインサービス」や「アプリケーションサービス」として設計することがあります。
このような操作をAPIで公開する際は、カスタムメソッド(例: POST /orders/{orderId}/confirm
)や、操作の実行結果として生成されるリソース(例: カートへの商品追加リクエスト POST /cart-items
→ カート明細リソースの生成と応答)として表現することが考えられます。DDDで定義されたドメインサービスやアプリケーションサービスのメソッドを、APIエンドポイントの処理としてマッピングします。
4. 境界づけられたコンテキストとAPI
大規模なシステムでは、複数の境界づけられたコンテキストが存在し、それぞれが異なるドメインモデルを持つことがあります。このような場合、コンテキストごとに独立したAPI群を設計するか、あるいはAPIゲートウェイ等を用いて複数のコンテキストにまたがるAPIを構成することが考えられます。重要なのは、それぞれのAPIがどの境界づけられたコンテキストのモデルに基づいているかを明確にすることです。コンテキスト間の連携は、APIコールやイベント通知など、明示的な手段を通じて行われます。
DDDの概念を考慮しない場合のアンチパターン
DDDの概念、特に集約の境界を無視してAPIを設計すると、以下のような問題が発生しやすくなります。
- 集約内部のエンティティを独立したトップレベルリソースとして公開する: 例えば、
GET /order-items/{orderItemId}
のようなリソースを設計し、これを使って注文明細を単独で操作できるようにしてしまうと、注文全体の整合性(例: 注文合計金額との整合性)を保つのが難しくなります。クライアントが集約の境界を意識せずに内部データをバラバラに操作できる設計は、データの不整合を招く可能性が高いです。 - 値オブジェクトの属性をトップレベルにフラットに配置する: 例えば、顧客リソースに
shippingStreet
,shippingCity
,shippingZipCode
のように住所の属性をフラットに持つ設計です。これは見た目はシンプルかもしれませんが、住所というまとまり(値オブジェクト)が持つ意味や不変性といったドメインの知識が失われます。住所のフォーマット変更などがあった際の影響範囲が広がり、保守性が低下する可能性があります。 - 複数の集約にまたがる操作を単一のAPIで提供する: 複数の集約に関わる操作を行う場合、それぞれの集約の一貫性ルールを守る必要があります。単一のAPIで安易に複数の集約を変更する設計は、分散トランザクションのような難しさや、部分的な失敗によるデータ不整合のリスクを高めます。このような場合は、オーケストレーションやSAGAパターンなど、より複雑な整合性管理の仕組みが必要になります。
まとめ
RESTful APIのデータモデリングにドメイン駆動設計(DDD)の考え方を取り入れることは、ビジネスロジックとの整合性が高く、保守性の高いAPI設計につながります。
- 集約はAPIリソースの適切な粒度を考える上で強力なヒントになります。集約ルートをリソースの根幹とすることで、データの整合性を保ちやすくなります。
- 値オブジェクトは、APIリソース内でネストした構造として表現することが、ドメインの意味を反映し、不変性を保つ上で有効です。
- 集約内のエンティティを単独のリソースとして公開するか、集約リソースの一部として含めるかは、そのエンティティの独立性や整合性ルールへの影響を考慮して判断します。一般的には集約リソースの一部として表現する方がシンプルで整合性を保ちやすい傾向があります。
DDDは決して銀の弾丸ではありませんが、複雑なドメインを持つシステムにおいて、データモデリングの指針を与えてくれる有効なアプローチです。ドメインエキスパートとの対話を通じてドメインモデルを深く理解し、それをAPIリソース設計に反映させることで、真にビジネスに貢献するAPIを構築することができるでしょう。