ドメイン層をfeatures/user/domain/に置いているプロジェクト、結構見かけます。自分も以前やってました。ページ単位のpages/settings/domain/に入れているケースも。これ、厳密には設計ミスです。
ドメインは何にも依存しない。ページにも、endpointにも、よくわからない「機能単位」のディレクトリにも。だからどこかの配下に置いてはいけません。
独自の解釈でやっていると破綻するよね
海外では2025年後半からこの議論が再燃していて、Clean Architecture派・Vertical Slice派・DDD派がそれぞれの立場から意見を出しています。表面的には対立しているのに、結論はほぼ同じ場所に収束しています。
よく見るアンチパターン3つ
まず、実際によく見かける「ドメインの置き場所ミス」を整理します。
1. featureディレクトリ配下にドメインを置く
src/
features/
user/
domain/ # UserエンティティやValueObject
api/
components/
order/
domain/ # OrderエンティティやValueObject
api/
components/
Bulletproof Reactなど有名なテンプレートがこの構成を採用していて、広く模倣されています。一見きれいに見えるのですが、問題が2つあります。
1つ目は、featureの定義が曖昧なこと。「user」はfeatureなのか、ドメインの文脈なのか。開発者によって解釈がブレます。ある人は「ユーザー管理画面」をfeatureと呼び、別の人は「ユーザーというビジネス概念」をfeatureと呼ぶ。
2つ目は、クロスインポートが発生すること。OrderエンティティがUserを参照したいとき、features/user/domain/からインポートすることになる。featureは本来独立しているはずなのに、ドメインを通じて結合してしまいます。
2. ページ・ルート単位でドメインを置く
src/
pages/
settings/
domain/
components/
dashboard/
domain/
components/
Next.jsのApp Routerが普及してから増えた構成です。ページとドメインは1対1にならないのに、ページのディレクトリに閉じ込めてしまう。
Userエンティティは設定画面でもダッシュボードでもプロフィールページでも使われます。どのページに所属させるかで悩む時点で、設計が間違っている証拠です。
3. endpointごとにディレクトリを切る
src/
features/
create-user/
handler.ts
validator.ts
types.ts
get-user/
handler.ts
types.ts
update-user/
handler.ts
validator.ts
Vertical Slice Architectureの影響で増えた構成です。これの問題は技術的な関心(HTTPエンドポイント)がディレクトリ構造に漏れていること。
「create-user」「get-user」「update-user」は同じ「ユーザー」というドメイン概念に属します。CRUDの操作名でディレクトリを切ると、ビジネスの文脈ではなくHTTPメソッドの文脈でコードが整理されてしまいます。
Milan JovanovicがScreaming Architectureの記事で指摘しているように、ディレクトリ構造を見て「これはASP.NETアプリだ」ではなく「これは予約管理システムだ」と分かるべきです。endpoint単位の構成は「これはREST APIだ」と叫んでいるだけ。
API設計アンチパターン7選 - UI都合の設計が招く技術的負債開発なぜドメインは独立すべきか
ドメイン層の独立性は、Clean Architectureの「依存性ルール」として広く知られています。Uncle Bobの原則では、ソースコードの依存は常に内側(ドメイン)に向かう。ドメインが外側のレイヤーに依存してはいけない。
でも「ルールだから」で終わると説得力がない。実際に困るポイントは3つあります。
テストが書けなくなる
ドメインがfeatureやページのディレクトリ配下にあると、テスト時にそのfeatureの文脈を丸ごとセットアップしないとインポートすらできない場合があります。ドメインが独立していれば、import { User } from '@/domain/user'だけで完結します。
// ドメインが独立 → テストがシンプル
import { Order } from '@/domain/order/entity'
import { Money } from '@/domain/shared/value-objects'
test('注文金額が税込で計算される', () => {
const order = Order.create({
items: [{ price: Money.of(1000), quantity: 2 }],
taxRate: 0.1,
})
expect(order.totalWithTax.value).toBe(2200)
})
Reactのhooksもフレームワークのcontextも不要。ドメインロジックの純粋なテストができます。
複数の文脈から参照できない
Userエンティティをfeatures/auth/domain/に置いたとします。features/profile/やfeatures/settings/からもUserを使いたくなったとき、どうしますか?
結局features/auth/domain/user.tsをインポートすることになり、「authがuserを所有する」という意図しない依存関係が生まれます。authを変更するとprofileとsettingsが壊れるかもしれない。
ドメインの概念が技術的関心に汚染される
featureディレクトリは技術的な分類です。「認証機能」「設定画面」「ダッシュボード」。これらはUIやルーティングの関心であり、ビジネスの関心ではない。
ドメインの概念(ユーザー、注文、支払い)を技術的な箱に入れると、ビジネスロジックがUI構造に引きずられ始めます。これが地味にキツいです。Rico Fritzscheは「Layered architectures tend to dilute domain concepts across technical layers(レイヤードアーキテクチャはドメイン概念を技術レイヤーに希釈する傾向がある)」と指摘しています。
Bounded Contextで整理する
ではどうすべきか。ドメイン駆動設計のBounded Context(境界づけられたコンテキスト)でトップレベルを整理し、その内部でレイヤー分離する。
src/
modules/
user/
domain/ # エンティティ、ValueObject、ドメインサービス
application/ # ユースケース
infrastructure/ # API、リポジトリ実装
presentation/ # コンポーネント、hooks
order/
domain/
application/
infrastructure/
presentation/
payment/
domain/
application/
infrastructure/
presentation/
shared/
domain/ # 共有ValueObject(Money, EmailAddressなど)
infrastructure/ # ロガー、認証ミドルウェアなど
これはMartin Fowlerの見解とも一致します。
“Once any of these layers gets too big you should split your top level into domain oriented modules which are internally layered.” (レイヤーが大きくなりすぎたら、トップレベルをドメイン指向のモジュールに分割し、内部でレイヤー化すべき)
実際にDDD系の著名な実装サンプルを見てみると、ほぼ全てがこの構成を採用しています。
Vaughn VernonのIDDD Samplesはiddd_collaboration/やiddd_identityaccess/のように、Bounded Contextごとにモジュールを分離している。ddd-by-examples/libraryはcatalogue/とlending/の2つのBounded Contextで構成され、それぞれが内部にドメイン層を持っている。Khalil StemmlerのDDD ForumはTypeScriptでmodules/users/domain/、modules/forum/domain/という構成。
3つとも言語もフレームワークも違うのに、たどり着いた形は同じ。偶然ではなく、実践で検証された結果です。
言語やフレームワークに依存しない普遍的なパターンということです。
featureディレクトリとの違い
「これ、結局featureディレクトリと同じじゃないの?」と思うかもしれません。決定的な違いが1つあります。
// featureディレクトリ: 技術的・機能的な分類
features/
auth/ # 認証「機能」
settings/ # 設定「画面」
dashboard/ # ダッシュボード「画面」
// Bounded Context: ビジネスの文脈による分類
modules/
identity/ # 「ユーザーの認証・認可」というビジネス文脈
catalog/ # 「商品カタログ」というビジネス文脈
ordering/ # 「注文」というビジネス文脈
featureは「何をする画面か」で切り、Bounded Contextは「何のビジネス領域か」で切ります。画面は変わってもビジネスの文脈は変わりにくい。だから後者の方が安定した構造になります。
反対派の意見も聞いてみる
「ドメインなんて適当に配置したらいい」という意見もあります。
「Clean Architectureはフォルダ構造ではない」
Steve Bishopは「Clean Architecture is NOT a project structure(Clean Architectureはプロジェクト構造ではない)」と主張しています。重要なのは依存の方向であって、ファイルの物理的な配置ではない、と。
Vinod Jagwaniも同様に「Clean Architecture does not care where your files live. It only cares about who depends on whom.(Clean Architectureはファイルの場所を気にしない。誰が誰に依存するかだけを気にする)」と述べています。
この主張は技術的には正しい。ESLintのimport制約やTypeScriptのpathsで依存方向を強制できるなら、物理的な配置は自由です。
ただ、現実のチーム開発ではディレクトリ構造が開発者の認知を規定する。features/auth/domain/user.tsにあるUserエンティティを「これは独立したドメインモデルだ」と認識できる開発者は少ない。物理的な配置は「暗黙のドキュメント」として機能します。
「一緒に変わるものは一緒に置け」
James Michael Hickeyの主張です。1つの機能を実装するのに20フォルダを跨ぐのはコンテキストスイッチが多すぎる。
“Things that change together ought to live together.” (一緒に変わるものは一緒に置くべき)
これも一理あります。ただ、ドメインロジックはUIやAPIと同じ頻度では変わりません。注文の税計算ロジックは、管理画面のデザイン変更では変わらない。変更の頻度が違うものを同じ場所に置くと、不要な変更リスクが増えます。
「規模が小さいなら過剰設計」
Milan Jovanovicは「For startups or MVPs, feature-based packages are often the best choice(スタートアップやMVPにはfeatureベースが最適なことが多い)」と述べています。
これはその通りです。5ファイルのプロジェクトにBounded Contextは過剰です。プロジェクトの規模で判断するのが正解。
議論は収束している
調べてて気づいたのは、Clean Architecture派・Vertical Slice派・DDD派が実は同じ結論に向かっていることです。
| 陣営 | 主張 | 合意点 |
|---|---|---|
| Uncle Bob(Clean Architecture) | 依存は内側に向けろ | トップレベルはビジネス概念で切る |
| Jimmy Bogard(Vertical Slice) | 機能で縦に切れ | 共有ドメインモデルはスライスの外に |
| Eric Evans(DDD) | Bounded Contextで分けろ | モジュール内部でレイヤー化 |
| Martin Fowler | レイヤーが大きくなったらモジュール化 | ドメイン指向モジュール + 内部レイヤー |
表面的には「レイヤー vs スライス」の対立に見えますが、実践レベルでは全員がBounded Context単位のモジュール + 内部レイヤー分離に着地しています。
Milan Jovanovicも最近のLinkedIn投稿で「The Domain layer folder structure should express intent(ドメイン層のフォルダ構造は意図を表現すべき)」と述べ、技術的なグルーピング(Entities/ValueObjects/Services)ではなくAggregate単位での整理を推奨し始めています。
実装例:TypeScriptでの構成
フロントエンドでもバックエンドでも使える、TypeScriptでの具体的な構成例です。
src/
modules/
ordering/
domain/
order.ts # Orderエンティティ
order-item.ts # OrderItem ValueObject
order-status.ts # OrderStatus(Discriminated Union)
order-repository.ts # リポジトリのインターフェース
application/
create-order.ts # ユースケース
cancel-order.ts
infrastructure/
order-api-repository.ts # リポジトリの実装
presentation/
order-list.tsx
order-detail.tsx
catalog/
domain/
product.ts
category.ts
application/
search-products.ts
infrastructure/
product-api-repository.ts
presentation/
product-card.tsx
shared/
domain/
money.ts # 共有ValueObject
email-address.ts
infrastructure/
http-client.ts
ポイントはdomain/order-repository.tsがインターフェースだけを定義し、実装はinfrastructure/に置くこと。依存性逆転の原則により、ドメインがインフラに依存しない構造を維持できます。shared/domain/にあるMoneyやEmailAddressのようなValueObjectは、Zodスキーマでバリデーションと型定義を一元管理すると運用が楽になります。
// modules/ordering/domain/order-repository.ts
// インターフェースだけ。実装はinfrastructureに。
export interface OrderRepository {
findById(id: OrderId): Promise<Order | null>
save(order: Order): Promise<void>
}
// modules/ordering/infrastructure/order-api-repository.ts
import type { OrderRepository } from '../domain/order-repository'
export class OrderApiRepository implements OrderRepository {
constructor(private httpClient: HttpClient) {}
async findById(id: OrderId): Promise<Order | null> {
const dto = await this.httpClient.get(`/orders/${id}`)
return dto ? Order.fromDTO(dto) : null
}
async save(order: Order): Promise<void> {
await this.httpClient.post('/orders', order.toDTO())
}
}
フレームワークがディレクトリ = endpointを強制する場合
Next.jsのApp RouterやHonoのfile-based routingでは、ディレクトリ構造がそのままURLになります。app/api/orders/route.tsを作れば/api/ordersが生える。これは逆らえない。
この場合、route.tsを薄いラッパーにして、ロジックはmodulesに置きます。
// app/api/orders/route.ts
// ここにはロジックを書かない。moduleのユースケースを呼ぶだけ
import { createOrder } from '@/modules/ordering/application/create-order'
export async function POST(request: Request) {
const body = await request.json()
const result = await createOrder(body)
return Response.json(result)
}
構造としては2層になる。
app/ # フレームワークが強制する構造(ルーティング)
api/orders/route.ts # 薄いラッパー
src/
modules/ # 自分たちが設計する構造(ドメイン)
ordering/
domain/
application/ # ← route.tsから呼ばれる
フレームワークのルーティング構造と、ドメインの構造を分離する。app/api/orders/にビジネスロジックを直書きし始めると、URL設計を変えただけでドメインが壊れる、みたいなことが起きます。route.tsはHTTPリクエストの受け取りとレスポンスの返却だけに徹する。これが「endpointのディレクトリにドメインを置くな」の実践です。
まとめ
ドメイン層をfeatureディレクトリやページ単位で配置するのは、小規模なら問題ないけれど、成長すると破綻します。
endpointごとにディレクトリを切るのも、技術的な関心がディレクトリ構造に漏れるアンチパターン。「create-user」「get-user」で切るのではなく、「user」というビジネスの文脈で切るべきです。
海外の議論を追ってみたら、Clean Architecture派もVertical Slice派もDDD派も、最終的にはBounded Context単位のモジュール + 内部レイヤー分離に収束していました。対立しているように見えて、実践者は同じ場所にたどり着いている。
判断基準はシンプルです。ドメインエンティティが複数のfeatureから参照されるようになったら、Bounded Contextへの移行タイミング。
フロントエンドにクリーンアーキテクチャは必要か?判断基準とWeb/モバイルの違い開発