第07章:設計入門② “責務”ってなに?(薄い層の意識)🧁📚
この章のゴール🎯✨
- 「この処理、どこに書けばいいの?」で迷う回数を減らす🧭💡
- Controller / Service / Repository の“役割分担”を、サッと説明できるようになる💬
- Sagaを作るときに、コードがゴチャッと混ざるのを防ぐ🧯🧩
1) “責務”ってなに?🤔🧩
**責務 = その部品が引き受ける仕事の範囲(担当範囲)**だよ😊✨
たとえば「注文を作る」って一言でも、実は中身がいろいろあるの👇
- HTTPリクエストを受け取る📩
- 入力チェックする✅
- どんな順番で処理するか決める(ビジネス手順)🧠
- DBに保存する🗄️
- エラーが起きたらどう返すか決める⚠️
- ログや追跡IDを付ける🔎
この全部を1つの関数に詰め込むと、すぐに“迷子コード”になる😵💫💦 だから **「責務を分ける」**のが設計の最初の一歩だよ🚶♀️🌱
2) “薄い層”ってどういう気持ち?🧁
「薄い層」っていうのは、ざっくり言うと👇
- **各層は“やることを少なく”**して、判断を分散しない
- 上の層は下の層を使うだけ(下の層の事情に踏み込まない)
- それぞれが“自分の仕事だけ”をして、他人の仕事を奪わない🙅♀️
これをやると嬉しいことがいっぱい😊💕
- 修正が局所で済む🔧
- テストしやすい🧪
- 追加機能で破壊しにくい🛡️
- 「ここ見ればいい」が分かりやすい🗺️
3) まずは王道:Controller / Service / Repository 👑
バックエンドで超よくある分け方がこの3つだよ👇 (NestJSみたいなTypeScript向けフレームワークでも、Controller と Service(Provider)中心で組み立てる考え方が基本にあるよ😊) (NestJS Docs)
Controller(入口)🚪📩
やること:外から来たものを受け取って、返す
- リクエスト/レスポンスの整形
- 入力の軽いバリデーション
- 認可/認証の“入り口”(詳細は別部品に任せがち)
- Serviceを呼ぶだけ(ここが大事!)✨
✅ 合言葉:Controllerは薄く!(ビジネス判断を置かない) (GitHub)
Service(手順・判断の中心)🧠🧑🍳
やること:ビジネスの手順を組み立てる(ユースケース)
- 「注文→決済→在庫→発送」みたいな流れを決める
- “どこまで成功した?”を見て次を決める
- エラーの扱い(リトライする?補償する?)の判断
- いろんなRepositoryや外部I/O部品を組み合わせる
✅ 合言葉:手順(ユースケース)はServiceへ! (GitHub)
Repository(保存と取得)🗄️📦
やること:データの出し入れ(永続化)
- DBから取る
- DBに保存する
- できれば“ビジネス判断”は入れない(入れると地獄)😇💦
- ORMやSQLの都合をここに閉じ込める
「RepositoryパターンでDB/ORMへの依存を薄くする」考え方は、NestJS界隈でもよく語られるよ🧩 (Medium)
✅ 合言葉:Repositoryはデータ係! 📚

