# ADR-0005: Layer-First アーキテクチャ + 機械強制 (feature-first 不採用)

- **Status:** Accepted
- **Date:** 2026-04-23 (確定) / 2026-04-30 (ADR 化)
- **Decision Drivers:** BFF と feature の所有権ミスマッチ、複数 feature から共有参照される table の境界、schema の feature 越境 import 問題、1 人開発 + AI 開発での迷いにくさ、ESLint / madge / dependency-cruiser での機械強制
- **Stakeholders:** dev@parky.co.jp (sole maintainer)、Flutter 業者投入時の API 構造把握

## Context

### 出発点

Parky API (`api/src/`) は当初、Hono のルーティング規約に従って `src/routes/<resource>.ts` のフラット構造で実装されていた。endpoint が増えるにつれ、SQL / ビジネスルール / response schema が同じファイルに混在する状態になった。

### 一度試した feature-first

2026-04-22 頃、`features/<feature>/` で機能単位に分けるアーキテクチャ (feature-first) を 1 度採用した。`features/parking-lots/`, `features/reviews/`, `features/auth/` のように feature ごとにフォルダを切る構成。

しかし以下の問題が出た:

1. **BFF は画面単位であって feature 所有物ではない**
   - 例: `mobile/views/lot-detail.ts` は parking-lot / reviews / pricing / nearby-spots を横断して合成する画面
   - 「どの feature の所有か」が決まらない (parking-lots/ に置くと reviews が import される、reviews/ に置くと parking-lots が import される)

2. **table (data) は特定 feature の所有物ではない**
   - 例: `app_users` table は auth / profile / reviews / sessions 等 5 feature 以上から共有参照される
   - feature-first だと「app_users.data.ts は何 feature 配下に置くか」が決まらない

3. **schema の feature 越境 import が頻発**
   - `features/parking-lots/schema/parking-lot.ts` を `features/reviews/` から import すると feature 境界が壊れる
   - feature-first を維持するには schema を feature ごとに重複定義する必要があり破綻

### 結論

2026-04-23 のアーキテクチャ議論で **Layer-First** に再確定。トップレベル 4 ディレクトリ + 共通 2 ディレクトリの 4+2 構成で固定。core 配下のみ capability/feature 単位のサブフォルダを許容する (capability は feature と異なり、技術的責務単位)。

モジュラーモノリス (単一 Cloudflare Workers デプロイ) で、関数境界のみで層を分ける。solo dev + AI 開発では BFF (画面) → core (機能) → data (テーブル) → schema (型) の 4 軸が最も迷いにくい。

## Decision

### フォルダ構造

```
parky/api/src/
├── bff/                      ← (1) 画面 / endpoint 単位
│   ├── mobile/
│   │   ├── views/            ← L3 SDUI View endpoint (画面 1 ファイル)
│   │   ├── actions/          ← L3 SDUI Action endpoint (操作 1 ファイル)
│   │   └── telemetry/        ← fire-and-forget 記録
│   ├── web/                  ← Web Home (Astro) 向け
│   ├── admin/                ← Admin Portal 向け
│   ├── owner/                ← Owner Portal 向け
│   ├── owner-public/         ← Owner Portal の認証不要 endpoint
│   ├── marketing/            ← Marketing Portal 向け
│   └── webhooks/             ← Stripe / Apple / Google からの webhook
├── core/                     ← (2) capability 単位 (機能分類 OK)
│   ├── parking-lots/
│   ├── parking-sessions/
│   ├── reviews/
│   ├── gamification/
│   ├── subscriptions/
│   ├── auth/
│   ├── users/
│   ├── notifications/
│   ├── pricing/              ← cross-domain orchestration
│   ├── home-feed/            ← cross-domain orchestration
│   ├── search/
│   └── ...
├── data/                     ← (3) データドメイン / table 単位
│   ├── parking-lots.data.ts  ← 1 ファイル = 1 table cluster
│   ├── parking-sessions.data.ts
│   ├── reviews.data.ts
│   ├── app-users.data.ts
│   └── ...
├── schema/                   ← (4) 型種別 × entity
│   ├── domain/               ← core が使う domain entity (camelCase)
│   ├── view/                 ← bff が使う View Model
│   └── row/                  ← data が使う DB row 型 (snake_case)
├── shared/                   ← 共通
│   ├── lib/
│   ├── middleware/
│   └── schema/
└── app/
    ├── routes-manifest.ts    ← 全 endpoint の SSoT
    └── index.ts
```

### 依存方向 (ESLint で強制)

```
bff   →  core / schema/{view,domain} / shared
core  →  core (sibling) / data / schema/{domain,row} / shared
data  →  schema/row / shared/lib
schema →  schema 内のみ / shared
shared →  依存なし (本当に横断するものだけ)
```

