0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

要件から逆算するアーキテクチャ設計 - ユースケース駆動で「非機能→構成→実装」まで一気通貫

Posted at

この記事で伝えること

  • 画面起点ではなくユースケース起点で要件を落とす方法
  • 非機能要件(NFR)→意思決定→参照構成の一本道
  • React / Vue / 純JS + Node(Fastify, Node 20)による薄い実装例
  • 設計を残すADR(Architecture Decision Record)テンプレとレビュー・チェックリスト

1. 背景

「とりあえず画面とAPIを作る」から卒業する。上流で合意すべきは業務ユースケース・品質・コストであり、ここを言語化してから構成・コードへ降ろす。
本稿は、その逆算の“道筋”を示す。


2. 要件から非機能へ(NFR決定表の型)

業務要件(ビジネスフロー)をユースケースに分解し、NFRを定量化する。

観点 指標(例) 目標値例 備考
可用性 稼働率 99.9% 業務時間帯のみ等も可
性能 95Pレスポンス ≤ 300ms 画面別に設定
スケール 同時接続 2,000 ピーク帯の根拠を明記
セキュリティ 認可粒度 行レベル PII取り扱い有無
運用 監査証跡 保持12カ月 参照UIの要否
コスト 月額上限 35万円 変動幅と根拠

ポイント

  • 目標値をユースケース単位で決める(画面横断の平均値は不可)
  • 根拠(トラフィック仮説・利用率)を設計文書の同一箇所に残す

3. 意思決定を残す:ADRテンプレ(TypeScript)

「なぜそうしたか」を残さなければ、上流の判断は再現できない。
(通常md形式が主流ですが、私がよくTSで書いてるので、それを掲載)

// /docs/adr/0001-bff-and-api-composition.ts
export type ADR = {
  id: string; date: string; title: string;
  context: string; decision: string; alternatives: string[];
  consequences: { positive: string[]; negative: string[] };
  nfrImpact: { availability?: string; latency?: string; security?: string; cost?: string; };
};

export const ADR_0001: ADR = {
  id: "ADR-0001",
  date: "2025-07-29",
  title: "BFF + API Composition を採用する",
  context: "フロントごとにAPIの粒度要件が異なる。バックエンドは複数サービス。",
  decision: "画面要件に近い粒度はBFFで合成。バックエンドは疎結合を維持。",
  alternatives: ["バックエンド統合", "GraphQLの集中導入", "フロント直叩き"],
  consequences: {
    positive: ["UI変更の影響をBFFに閉じ込められる", "バックエンドの責務が明瞭"],
    negative: ["BFFの運用・性能管理が必要", "二重キャッシュ設計の複雑化"]
  },
  nfrImpact: { latency: "BFFで集約→RTT削減", cost: "BFF運用コスト増を許容" }
};

4. 参照構成(論理図)

[Browser (React / Vue / JS)]
        │ HTTPS
        ▼
[BFF (Node 20 + Fastify)]───[AuthZ/Cache(Redis)]
        │
        ├── [Service A (REST)]
        ├── [Service B (REST)]
        └── [Reporting / Query DB]

意図

  • BFFでユースケース粒度へ合成、リトライ/タイムアウト/サーキットブレーカを集中
  • 認可はBFFで最終判定(画面条件を考慮)
  • 読み取り最適化(集計/二次インデックス)は別系統へ

5. ユースケース駆動の粒度設計(例)

医療系の案件を請け負うことが多いので、患者一覧を例にします。
UC-01:患者一覧の検索(複合条件・ページング)

  • 入力:診療科、在院/退院、期間、フリーテキスト
  • 出力:20件/ページ、合計件数
  • NFR:95P≤300ms、同時500

BFF(Fastify, Node 20, TS)

import Fastify from "fastify";
const app = Fastify();

app.get("/api/patients", async (req, reply) => {
  const q = req.query as { dept?: string; status?: string; page?: string; s?: string; };
  const page = Number(q.page ?? 1);
  const timeout = 1500; // ms (NFRから逆算)
  // 並列呼び出しとタイムアウト制御(擬似)
  const [list, total] = await Promise.all([
    fetchWithTimeout(`http://svc/list?...`, timeout),
    fetchWithTimeout(`http://svc/count?...`, timeout),
  ]);
  return reply.send({ items: list.items, total, pageSize: 20, page });
});

app.listen({ port: 3000 });

React(TSX)

import { useEffect, useState } from "react";

export function PatientList() {
  const [state, setState] = useState({ items: [], total: 0, page: 1, loading: false });

  useEffect(() => {
    setState(s => ({ ...s, loading: true }));
    fetch(`/api/patients?page=${state.page}`)
      .then(r => r.json())
      .then(d => setState(s => ({ ...s, ...d, loading: false })));
  }, [state.page]);

  return (
    <div>
      <h2>患者一覧</h2>
      {state.loading ? "loading..." : (
        <>
          <ul>{state.items.map((x: any) => <li key={x.id}>{x.name}</li>)}</ul>
          <p>{state.total}</p>
        </>
      )}
    </div>
  );
}

Vue 3(<script setup lang="ts">

<script setup lang="ts">
import { ref, watchEffect } from "vue";
const page = ref(1);
const items = ref<any[]>([]);
const total = ref(0);
const loading = ref(false);

watchEffect(async () => {
  loading.value = true;
  const r = await fetch(`/api/patients?page=${page.value}`);
  const d = await r.json();
  items.value = d.items; total.value = d.total; loading.value = false;
});
</script>

<template>
  <div>
    <h2>患者一覧</h2>
    <div v-if="loading">loading...</div>
    <ul v-else>
      <li v-for="x in items" :key="x.id">{{ x.name }}</li>
    </ul>
    <p>{{ total }}</p>
  </div>
</template>

純JavaScript(理解用ミニ)

<script>
let page = 1;
async function load() {
  const d = await fetch(`/api/patients?page=${page}`).then(r=>r.json());
  document.querySelector("#total").textContent = d.total + "";
  document.querySelector("#list").innerHTML = d.items.map(x=>`<li>${x.name}</li>`).join("");
}
load();
</script>
<ul id="list"></ul><p id="total"></p>

6. アンチパターン → 改善

  • アンチ:Controllerで外部APIを直列多段呼び出し(RTT増, タイムアウト不統一)
  • 改善:BFFで並列合成 + 共通のリトライ/タイムアウト戦略 + キャッシュ鍵設計

7. 設計レビュー・チェックリスト

  • ユースケースごとにNFRが定量化されているか
  • そのNFRからタイムアウト/リトライ/並列のパラメータに落ちているか
  • 認可は画面条件に依存するルールをBFFで最終判定しているか
  • ADRに代替案と負の帰結まで記録したか
  • 観測(トレーシングID、重要メトリクス)は最初から埋め込み済みか

8. まとめ

ユースケース→NFR→ADR→参照構成→薄実装。この一本道で「上流もできる」を可視化できる。コードは薄く、意思決定を厚く残すのが肝要である。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?