4) Sagaと“責務”の相性🍱✨(混ざりやすいポイント注意!)
Sagaって「複数ステップの手順+失敗時の戻し」だから、放っておくと全部が混ざりやすい😵💫💦
だから、責務をこう割るとスッキリしやすいよ👇
-
Controller:Saga開始の入口(例:POST /orders)
-
Service(または Orchestrator):Sagaの手順の司令塔(次のStepは何?失敗なら補償?)🎻
-
Repository:
- 注文データの保存
- Saga状態(進捗)の保存(どこまで成功したか)
- “実行済み”などの記録
「層を分けて、Web層が勝手に下層へ侵入しない」みたいな設計指針は、Nodeのベストプラクティスでも強調されがちだよ🔒 (GitHub)
5) 迷ったときの“置き場所ルール”🧭✨
「どこに書くべき?」ってなったら、この質問を順番にしてね😊
Q1: これはHTTPの話?それともビジネスの話?🌍
- HTTPの話(ステータスコード、req/res整形) → Controller
- ビジネスの話(注文できる条件、実行順、補償判断) → Service
Q2: これはDB/保存の話?🗄️
- SQL/ORM/保存形式が絡む → Repository
Q3: “手順”なの?“部品”なの?🧩
- 手順(AしてBしてCして…) → Service
- 部品(保存する、送る、呼ぶ) → Repositoryや外部I/O用のクラス
6) ミニ例:注文Sagaの“薄い層”サンプル🛒🧯
6-1. フォルダ構成イメージ📁✨
order.controller.ts(入口)order.service.ts(ユースケース)order.repository.ts(DB)orderSaga.orchestrator.ts(Saga司令塔:Serviceに含めてもOK)
「層で分ける or 機能(feature)で分ける」は、近年のNode/TSでも定番の整理軸としてよく出るよ📦 (DEV Community)
6-2. Controllerは“薄く”✨(Serviceを呼ぶだけ)
// order.controller.ts
import { OrderService } from "./order.service";
export class OrderController {
constructor(private readonly orderService: OrderService) {}
async createOrder(reqBody: { userId: string; items: Array<{ sku: string; qty: number }> }) {
// ✅ 入口の軽いチェック(重すぎない)
if (!reqBody.userId) throw new Error("userId is required");
if (!reqBody.items?.length) throw new Error("items is required");
// ✅ ビジネスの手順はServiceへ
const result = await this.orderService.startOrderSaga(reqBody);
// ✅ レスポンス整形(ここはControllerの仕事)
return { orderId: result.orderId, sagaId: result.sagaId };
}
}
6-3. Serviceが“手順”を持つ🧠🎻
// order.service.ts
import { OrderRepository } from "./order.repository";
import { OrderSagaOrchestrator } from "./orderSaga.orchestrator";
export class OrderService {
constructor(
private readonly orderRepo: OrderRepository,
private readonly saga: OrderSagaOrchestrator
) {}
async startOrderSaga(input: { userId: string; items: Array<{ sku: string; qty: number }> }) {
// ✅ ここからは「注文を成立させるための手順」
// 1) 注文を“下書き”で作る(ビジネス判断)
const order = await this.orderRepo.createDraftOrder(input.userId, input.items);
// 2) Saga開始(手順の司令塔に任せる)
const sagaId = await this.saga.start({
orderId: order.orderId,
userId: input.userId,
items: input.items,
});
return { orderId: order.orderId, sagaId };
}
}
6-4. Repositoryは“データ係”🗄️📦
// order.repository.ts
type OrderRow = { orderId: string; userId: string; status: "DRAFT" | "CONFIRMED" | "CANCELLED" };
export class OrderRepository {
// ここではDB接続などを想定(省略)
async createDraftOrder(userId: string, items: Array<{ sku: string; qty: number }>) {
// ✅ 永続化の詳細はここに閉じ込める
const orderId = crypto.randomUUID();
const row: OrderRow = { orderId, userId, status: "DRAFT" };
// insert into orders ... (省略)
// insert into order_items ... (省略)
return row;
}
}
7) ありがち事故あるある😇💥(そして直し方)
事故①:Controllerが太る🍔💦
- Controllerに「決済失敗なら返金して…在庫戻して…」みたいな手順が書かれてる ✅ 対策:手順はService/Sagaへ移動!
事故②:Repositoryにビジネス判断が入り込む🌀
if (order.total > 10000) discount...がRepositoryにある ✅ 対策:判断はService/Domainへ!Repositoryは保存だけ!
事故③:ServiceがHTTPの都合を持つ📮
- Serviceが
res.status(400)とか返し始める ✅ 対策:HTTPはControllerへ戻す!
8) AI活用:迷子を減らす“お願いテンプレ”🤖💬✨
コピペで使えるよ〜😊
- 「この関数を Controller / Service / Repository に分けて、責務をコメントで説明して」
- 「Controllerは薄く、Serviceに手順、Repositoryに永続化。ルール違反があれば指摘して」
- 「SagaのOrchestratorとして妥当か、責務の境界が崩れてないかレビューして」
AIは雛形づくりが得意だから、**“分け案を出させて、人間が責務チェックする”**のが相性いいよ✅💕
9) 章末演習:注文処理の責務を3つに分けよう📝✨
お題😈
次の「全部入り関数」を、Controller / Service / Repositoryに分割してね!
async function createOrderEverything(reqBody: any) {
// 入力チェック
if (!reqBody.userId) throw new Error("userId missing");
// DB保存
const orderId = crypto.randomUUID();
// insert into orders...
// 決済API呼び出し
// charge...
// 在庫確保
// reserve...
// 失敗したら返金・在庫戻し
// refund / release...
return { orderId };
}
やること✅
- Controller:入力を受けてServiceを呼ぶだけにする🚪
- Service:手順(Saga開始)を書く🧠
- Repository:注文の保存だけにする🗄️
チェックポイント🔎
- Controllerに“手順”が残ってない?
- Repositoryに“判断”が入ってない?
- Serviceに“永続化の細かいSQL”が入ってない?
(解答例は、6章のミニ例をベースにすればOKだよ😊🫶)
まとめ🌸
- 責務 = 担当範囲。混ぜると迷子になる😵💫
- Controllerは薄く、Serviceが手順、Repositoryが保存🧁
- Sagaは混ざりやすいから、司令塔(手順)をService/Orchestratorに寄せるのがコツ🎻
- 迷ったら「HTTP?手順?保存?」で仕分け🧭✨