逆参照 / 越境 import はすべて lint error:

- `data → core / bff` 禁止
- `core → bff` 禁止
- `schema/view` を core / data が import 禁止 (view は bff 専有)
- `schema/row` を core / bff が import 禁止 (row は data 専有)

実装は `api/eslint.config.js` の `no-restricted-imports` パターンと、追加で SDUI L4/L5 シグナルキー禁止 ([ADR-0004](./0004-sdui-level-3-fixed.md))、`max-lines` 400 (warn)、`max-lines-per-function` 60 (error)、`complexity` 10 (error)、`max-depth` 4 (error)、`max-params` 4 (error) で機械強制。

### 各層の責務

| 層 | 書いていいこと | 書いてはいけないこと |
|---|---|---|
| **bff/** | Zod schema / ViewEnvelope 組立 / Domain → View 変換 / core 呼出 | SQL / if によるビジネス判定 / data 直叩き |
| **core/** | domain 演算 / 複数 data の orchestration / 他 core 呼出 | SQL 直書き / HTTP 応答知識 / `schema/view` import |
| **data/** | SQL / postgres.js / Row → Domain 変換 | ビジネスルール / HTTP 応答知識 / View 形状をそのまま返す |
| **schema/domain/** | camelCase の domain entity 型 | DB 列名そのまま / HTTP view 形式 |
| **schema/view/** | Zod schema (ViewEnvelope の data 部分) | domain entity の再定義 / row 型の流用 |
| **schema/row/** | snake_case の DB 列名マッピング | domain 意味論 / ビジネスルール |

### マッピング責務

- **Row → Domain は data の責務**: `rowToXxx()` を data ファイル先頭に inline
- **Domain → View は bff の責務**: `entryToViewItem()` 等を bff ファイル先頭に inline
- core は Row を知らない / bff も Row を知らない
- mapper 外出し閾値: 40 行超 / 複数種 / ネスト / テスト動機 のいずれかで `_internal/*.mapper.ts` へ退避

### 「テーブル変更が BFF に影響しない」保証

例: `app_users.display_name` → `nickname` へ列名変更するケース:

1. `data/app-users.data.ts` の SQL を修正
2. `schema/row/app-users.row.ts` の型を修正
3. `schema/domain/app-user.ts` は **touched しない** (domain は意味論を維持)
4. `core / bff` は touched しない

これが Layer-First で自動的に担保される。row と domain を分離しているのが決め手。

### 単純な CRUD の層省略

core が完全パススルー (listTags = listTagsData をそのまま呼ぶだけ) なら、core を作らず bff から直接 data を呼ぶのも許容。ただしロジックが出現したら即 core に引き上げる。

```typescript
// bff/mobile/views/codes-list.ts (単純なケース)
import { listCodesData } from "../../../data/codes.data";
export const listCodes = async (c) => c.json(await listCodesData(c.env));
```

### Multi-channel BFF

同じ core を異なる channel で提供する時は `bff/<channel>/` を並置:

```
bff/mobile/views/lot-detail.ts   ← Flutter mobile 向け (L3 SDUI)
bff/owner/views/lot-detail.ts    ← Owner Portal 向け (Resource API)
```

両方とも `core/parking-lots/load-lot-detail.ts` を呼ぶが、view 整形は独立。

### 新規 endpoint 追加時の手順

1. 画面 / 操作を決める → `bff/<channel>/<kind>/<name>.ts` を作る (Zod schema は `schema/view/<name>-view.ts`)
2. 必要な use case を決める → `core/<capability>/<use-case>.ts` を作る (なければ bff から直接 data でも OK)
3. 触る table を決める → `data/<table>.data.ts` に関数追加
4. `app/routes-manifest.ts` に entry 追記

新規 endpoint は **絶対に** `routes/` 配下に作らない。

### 既存 routes/*.ts の扱い

- Boy Scout: 触ったら同 PR で `bff/` に層分離して移動
- 共存期間: 3-6 ヶ月。`routes-manifest.ts` が `bff/` と既存 `routes/` の両方を mount 可能

### 機械強制の構成

- ESLint flat config (`api/eslint.config.js`) で `no-restricted-imports` / SDUI L4/L5 検出 / file size / function size / complexity
- `madge` で循環依存検出 (`scripts/check-madge.sh`)
- `dependency-cruiser` で layer 間の逆流検出
- CI workflow がこれらを PR で実行

## Consequences

### Positive

- 「どこに何を書くか」の判断が画面 → 機能 → テーブル → 型の 4 軸で決まる。1 人開発でも迷わない
- table 変更が BFF まで波及しない (Row / Domain 分離)
- core / data 層の分離により、business logic と SQL が混ざらない
- `routes-manifest.ts` が全 endpoint の SSoT になり、OpenAPI / Swagger / 公開 docs の自動生成が成立
- ESLint で逆参照禁止 + ファイル長 + 関数長 + 複雑度を error 化しているので、PR 段階で違反検出
- multi-channel BFF (mobile / web / admin / owner / marketing) を同じ core で支える構造が実現
- 業者参入時の「どこに何を書くか」のオンボーディングコストが低い

### Negative / Trade-offs

- ファイル数が増える (1 endpoint で bff + core + data + schema の 4-5 ファイル触る)
- 単純 CRUD でも data 層 + schema/row + schema/domain が必要 (例外: 非常に単純な read-only は core 省略可)
- mapper (`rowToXxx`, `entryToViewItem`) の inline コードが増える
- 業者が feature-first 経験のみで Layer-First 未経験の場合、立ち上がりコストが発生

### Mitigations

- `api/CLAUDE.md` に Layer-First 規約と禁止アンチパターンを明文化
- `parky/reference_parky_file_layout.md` (memory) でファイル位置予測表を提供 (Grep する前にこのパスで Read を試す)
- 単純 CRUD の層省略を明示的に許容 (small case の boilerplate を強制しない)
- ESLint がリアルタイムで違反検出 → エディタ上で即フィードバック
- 業者向け read-through reading として、典型実装ファイル (`parking-lots.read.ts`, `bff/mobile/views/lot-detail.ts`) を提示

## Alternatives Considered

- **Alternative A: feature-first (`features/<feature>/<layer>/`)**
  - 不採用理由: 上記 3 つの問題 (BFF 所有権 / table 共有 / schema 越境) で破綻。一度採用したが 2026-04-23 に Layer-First に再確定

- **Alternative B: μservice (Cloudflare Workers を機能ごとに分割)**
  - 不採用理由: 1 人開発で μservice はオーバーヘッドが大きすぎる。Worker 間通信 / デプロイ複雑度 / 横断トランザクション喪失。モジュラーモノリスのまま 4 wrangler*.toml で Worker 分割するほうが筋が良い ([ADR-0010](./0010-wrangler-multi-toml-strategy.md))

- **Alternative C: Hono routes/ のフラット構造のまま**
  - 不採用理由: routes/ ファイル内に SQL / ビジネスルール / response schema が混在し、1 ファイル 1000 行超のケースが発生。テスト・レビューが困難

- **Alternative D: Hexagonal / Clean Architecture (Port & Adapter)**
  - 不採用理由: Hono + Cloudflare Workers の薄いランタイムに対しては overengineering。ports / adapters / use cases / entities の 4-5 層に分けるよりも、4 軸 (bff/core/data/schema) のシンプル分離のほうが Solo dev + AI 開発に適している

- **Alternative E: DDD bounded context (`domains/<context>/`)**
  - 不採用理由: feature-first と同じ問題 (画面横断の合成 / table 共有) が発生。DDD の本質である「ユビキタス言語」「集約境界」は schema/domain で部分的に取り入れる

## References

- 関連 memory:
  - `C:/Users/Taiga/.claude/projects/e--Claude/memory/parky/feedback_parky_layer_first_architecture.md` (2026-04-23 確定の原典)
  - `C:/Users/Taiga/.claude/projects/e--Claude/memory/parky/reference_parky_file_layout.md` (ファイル位置予測表)
  - `C:/Users/Taiga/.claude/projects/e--Claude/memory/parky/feedback_api_natural_key_strategy.md` (API 層の natural key 戦略)
  - `C:/Users/Taiga/.claude/projects/e--Claude/memory/parky/feedback_db_function_default_bff_only.md` (DB 関数 bff_only 配置)
- 関連 code:
  - `e:/Claude/high-field/parky/api/eslint.config.js` (依存方向の機械強制)
  - `e:/Claude/high-field/parky/api/CLAUDE.md` (Layer-First 規約 / 禁止アンチパターン)
  - `e:/Claude/high-field/parky/api/src/bff/` / `core/` / `data/` / `schema/` 各ディレクトリ
  - `e:/Claude/high-field/parky/api/src/app/routes-manifest.ts` (endpoint SSoT)
  - `e:/Claude/high-field/parky/api/scripts/check-madge.sh` (循環依存検出)
- 関連 docs:
  - `../architecture.html`
  - `../conventions.html#layer-first`

## Revisit Triggers

- 開発者数が 5 人以上に拡大し、feature-first / DDD bounded context のメリットが Layer-First のメリットを上回る時
- core 配下の capability が 50 を超えて整理が必要になった時 (capability の sub-grouping を検討)
- ESLint / madge の検出ルールでは捕捉できない違反パターンが発生し、機械強制が崩れた時
- Cloudflare Workers から別ランタイムに移行する場合、ランタイム固有の制約 (isolate / I/O 分離) で構造変更が必要になった時
