BFF的な設計思想とドメイン的な設計思想の分離にモノリスなアーキテクチャで挑む
はじめに
プロダクト開発で奮闘しながら苦戦しているかわうそです。
前回書いたふりかえり記事の終わりにアーキテクチャに関する記事を書くと宣言したので、今回はアーキテクチャのことについて話そうと思います。
創業期から複数プロダクトを開発してきたDRESS CODEはアーキテクチャ的にも考えることも多く、「このままモノリスのシステムでいけるのか?」と思うことが多々あります。だからと言って、マイクロサービスなどの分散システムはオーバーエンジニアリングにもなるし、リソースも限られている状況です。
こんな中で課題となったのが、「モノリスなアーキテクチャでBFF的な設計思想とドメイン的な設計思想をなんとか分離できないか?」 という課題です。
今回はこの課題についてどのように立ち向かっているのかについてご紹介したいと思います。
なぜこの課題が発生したのか?
まずはなぜ上記で挙げた課題が発生したのか、よくあるシステム構成などを踏まえて説明していきます。
よくあるシステム構成
本当によくあるシステム構成です。フロントエンドはSPA、バックエンドは軽量のサーバーサイドフレームワーク(図ではNestJS)、DBはRDBみたいな構成です。
(DRESS CODEも基本はこの構成です)
この構成でよくあるAPIの設計パターンとして以下の2つがあると思います。
- フロントエンド(UI)駆動な設計
- フロントエンド(UI)に必要な画面単位で設計
- ドメインを意識するというよりは画面単位で必要なAPIを設計
- バックエンド(ドメイン)駆動な設計
- バックエンドが汎用的なAPIを設計
- フロントエンドにとっては使いづらいAPIになってしまいがち
フロントエンド(UI)駆動な設計
フロントエンド(UI)駆動な設計になると、APIは画面(UI)ベースな設計が基本になると思います。
例えば、以下のようなAPIの設計です。
GET /api/v1/dashboard: ユーザーのプロフィール、最近の注文数、通知の未読数をまとめて返す。
{
"user": { "id": "123", "name": "太郎", "email": "taro@example.com" },
"recentOrders": 5,
"unreadNotifications": 3
}
GET /api/v1/product/{id}: 商品情報、在庫状況、関連レビューを一度に返す。
{
"product": { "id": "456", "name": "Tシャツ", "price": 2000 },
"stock": 10,
"reviews": [{ "user": "花子", "rating": 4, "comment": "良い" }]
}
これらのAPIをそのまま実装してしまうと、ドメインとしての表現やルールを意識せず実装されてしまうことが多く、ApplicationレイヤーはDBへのCRUDを行うだけのレイヤーとなってしまいやすいです。
いわゆるドメインを表現するレイヤーやビジネスロジックを表現するレイヤーを見失いがちというイメージです。
(MVCフレームワークとかもこれに近い状態に陥りやすいイメージです)
この構成だとビジネスロジックが分散しやすく、変更容易性が低下しやすい印象があります。
(DBの設計までも画面に引っ張られて、画面とデータが1対1の構造にもなることも)
そのため、このアーキテクチャのままシステムを育てていってしまうと、改善するためにモジュラーモノリスやマイクロサービスみたいなアーキテクチャを採用して、少しづつ領域ごとにモジュールやシステムを分割して、再設計していくみたいな流れが多くなる(はず)です。
バックエンド(ドメイン)駆動な設計
今後は逆にバックエンド(ドメイン)駆動な設計の場合ですが、この場合はAPIは汎用的なAPIもしくはドメインベースなAPIを設計しがちです。ドメインをベースに、バックエンドがエンティティをリソースとして表現するパターンです。
例えば、フロントエンド(UI)を考慮せず、以下のようなAPIが設計されるようなイメージです。
GET /users: ユーザー一覧を取得
GET /users/{id}: 特定のユーザーを取得
POST /users: 新しいユーザーを作成
PUT /users/{id}: ユーザー情報を更新
DELETE /users/{id}: ユーザーを削除
「バックエンド(ドメイン)駆動な設計」のAPIは、ドメインの整合性や再利用性を重視する一方で、フロントエンドにとっては「欲しいデータが一度に揃わない」「呼び出し回数が増える」といった使いづらさが出がちです。これを解消するために、以下のような仕組みがよく取り入れられます。
- GraphQLの導入
- フロントエンドが必要なデータ構造を柔軟に指定
- BFF(Backend for Frontend)
- フロントエンド専用のAPI層を用意し、バックエンドを仲介
例えば、BFFを設けると以下のような構成になります。
確かに、BFFを設けることでフロントエンドの負荷を減らしつつ、バックエンドもドメインをベースとした設計をやりやすくなるかもしれないですが、BFFという1つのシステムコンポーネントが増えることで、運用の負荷などは増加し、BFFにビジネスロジックが紛れ込む可能性もあります。
BFFも増やしたくないけども、フロントエンド(UI)駆動な設計とバックエンド(ドメイン)駆動な設計をどちらも実現したい。
バックエンドでBFF的な設計とドメイン的な設計を組み合わせたアーキテクチャを実現できないだろうか? ということを考えてみました。
図で表すと以下のようなアーキテクチャです。
これを上手いこと表現できないかということで考えたのが次のアーキテクチャです。
BFF的な設計思想とドメイン的な設計思想を分離させたモノリスなアーキテクチャ
このアーキテクチャの意図を簡単にまとめると以下になります。
- BFF的な設計思想をPresentationレイヤーに閉じる
- APIは画面(UI)に合わせて設計
- 画面(UI)に必要なデータはそのモジュールで好きなように組み立ててよしとする
- 副作用(書き込み等)を発生するものはドメインを参照する
- ドメイン的な設計思想はDomainレイヤーを中心に閉じる
- ここは戦術的DDDやクリーンアーキテクチャのドメインと同じイメージ
- ドメインのルールやビジネスロジックはユースケースではなくドメインで表現する
BFF的な設計思想をPresentationに閉じ込める
今回、解決したかった課題の1つに「フロントエンド(UI)駆動な設計」も表現したいというものがあります。
そこで考えたのが、PresentationレイヤーにBFF的な設計思想を詰め込めないかです。このレイヤーが一番フロントエンドに近く、設計思想もフロントエンドに合わせやすいレイヤーです。
でもその概念をバックエンドとしては外に出したくないという事実もあります。
また、画面に必要なデータのためにたくさんのSQLを発行したりするのもコストになるので、自由にSQLを組み立てられるようにしたいというのもあります。
ただし、フロントエンドの設計思想の中で副作用(書き込み等)を発生するようなものは表現したくないので、このレイヤーであくまで表現して良いのみは取得のみとしています。
これらを踏まえて、上記の図のようなアーキテクチャを採用しました。
実際にDRESS CODEでも以下のようなディレクトリ構成になっています。
src/
├── api/ # Presentationレイヤー(API)
│ ├── user-dashboard/ # 画面をベースにモジュールを分割
│ │ ├── controller/ # エンドポイント
│ │ ├── infrastructure/ # インフラストラクチャ(取得のみ)
│ │ │ └── query-service/ # 画面で必要な取得(SQL)を好きに設計
│ │ └── usecase # ユースケース
│ ├── user-settings/
│ │ ├── controller/
│ │ ├── infrastructure/
│ │ │ └── query-service/
│ │ └── usecase
│ └── ...
ドメイン的な設計思想はDomainレイヤーを中心に閉じる
ここの部分は戦術的DDDやクリーンアーキテクチャなどの一般的なアーキテクチャと表現したいことはほとんど同じものです。
このレイヤーにおいては 画面(UI)を意識することなく、ドメイン的な設計に集中させる ことを実現しようとしています。
画面からどう使われるなどは意識しないということです。どんなルールが必要か、登録時に満たすべきValidationは何が必要か、登録後に行わないといけないことは何か、などドメインにおける関心ごとに意識を向けます。
ここでもう1つのポイントがドメイン側に取得に特化したモジュール(Query Service)を設けている点です。
これはBFF設計思想側の各モジュールごとに共通的な取得処理を定義するために設けています。マスタ的なデータの取得であったり、人の情報を一発で取得するようなクエリが定義されていたりします。
(完全に、ReadとWriteを分断するならドメイン側に定義しなくても良いかなと思いましたが、置き場所としてあまり"Shared"のような概念を定義したくないということもあり、ドメイン側に配置しました)
悩み
一見良さそうにも見えるアーキテクチャですが、以下のような技術的な課題も抱えています。
- トランザクションを発行するのはApplicationレイヤー(ユースケース)になっている
- 本来、気にするべきところは集約(強整合性)であり、その単位でトランザクションを発行したいが、Applicationレイヤーから知る術がない
- 現状はドメイン側にある処理がResult型(neverthrow)を返却して、Applicationレイヤー(ユースケース)でトランザクションを管理
- BFF設計思想側とドメイン設計思想側が参照しているDBが同じ
- そのため、ドメイン側がDB定義を変更する際、BFF側も参照している可能性はあるので、変更時にBFF側も意識する必要がある
- フロントエンド側に返却するAPIにドメイン側のEntityなどを返却してしまうことがある
- これは悪いわけではないと思いつつ、ドメイン側以外の領域で、新たにドメイン的な概念を生み出してしまう可能性がある
- 変換のコストはかかるが、必ず用意した型(DTO)をベースに返却するようにしたい
(アーキテクチャの移行段階のため全てが上記の構成になっているわけではないので、それもそもそもの課題ではありますが、段階的に移行できれば良いかなって考えています)
CQRSではないのか?
ぱっと見、CQRS(コマンド・クエリ責務分離)と似てそうで似てないようなアーキテクチャにも思えます。
簡単に似ていると思える要素を整理すると以下になります。
- 読み取り(Query)と書き込み(Command)の分離
- BFF設計思想側では読み取り(Query)に特化させて、ドメイン設計思想側で書き込み(Command)に特化させている
- これはCQRSの「Queryは読み取り専用、Commandは状態変更」という点で似ている
- 画面ごとの最適化
- BFF設計思想側でUIのニーズに合わせたデータ取得を設計している
- CQRSではQuery側をUIやクライアントの要求に応じて柔軟に設計(Read Model)
- Domainレイヤーの書き込み特化
- ドメイン設計思想側ではドメインのルールやビジネスロジックをUIを意識せずに管理
- CQRSのCommand(Write Model)では、ドメインイベントや集約を通じて整合性を保つ
厳密にCQRS(コマンド・クエリ責務分離)とは違うところを整理すると以下になります。
- 完全な分離ではない
- データストアの分離までは行っていない
- ドメイン設計思想側でもQuery Serviceのような概念が存在している
- 分離の主目的が違う
- CQRSではコマンドとクエリの分離が主目的
- 今回はBFF的な設計思想とドメイン的な設計思想の分離が主目的
- (ん?なんか似てそうで似てるのか笑)
おわりに
今回はBFF的な設計思想とドメイン的な設計思想の分離をモノリスなアーキテクチャで挑戦してみました。
PresenationレイヤーにBFF的な設計思想を閉じることで、ドメインレイヤーが画面(UI)を意識することなくドメインに集中できるようなアーキテクチャになりました。結果、CQRSで似てそうで似てなさそうなアーキテクチャに近づきました。
現在、開発しているDRESS CODEはイベントソーシングとも相性が良い領域とも考えており、ここからイベントソーシングを導入して正式なCQRSを目指していこうと思っています。
(すでに、イベントソーシングの設計思想を取り入れて開発を行っているので、真のCQRSになる日は近いかもと思いつつ、絶賛修行中です笑)
Discussion