前回は、C4モデルを使ってアプリケーション全体の構成を整理しました。
そこまで進めてみると、フロントエンドとバックエンド、外部システムの関係はかなり見えやすくなってきました。
ただその一方で、バックエンドの中をどう分けて、どこに何を置くのか はまだ曖昧なままでした。
このまま実装に入ることもできなくはなかったのですが、あとから責務がぐちゃっとしそうな気がしたので、
今回は一度立ち止まって、バックエンドのアーキテクチャを整理することにしました。
🧭 今回やったこと
今回やったのは、バックエンドの構造を言語化することです。
具体的には、
- レイヤードアーキテクチャ
- DDD(ドメイン駆動設計)
- モジュラーモノリス
- Package by Feature
あたりを意識しながら、
このSaaSのバックエンドをどう組むかを整理しました。
最初からここまで考えなくても、動くものを作ること自体はできると思います。
でも今回は、今後の拡張や変更にある程度耐えられる形にしたかったのと、
自分自身の設計の練習としても、少しちゃんと向き合ってみたかったです。
🏗️ ベースはレイヤードアーキテクチャにした
今回のバックエンド設計では、まずベースとして レイヤードアーキテクチャ を採用しました。
理由はシンプルで、責務を分けやすくて、保守もしやすそうだったからです。
FastAPI だと、そのままルーターに処理を書きながら進めることもできますが、
今回はなるべくロジックを整理した状態で持ちたかったので、最初に構造を意識することにしました。
あわせて、ただレイヤーを分けるだけではなく、
どの業務ロジックがこのプロダクトの中心なのか を考えるために、DDD の考え方も取り入れています。
また、構成としてはマイクロサービスではなく モジュラーモノリス を選びました。
今の段階でいきなりシステムを細かく分割するよりも、1つのアプリケーションの中で境界をきちんと引くほうが、自分には合っていると感じたからです。
🧠 まずは、このSaaSのコアをどこに置くか考えた
今回の設計で一番大事だったのは、
このSaaSにとって何がコアドメインなのかを決めること でした。
整理してみた結果、ドメインは次のように分けることにしました。
-
コアドメイン
外部サービスごとの料金計算 -
支援サブドメイン
外部サービスの料金情報の参照
アプリごとの外部サービスコスト管理(集計) -
汎用サブドメイン
ユーザーの識別
この中で、やっぱり一番重要なのは 外部サービスごとの料金計算 だと思っています。
このプロダクトの価値って、単にサービス情報を登録できることではなくて、
利用量や課金体系に応じてコストをどう扱うか、どう計算するかにあるはずです。
なので、そこをコアドメインとして扱う方針にしました。
📦 コンテキストは2つに分けた
今回は大きく2つのコンテキストを用意しています。
calcServiceCostuserAuthentication
calcServiceCost は、アプリケーションとサービスの関係、料金計算、利用量の管理などを担う中心的なコンテキストです。
一方の userAuthentication は、認証・認可を扱うためのコンテキストです。
こちらは差別化ポイントというより、システムとして必要な共通機能に近いので、中心とは分けて考えることにしました。
この分け方にしたことで、
どこがプロダクトの価値に直結する部分で、どこが共通基盤なのか がだいぶ見えやすくなった気がします。
🪜 レイヤーの役割も整理した
calcServiceCost の中では、以下のようなレイヤー構成を考えています。
- domain
- application
- infrastructure
- presentation
ざっくりいうと、
- domain は業務ルールを持つ場所
- application はユースケースを組み立てる場所
- infrastructure はDBや外部サービスとの接続を担う場所
- presentation は FastAPI のルーターを置く場所
というイメージです。
設計しながらあらためて感じたのは、
この「それぞれのレイヤーが何をする場所なのか」を理解するのが意外と難しい、ということでした。
💸 課金体系はドメインとして表現したいと思った
今回の設計で個人的に面白かったのは、
課金体系の違いをちゃんとドメインとして表現したいと思えたことです。
たとえば現時点では、
- TokenBasedUsage
- RequestBasedUsage
- MonthlyBasedUsage
のように、課金タイプごとの値オブジェクトを用意する方針にしています。
こうしておくと、
- トークン課金
- リクエスト課金
- 月額課金
の違いを、単なる if 文の分岐として処理するのではなく、
意味のあるモデルとして扱えるようになります。
将来的に料金体系が増えたとしても、この考え方の延長で整理しやすくなるはずです。
🤔 設計してみて、一番難しかったこと
今回やってみて、一番難しかったのは 要件定義書からドメインを抽出すること でした。
最初は、
- 何をエンティティにするのか
- 何を値オブジェクトにするのか
- どこまでを1つのコンテキストとして扱うのか
あたりがかなり曖昧でした。
加えて、
- エンティティと値オブジェクトの違い
- 各レイヤーの責務
- repository と infrastructure の違い
みたいなところも、言葉では分かっているつもりでも、実際に自分のプロダクトに当てはめるとかなり悩みました。
💡 でも、ユースケースを「オーケストレーション」と捉えてからかなり整理しやすくなった
途中でかなり大きかったのが、
ユースケースはオーケストレーションを担うものなんだ と理解できたことです。
つまり、
- ユースケースが処理の流れを組み立てる
- リポジトリを通して必要なデータを取得する
- ドメインのメソッドを使って状態やルールを扱う
- 最後に保存する
という流れで考えられるようになってから、設計がかなり進めやすくなりました。
それまでは「この処理は application に置くのか、domain に置くのか」みたいな迷いが多かったのですが、
ユースケースを中心に全体の流れを見られるようになってからは、各レイヤーの役割もだいぶ整理されました。
🔌 DIを考える中で、repository と infrastructure の違いも見えてきた
もう1つ、今回かなり腹落ちしたのが、repository と infrastructure の違いです。
ここも最初は少し曖昧だったのですが、FastAPI の Depends() を使った DI を考える中で、かなり見えやすくなりました。
自分の中では、DI はざっくり2段階あると捉えています。
- インフラに対して、DBセッションのような具体的な情報を注入する
- ユースケースに対して、そのインフラを通して使えるリポジトリを注入する
この流れで整理してみると、
インフラはかなり実装寄りの存在で、
リポジトリはユースケースが振る舞いを表現するために使う、より抽象的な存在なんだなと理解しやすくなりました。
たとえば SqlAlchemyApplicationRepository のような具体実装はインフラ側にあって、
ユースケースから見るのは ApplicationRepository という抽象です。
この違いが見えてから、
「業務の流れ」と「実装の都合」を少し切り分けて考えられるようになったのは大きかったです。
📝 4日目のまとめ
今回は、バックエンド実装に入る前に、アーキテクチャの整理を進めました。
具体的には、
- レイヤードアーキテクチャ
- DDD
- モジュラーモノリス
- Package by Feature
を意識しながら、
このSaaSのバックエンドがどういう責務分割で成り立つかを考えました。
特に大きかったのは、料金計算をコアドメインとして置く と決められたことと、
ユースケースをオーケストレーションとして捉えられたことで、各レイヤーの役割がかなり整理しやすくなったことです。
まだ実装しながら調整が必要な部分はあると思いますが、
少なくとも「どこに何を置くか」で迷いにくくなる土台は作れた気がしています。
次は、この設計をもとに実際のバックエンド実装に入っていこうと思います。