共通基盤は「構造を入れる」ことではない — マルチプロダクトでクリーンアーキテクチャを拡張する
Published on 2026年7月4日
設計はじめに
この記事は、AIとの壁打ちで思考を整理したアウトプットだ。正解を提示するものではなく、自分の考えを言語化するために書いている。
前提を先に置く。ここで考えたいのは、単一のプロダクトの中身ではなく、1つのサービスがエコシステムへ育っていくときに、複数プロダクトを横断する共通基盤をどう整備するかだ。モノレポで複数プロダクトを運用する構成を進めていくと、遅かれ早かれ「どこまでを共通に寄せ、どこからをプロダクト固有に残すか」という問いに突き当たる。この記事はそこを整理する。
そのうえで、2つの前提を明示しておきたい。
- クリーンアーキテクチャは、そのまま導入するものではなく、拡張して考えるものである。 同心円の図は単一プロダクトを想定して描かれている。マルチプロダクトの基盤では、その内側と外側の両方に「文脈(プロダクト)」という軸を足して考える必要がある。
- 構造を入れることは、設計ではない。 ディレクトリを domain / application / infrastructure に切っただけでは、それは器を用意しただけだ。設計とは「何をどこに置くか」「なぜそこに置くか」を、変更理由に基づいて決め続ける営みであって、パターンの導入はそのスタート地点ですらない。
なお、この記事が扱うのは「境界をどう割るべきか」という判断の座標系までだ。その判断を間違えたときにどう戻すか——運用と組織の話——は射程の外に置くが、外に置いたことの意味は記事の終盤で明示する。
そして自分の主軸はフロントエンドなので、これらのバックエンド由来の概念を抽象度を上げてフロントに適用する視点でも見ていく。
前提1: クリーンアーキテクチャを「拡張して」考える
DDD もクリーンアーキテクチャも、出発点は「業務ルールを中心に守る」だった。これは今も正しい。ただ同心円の図は、暗黙に1つのプロダクトを想定している。
- 一番内側: Entities(ドメインルール)
- その外側: Use Cases(ユースケースごとのアプリケーションルール)
現場で機能追加が来る単位は、たいていドメインではなくユースケースだ。「◯◯という操作を足したい」「このフローをこう変えたい」——見積もりもテストも仕様の議論も、ユースケース単位で語られる。一方で純粋なドメインルール(不変条件・計算・状態遷移)は変わりにくい。
変更頻度 置き場所
────────────────────────────────────────
ドメインルール 低い Domain(変わりにくい核)
操作のパターン 高い UseCase(変わりやすい外側)
マルチプロダクトの基盤では、この2層に**「文脈(プロダクト)」の軸**を重ねる。つまり Domain も UseCase も、それぞれ「全プロダクト共通」と「プロダクト固有」に割れる。クリーンアーキテクチャの円を、プロダクトという別の次元へ引き伸ばす——これが「拡張して考える」の中身だ。
Domain と UseCase の責務
Domain — 文脈を知らない世界のルール
- Entity(ID を持ち、ライフサイクルを持つ)
- Value Object(値としての同一性を持つ)
- Domain Service(単一の Entity に属しきれないロジック)
金額計算、状態遷移の判定、不変条件のチェック。ポイントは、「どの画面から呼ばれるか」「どのプロダクトで使われるか」を一切知らないこと。知った瞬間、それはもうドメインではない。
UseCase — 文脈付きの手順
- どの Repository からどの Entity を取るか
- どのドメインロジックを、どの順番で使うか
- トランザクションの境界
- 結果をどうまとめて返すか
ルールそのもの(計算・不変条件)は Domain に任せ、UseCase は段取りだけを持つ。
Atom ↔ Entity という見立て(フロントへの橋)
自分はフロント主軸なので、この責務分割を アトミックデザインの感覚に翻訳して掴んでいる。
Atomic Design Domain / UseCase
────────────────────── ────────────────────────────
Atom ≒ Entity / Value Object
Molecule / Organism ≒ UseCase が組み立てた振る舞い
厳密な 1:1 ではない。ただ「意味のある最小ピース」と「それを場面ごとにどう組むか」という二段構えはどの世界でも共通だ。Atom にビジネスロジックを詰めないのと同じで、Entity に文脈を詰めない。そう考えると、フロントの感覚のまま扱える。
抽象度を上げるとこうなる。
「変わりにくい意味の核」 → Entity / Atom
「文脈付きの、核の使い方」 → UseCase / Organism
どの層の言葉で話していても、問いは同じだ。「これは核か、核の使い方か」。
UseCase 層を「汎用」と「プロダクト固有」に割る
Domain → UseCase の二層だけでは、マルチプロダクトでは足りない。UseCase 層の中をさらに割る。
application/
shared/ ← プロダクト横断(ログイン、ユーザー登録、共通検索)
productA/ ← A 固有(プラン加入、キャンペーン適用)
productB/ ← B 固有(レポートのエクスポート、レガシー取り込み)
崩してはいけない線引きはひとつ。
- ルールそのものは Domain に置く
- ルールをどう繋げて使うかのシナリオを UseCase(汎用 or 固有)に置く
「よく使うから」という理由でドメインの不変条件や計算を汎用 UseCase に詰めると、UseCase 層が"もう一つのドメイン層"になる。これは崩壊の入口だ。汎用に置いていいのはシナリオの共通化であって、ルールの共通化ではない。
付け加えると、この shared/ は基盤の中で最も腐りやすい場所でもある。ログインや検索のような「共通に見える」シナリオほど、プロダクトの性質(B2B/B2C、認証要件など)で後から割れる。shared に置く判断は「今共通だから」ではなく、後述する「変更理由が同じか」で下す必要がある。
フロントでも同じ構図になる。packages/shared に置いていいのは全プロダクトで意味の変わらないもの(デザイントークン、認証フックなど)で、プロダクト固有の画面フローを「共通っぽいから」と shared に押し込むと、共通パッケージが片方の都合で壊れ始める。
共通 Entity とプロダクト固有の文脈
プロダクトごとに、共通 Entity へよりプロダクトに限定した文脈を設定できるか?
できるし、よく行われる。ここで効くのが DDD の バウンディッドコンテキストだ。
パターン 1: コンテキストごとに別の Entity
billing-context の User ← 請求・契約に必要な属性とルール
admin-context の User ← 権限・監査に必要な属性とルール
名前は同じ User でも、コンテキストが違えば別クラスにする。共通なのは ID など本当に共有したい情報だけで、コンテキスト間はマッピングで連携する。
パターン 2: 共通 Entity + コンテキスト固有ラッパー
CommonUser ← 全プロダクト共有の最小属性・ルール
├ ProductAUser / ...Service ← A の世界での扱い
└ ProductBUser / ...Service ← B の世界での扱い
共通 Entity は構造の最小公倍数、プロダクト固有の意味づけはコンテキスト側に寄せる。
ただし、この2つは対等な選択肢ではない。基本形はパターン1だ。パターン2の CommonUser は DDD で言う Shared Kernel にあたり、全コンテキストの変更を連動させる強い結合を生む。最小公倍数モデルは「全員のもの」であるがゆえに「誰のものでもない」貧血モデルに陥りやすい。パターン2を選ぶのは、共有する属性とルールが本当に安定していて、変更の調整コストを払う覚悟があるときに限る。
何を共通に寄せるかの判断軸
- その属性・ルールは本当に 2 つ以上のプロダクトで共有されるか
- 変更理由が同じか — 「A の都合で変えたいが B では変えたくない」ものは共通に入れない
2つ目が本質だ。今たまたま形が同じだけのものを共通化すると、片方の都合で両方が壊れる。共通基盤の失敗は、だいたいここで起きる。 形の一致を、意味の一致と取り違える。
前提2: 構造を入れることは、設計ではない
ここまでのレイヤー構成を、クリーンアーキテクチャ/オニオンに近い形にするとこうなる。
app/
presentation/ ← Controller, Presenter, DTO
application/ ← UseCase
shared/ productA/ productB/
domain/
common/user/ ← UserId / UserName / User
productA/subscription/ ← Plan / Subscription
productB/audit/ ← AuditLog
infrastructure/ ← DB・外部API・メッセージング
依存方向は一方向だ。
infrastructure → application → domain
内側(domain / application)は外側の具象を知らず、抽象(インターフェース)だけを知る。この向きが逆流した瞬間に、ドメインが DB やフレームワークに汚染される。
だが、このディレクトリを切ることは設計ではない。 これは器を並べただけだ。設計はこの器に対して、次の問いを変更のたびに問い続けることで初めて成立する。
- このロジックは"ルールそのもの"か、"ルールの使い方"か → domain か application か
- そのシナリオは全プロダクト共通か、特定プロダクトだけか → shared か productX か
- この属性を共通に寄せて、A と B は本当に同じ理由で変わるか → common か context か
構造は、この問いの答えを置く場所を用意するだけで、答えそのものは出さない。パターンを導入した時点で満足すると、器だけ立派で中身が混沌とした「クリーンアーキテクチャ風の何か」ができあがる。モデリングが崩れれば構造ごと崩れるのと同じで、器を入れても問いを立てなければ設計は始まらない。
フロント主軸でこれをどう使うか
自分の主軸はフロントなので、これらを抽象度を上げてフロント基盤に適用する。バックエンドの domain / application / infrastructure をそのまま移植するのではなく、対応する「軸」だけを借りる。
バックエンドの軸 フロント基盤での置き場所(例)
───────────────────────── ──────────────────────────────────
変わりにくい意味の核 packages/model, design-system/tokens
文脈付きの使い方(汎用) packages/shared の hooks / ロジック
文脈付きの使い方(固有) apps/productX の画面ロジック
技術詳細 api クライアント, インフラ層
以前フロント設計を整理したとき、src/domains に画面(pages)を持たせようとして「UI に domain 情報が入ると責務が単一でなくなる」と気づいて消した。あれと同じことで、フロントでも**「意味の核」と「その使い方」を混ぜない**という一点は変わらない。軸は共通で、置き場所の名前だけがフレームワークごとに変わる。
だから共通基盤を作るときに問うべきは「どのパターンを入れるか」ではない。**「このプロダクト群で、何が本当に共通の意味を持ち、何がプロダクトごとに変わるのか」**だ。それが決まって初めて、クリーンアーキテクチャが器として機能する。
この枠組みの射程 — 「正しい地図」と「悪路を走る車」
ここまでの整理には、あえて含めていないものがある。先に明示しておく。
この記事が扱っているのは、境界を「どう割るべきか」という判断の座標系だ。核か、核の使い方か。形の一致か、変更理由の一致か。この問いは出発点として正しいと考えているし、これ以外の出発点はたぶんない。
ただし、座標系が正しいことと、その上に建てた基盤がスケールすることは別の命題だ。実際のマルチプロダクト基盤の寿命を決めるのは、判断の正しさよりも、判断を間違えたときにいくらで戻せるかであることが多い。プロダクトが2つのうちは、共通化の判断ミスは切り戻せる。5つになったとき、shared に入れた一つの誤りは全プロダクトの変更を人質に取る。
だからこの枠組みを実際に運用するときは、3つの問いにもう1つを対にして持つ必要がある。
この共通化は、間違いだと判明したとき、どれだけのコストで戻せるか?
バージョニング、段階的な deprecation、コンテキスト間の防腐層、そして共通パッケージのオーナーシップ——つまり「誰がこの境界の番人か」という組織の設計。これらは本記事の射程の外だが、枠組みの外にあるのではなく、枠組みを現実で回すための前提だ。境界は正しく引くものであると同時に、間違えて引き直すものでもある。
言い換えると、この記事は地図を描いている。悪路でも走れる車——誤りを前提にした運用と組織——は、地図とは別に組む必要がある。地図なしに車だけあっても迷うが、車なしの地図は壁に貼られて終わる。両方を持って初めて、「問い続ける営み」は続けられる。
まとめ
一文にすると、
マルチプロダクトの共通基盤とは、パターンを導入することではなく、「何が共通の意味を持ち、何が文脈で変わるか」を変更理由に基づいて割り続けること——そして、割り間違いを戻せるように割ることだ。
前提1 クリーンアーキテクチャは拡張して考える
Domain / UseCase に「プロダクト」という軸を重ねる
前提2 構造を入れることは設計ではない
器は問いの答えを置く場所であって、答えではない
射程 この記事は判断の座標系(地図)まで
誤りを戻す運用と組織(車)は、地図と対で別途組む
Domain 文脈に依らない意味の核(共通 / プロダクト固有)
UseCase 文脈付きのシナリオ(shared / productX)
共通Entity / 文脈 Bounded Context を意識し「変更理由が同じか」で割る
可逆性 間違えたとき戻せるかを、共通化の判断に織り込む
フロント適用 軸だけ借りて、置き場所の名前をフレームワークに合わせる
新しいクラスやコンポーネントを書くたびに、こう問える。
- これは"意味の核"か、"核の使い方"か?
- そのシナリオは全プロダクト共通か、特定プロダクトだけか?
- この共通化は、形の一致か、それとも変更理由の一致か?
- この共通化は、間違いだったとき、どれだけのコストで戻せるか?
構造を入れた瞬間に設計が終わるのではない。構造は、この問いを立て続けるための足場にすぎない。 足場を組んだだけで満足しないこと、そして足場は組み直せるように組むこと——それが、スケールしたときに崩れにくく、崩れても立て直せる共通基盤への最初の一歩になる。