FSD(Feature-Sliced Design)のentitiesレイヤーを部分導入してドメイン知識を整理した話
はじめに
どうも!e-dashのプロダクト開発部所属の高尾です。
こちらはe-dash advent calendar 2025 10 日目の記事となります。
どんなプロジェクトでも成長するにつれてコードベースは複雑になっていくものです。
特に、複雑化に伴いドメイン知識(ビジネスロジックや型定義、定数など)が複数の場所に散らばってしまうと、
ビジネスの核となる概念がプロジェクト内で定まらなくなり、結果として開発効率や保守性に大きく影響を及ぼします。
弊社プロジェクトにおいても、フロントエンドのコードベースが成長するにつれて、ドメイン知識の管理に課題を感じるようになりました。
そこで、FSD(Feature-Sliced Design)のentitiesレイヤーの概念を部分導入し、ドメイン知識を体系的に整理することにしました。
本記事で扱う内容
- FSD(Feature-Sliced Design)とは何か、どういう効果があるか
- entitiesレイヤーを部分導入することでどのようなメリットがあったか
想定する読者
- フロントエンドアプリケーションにおけるドメイン知識の管理に課題を感じている方
- FSDに興味があるが、どこから始めれば良いか迷っている方
導入した背景
課題が発生したのは顧客管理システムのプロジェクトです。
このプロジェクトではお客様のCO2排出量算定の元となる請求情報、企業情報など、複数のビジネスドメインを扱っています。
この記事におけるこの後の説明では、仮定としてこのシステムが持つ
「お客様がアップロードされた請求書ファイルに対して、バックオフィスの要員がチェックフローを流せる機能」
を例として取り上げます。
請求書のチェックフローには、チェック待ち、ダブルチェック待ち、チェック完了というステータスがあり、流れていくとします。
プロジェクト構成
フロントエンドアプリケーションの構成は以下のようになっていました。(説明に必要な部分だけ抜粋)
frontend
├── components/ # Reactコンポーネント
| ├── layouts/ # ページ全体のレイアウトを定義するコンポーネント
| ├── pages/ # ページ単位のコンポーネント
| └── ui/ # ボタンや入力フィールドなどの基本的なUIコンポーネント
├── features/ # ドメインに依存した機能単位のコンポーネントや関連ファイル
├── helpers/ # ドメインに依存しない汎用的なヘルパー関数
├── constants/ # なんとなく共通で使う定数定義
└── types/ # なんとなく共通で共通で型定義
抱えていた課題
はい。構成にコメントで記載の通り、定数や型を、「なんとなくプロジェクト全体で使うからここにおいておこう」というニュアンスで配置していました。
これにより、同じドメインに関連する情報が複数の箇所に散らばってしまうという事象、それに伴う問題に直面します。
実際には以下のように散らばっていました。
-
型定義:
types/に配置 -
定数:
constants/に配置 -
ビジネスロジック: 各コンポーネント内や
features/内に散在
例として、「お客様がアップロードした請求書ファイルのステータス」が実際どのように散らばっていたかというと・・・
定数は constants/index.ts に集約。
// frontend//constants/index.ts
export const INVOICE_STATUSES = ["pending", "checked", "completed"] as const;
型定義は types/index.ts にあり、定数から型を生成。
// frontend//types/index.ts
import type { INVOICE_STATUSES } from "@/constants";
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
このように1つのドメインとして密接に絡んでいるものが分散してしまっています。
そしてページコンポーネント内においては、なんと下記のように請求書のステータスを重複実装してしまっていました。
// frontend/components/pages/invoice/invoice-status-label.tsx
export const invoiceStatus = ["pending", "checked", "completed"] as const;
export type InvoiceStatus = (typeof invoiceStatus)[number];
const labels: Record<InvoiceStatus, Label> = {
pending: { variant: "dormant", text: "未対応" },
checked: { variant: "caution", text: "一次チェック済" },
checked: { variant: "completed", text: "確認完了" },
};
export function getStatusLabel(status: string) { /* ... */ }
これはまだましな例で、typesに型定義せず、各コンポーネント内や features/ 内に定義し、それを別コンポーネントから利用するという依存になっている箇所もありました。
そうなるともうどこがどこに依存しているのかを把握できなくなって保守性が、、、、というのを直感的にお分かりになると思います。
このようにドメインに関連する型、定数、ロジックが複数の場所に散らばっており、以下のような問題が発生していました。
- 発見コストの増大: 「請求書関連の型はどこにあるのか?」を探すのに、複数のディレクトリを確認する必要があった
-
重複の発生: 似たような定数や型が複数箇所に定義され、どれが正しいのか判断が困難だった
- ステータスに新しい値が追加された際、複数箇所を更新する必要があり、修正漏れのリスクがあった
このような事態に陥ってしまった理由は、以下だと考えています。
- 明確な設計方針の欠如: ドメイン知識をどこに配置すべきかという明確なルールやガイドラインが存在しなかった
-
「共通」という曖昧な基準: 「なんとなくプロジェクト全体で使うから」という曖昧な基準で
types/やconstants/に配置していたため、ドメインごとの関連性が失われた
ということで、じゃあどこかに集約したらいいじゃないという話になるのですが、どこにしようか?ということが次に考えることです。
そこで我々チームではFSD(Feature-Sliced Design)の概念として存在するentitiesレイヤーを部分導入することにしました。
FSD(Feature-Sliced Design)とは
FSDの概要
FSD(Feature-Sliced Design)は、フロントエンドアプリケーションをレイヤー、スライス、セグメントという3つの軸で整理するアーキテクチャ設計手法です。
この設計手法により、コードの責務が明確になり、依存関係の管理が容易になります。
レイヤーは、コードの抽象度に応じて以下のように分けられます。
app/ # アプリケーション全体の設定・初期化
pages/ # ページレベルのコンポーネント(ルーティング単位)
widgets/ # 独立したUIブロック(複数のfeaturesを組み合わせた複合コンポーネント)
features/ # 再利用可能なユーザーのアクションや機能(ビジネス機能単位)
entities/ # ビジネスエンティティ(ドメイン知識)
shared/ # 再利用可能な汎用コード(UIコンポーネント、ユーティリティ)
そして 依存関係は必ず上位から下位レイヤーに向ける必要があります。同レイヤー間の依存も不可です。
この一方向の依存関係により、変更時の影響範囲が予測しやすくなります。
依存方向もシンプルなため、Linterなどで簡単に制御できるのも魅力です。
実際に公式でsteigerというアーキテクチャLinterが公開されていたり、
eslintのプラグインもあります。
スライスは、各レイヤー内で機能やエンティティごとに分割した単位です。
例えば、features/check-invoiceは「請求書をチェックする際に利用するビジネス機能」が集約されていることになります。
セグメントは、各スライス内でコードの種類ごとに分類した単位です。標準化されているセグメントはありますが、独自セグメントの管理も問題ありません。
このように、レイヤー、スライス、セグメントの3つの軸で整理することで、関連するコードが近い場所にまとまり、変更時の影響範囲が明確になります。
entitiesレイヤーの役割
今回の主役entitiesレイヤーを少し深堀ります。
entitiesレイヤーはビジネスドメインに関する知識を集約する場所です。UIやユーザーアクションに依存しない、純粋なドメイン知識を配置します。
- ドメインの型定義: 型、Zodスキーマ等
- ドメイン固有の定数: ステータス値、ラベル、オプション等
- ビジネスロジック: ドメインルールに基づく処理(計算、変換等)
- バリデーション: ドメインオブジェクトの検証ロジック
また前述のレイヤー間の依存の方向制御の原則により、他のレイヤー(features、pages等)から参照されるが、それらのレイヤーには依存しません。
実際の導入内容
分散してしまったドメイン知識を整理するため、前述の構成に対してFSDのentitiesレイヤーの概念を導入。
ドメインエンティティごとにディレクトリを作成し、関連するものをまとめました。
frontend
├── components/
├── features/
├── helpers/
└── entities/ # ← 新規追加:FSDのentitiesレイヤー `types/`、`constants/`中身をここにドメインごとに集約
└── invoice/
└── invoice-status.ts
実装を前後比較
Before: 散在していた定数と型
// frontend/constants/index.ts
export const INVOICE_STATUSES = ["pending", "checked", "completed"] as const;
// frontend/types/index.ts
import type { INVOICE_STATUSES } from "@/constants";
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
// frontend/components/pages/invoice/invoice-status-label.tsx
// 重複定義してしまっていた
export const invoiceStatus = ["pending", "checked", "completed"] as const;
After: entitiesレイヤーで集約
// frontend/entities/invoice/invoice-status.ts
export const INVOICE_STATUSES = ["pending", "checked", "completed"] as const;
export type InvoiceStatus = (typeof INVOICE_STATUSES)[number];
export const INVOICE_STATUS_TEXT: Record<InvoiceStatus, string> = {
pending: "未対応",
checked: "Wチェック待ち",
completed: "対応済み",
};
entitiesの中を見れば、そのドメインに属する定数、関数、ビジネスロジックが一目瞭然になりましたね。
やったことはとてもシンプルです。この手軽さが導入のハードルを下げてくれます。
導入によって得られたメリット
このentitiesレイヤーの部分導入により、抱えていた課題を解消し、以下のメリットを享受することができました。
1. 開発効率の向上
- 発見コストの削減: 型や定数を探す時間の短縮(複数ディレクトリを確認する必要がなくなる)
- 学習コストの低減: 新規メンバーでも、ディレクトリ構造から「どこに何があるか」を推測しやすい
- 情報の集約: ドメイン単位でファイルがまとまっているため、関連する情報を一度に確認できる
2. 保守性の向上
- 重複の解消: 似たような定数や型が複数箇所に定義されることがなくなり、修正漏れを防げる
- 影響範囲の明確化: ビジネスロジックの変更や追加時、entities内を見れば影響範囲が明確になり、修正漏れを防げる
まとめ
本記事では、FSDのentitiesレイヤーを導入した背景から実際の効果まで通して紹介しました。
entitiesレイヤーだけの導入であれば、FSDの原則により他のレイヤー(features、pages等)に依存しないため、段階的に導入できるのも大きなメリットだなと感じました。
既存プロジェクトでも比較的低リスクかつお手軽に始められ、効果を実感しやすいと思います。
現時点ではentitiesだけですが、順次他レイヤーを導入をすることでよりFSDの恩恵を受けた堅牢なアプリケーションにしていきたいです。
また、FSDには他にも参照できるモジュールを制限するPublicAPI化など面白く保守性に寄与しそうな概念が残っているため、こちらも段階的に導入をしていけたらと思います。
Discussion