SDD
Specification-Driven Development (SDD) 完全解説
1. はじめに
Specification-Driven Development(仕様駆動開発、以下 SDD) とは、文字どおり「仕様を起点にソフトウェアを構築する」という開発スタイルの総称です。コードを先に書いてから後付けでドキュメントを整えるのではなく、まず仕様を実行可能な、もしくは機械可読な成果物として 確立し、その仕様からテスト・実装・ドキュメント・モックを自動的に派生させる――この発想が SDD の中核にあります。
ただし、SDD という言葉は単一の手法を指すわけではありません。1970 年代から綿々と続く形式仕様の世界、2010 年代の API ファースト運動、Dan North らによる BDD、そして 2024〜2025 年に GitHub Spec Kit や Amazon Kiro が登場して一気に脚光を浴びた AI 時代の Spec-Driven Development ――これらすべてを覆う傘のような言葉になっています。
本稿では、これらの異なる「SDD の流派」を歴史的系譜とともに整理し、共通する設計原則、各流派固有の道具立てとワークフロー、そして 2026 年現在もっとも実務的にホットな AI 連携型 SDD を中心に、コード例・設定例・図解を交えて約 30 ページ相当の規模で詳述します。
1.1 なぜいま「仕様」なのか
ソフトウェア工学の歴史は、ある意味で「何を作るか」と「どう作るか」のあいだの主導権争いの歴史でした。ウォーターフォールは前者に偏りすぎ、アジャイルは後者に揺り戻し、TDD は両者をテストという成果物で接続しようと試みました。SDD は、いま再び「何を作るか」を機械が読める形で前置する 方向への揺り戻しと位置づけられます。
その背景には三つの圧力があります。
- マイクロサービスの爆発 ―― サービス間の境界を契約として固定しないと、分散モノリスに堕する。
- API エコノミー ―― 公開 API の互換性破壊は事業リスクであり、契約の自動検証が不可欠。
- 生成 AI の台頭 ―― LLM は曖昧な指示では破綻するが、よく書かれた仕様を渡せば極めて忠実にコードを生成する。「プロンプト=仕様」という気付きが、AI 時代の SDD を駆動している。
1.2 本稿で扱う SDD の 5 つの顔
本稿が扱う SDD は次の五つです。
| # | 流派 | 代表的成果物 | 時代 |
|---|---|---|---|
| 1 | 古典的形式仕様 | Z, VDM, B, TLA+, Alloy, Eiffel | 1970s〜 |
| 2 | API ファースト / 契約ファースト | OpenAPI, AsyncAPI, gRPC/Protobuf, GraphQL SDL | 2010s〜 |
| 3 | BDD / 実行可能仕様 | Gherkin, Cucumber, SpecFlow, Behave | 2006〜 |
| 4 | AI 時代の Spec-Driven Development | spec.md, plan.md, tasks.md, constitution.md(Spec Kit / Kiro) | 2024〜 |
| 5 | DDD の Specification パターン | Specification オブジェクト | 2003〜 |
これらは矛盾するものではなく、しばしば重ね合わせて使われます。たとえば AI 時代の Spec Kit ワークフローのなかで OpenAPI を生成し、その上で Gherkin の受入テストを書く、というのは 2026 年現在ごく一般的な構成です。
1.3 SDD と「Vibe Coding」の対比
2024 年に Andrej Karpathy が提唱して流行した vibe coding(雰囲気でコードを書く)は、AI ペアプログラミングの楽しさを謳う一方で、規模が大きくなると破綻することが多くの実務報告で示されました。SDD はその反動として、AI に対しても 明示的な仕様 を入力として与えることで、再現性とガバナンスを取り戻そうとする運動です。
「Vibe で書けるのは MVP まで。プロダクションに出すには Spec が要る」――これが 2025 年〜2026 年の合言葉になりました。
1.4 本稿のロードマップ
第 2 章で SDD の歴史的系譜をたどり、第 3 章で全流派に共通する原則を抽出します。第 4 章から第 7 章で各流派を順に深掘りし、第 8 章でアーキテクチャ統合、第 9 章でツール一覧、第 10 章と第 11 章で具体的なミニプロジェクト二本を扱います。最後にアンチパターン、CI/CD 統合、メリデメ、他手法比較、まとめへと進みます。
2. 歴史と系譜
SDD を一枚の絵で捉えるには、ソフトウェア工学が「仕様」をどう扱ってきたかの 50 年史を眺めるのが近道です。本章では五つの大きなマイルストーンを順に見ていきます。
2.1 形式仕様の時代(1970s〜1990s)
1968 年の NATO Software Engineering Conference で「ソフトウェア危機」が叫ばれて以降、研究者たちは 数学的に厳密な仕様言語 を追求しました。
- Z 記法(1977〜、Oxford の Jean-Raymond Abrial と Jean Sufrin) ―― 集合論と一階述語論理で状態と操作を記述。型付きの集合論を採用し、スキーマと呼ばれるブロックで状態と前提・事後条件を束ねる。
- VDM(Vienna Development Method, 1972〜、IBM Wien) ―― PL/I の意味論記述から派生し、Meta-IV を経て VDM-SL に至る。明示的な前条件・事後条件と精錬(refinement)の理論を持つ。
- B-Method(1996〜、Abrial) ―― Z の経験を踏まえ、機械的な精錬と証明をより重視。フランス国鉄 SNCF の地下鉄信号システム(パリ 14 号線、ロンドン Victoria 線)の実装に使われ、運用 20 年以上で重大欠陥ゼロという伝説的実績を残す。
- TLA+(1994〜、Leslie Lamport) ―― 時相論理 (Temporal Logic of Actions) で並行・分散システムを記述。Amazon Web Services が S3, DynamoDB, EBS など主要サービスの設計検証に使い、2015 年の論文 "How Amazon Web Services Uses Formal Methods" で広く知られた。
- Alloy(2002〜、MIT の Daniel Jackson) ―― 軽量形式手法。リレーショナル論理 + SAT ソルバ。"have your cake and eat it too" の思想で、形式仕様の有用性を保ちつつ学習曲線を抑える。
これらの言語の到達点は高いものの、習得コストの高さから一般のプロダクト開発には浸透しませんでした。しかし思想――実装に先立って、機械可読な仕様を書く――は後の世代に強く受け継がれます。
2.2 Design by Contract(1986〜)
1986 年、Bertrand Meyer は Eiffel 言語と同時に Design by Contract(契約による設計) を提唱しました。これは Z や VDM の重厚な体系をプログラミング言語の中に軽量に埋め込む 試みです。
- 前条件 (precondition): 関数呼び出し側が満たすべき条件
- 事後条件 (postcondition): 関数が満たすことを保証する条件
- 不変条件 (invariant): クラスインスタンスが常に保つ条件
class ACCOUNT
feature
deposit (amount: INTEGER)
require
amount_positive: amount > 0
do
balance := balance + amount
ensure
balance_increased: balance = old balance + amount
invariant
balance_non_negative: balance >= 0
end
DbC の遺伝子は、Java の assert、Python の icontract、Rust の debug_assert!、TypeScript の zod 等の入力検証ライブラリ、そして OpenAPI の required / minLength / pattern といった制約宣言にまで延びています。
2.3 Specification パターン(2003)
Eric Evans の Domain-Driven Design(2003)は、Specification を一種のデザインパターンとして紹介しました。これは「ある対象がビジネスルールを満たすか」をオブジェクトとして表現し、組み合わせ可能(and/or/not)にするテクニックです。
interface Spec<T> {
isSatisfiedBy(t: T): boolean;
and(other: Spec<T>): Spec<T>;
or(other: Spec<T>): Spec<T>;
not(): Spec<T>;
}
class PremiumCustomer implements Spec<Customer> { /* ... */ }
class HasOverdueInvoice implements Spec<Customer> { /* ... */ }
const eligibleForReminder = new PremiumCustomer().and(new HasOverdueInvoice());
これは本稿の主題である「仕様で開発を駆動する」とはやや違うレイヤーの話ですが、同じ用語が使われるため触れておきます。
2.4 API ファースト運動(2010s)
REST と JSON が事実上の標準になり、Swagger(2011 年初公開、後に OpenAPI Specification として 2015 年 Linux Foundation 配下に移管) が普及するに従って、「コードより先に API スキーマを書く」という運動が起こります。
- 2011: Swagger 1.0
- 2014: Tony Tam らが SmartBear に Swagger を売却、商標が OpenAPI Initiative に寄贈される
- 2017: OpenAPI 3.0
- 2021: OpenAPI 3.1(JSON Schema 2020-12 と整合)
- 同時期: gRPC(Google, 2015)、GraphQL(Facebook, 2015 公開)、AsyncAPI(2017、イベント駆動向け)が登場
「API ファースト」は単なるドキュメンテーションではなく、コードジェネレーション・モック・契約テスト・破壊的変更検出の自動化を可能にしました。Stripe、Twilio、GitHub などの API 企業がこの方法論を主導します。
2.5 BDD と Gherkin(2006〜)
2006 年、Dan North は TDD のテスト名が技術的すぎてビジネス側と共有しづらいことを問題視し、Behavior-Driven Development (BDD) を提唱しました。彼が作った JBehave、後に Aslak Hellesøy が Ruby に移植した RSpec / Cucumber は、Given / When / Then という自然言語に近い構文(Gherkin)で要件を書き、それをそのままテストとして実行可能にしました。
Feature: ATM withdrawal
Scenario: Successful withdrawal within balance
Given my account balance is 1000
When I withdraw 300
Then my balance should be 700
BDD は「ビジネス側・開発者・QA の Three Amigos が一緒に仕様を書く」という文化を伴って広がり、SpecFlow(.NET)、Behave(Python)、Cucumber-JVM、Godog(Go)などへと多言語展開しました。
2.6 AI 時代の Spec-Driven Development(2024〜2025)
転機は 2022〜2023 年の ChatGPT および GitHub Copilot の急進化、そして 2024 年の Claude Code・Cursor といった エージェント型コーディングツール の登場です。これらは数千行のコードを一度に生成できる代わりに、要求が曖昧だと「もっともらしいが間違った」コードを大量生産します。
その対策として 2025 年に二つの主要なフレームワークが公開されました。
- GitHub Spec Kit(2025 年 9 月公開、OSS):
/specify/plan/tasks/implementという四段階のスラッシュコマンドで、自然言語要求から実行可能タスクへ降ろしていく。Claude Code、Copilot、Gemini CLI、Cursor など複数のエージェントに対応。 - Amazon Kiro(2025 年プレビュー): AWS 寄りのワークフロー。
requirements.md→design.md→tasks.md→ 実装、という三本柱と、各フェーズで人間レビューを挟むゲートが特徴。.kiro/steering/に常時参照されるガイドライン群を置く。
これらに加え、Cursor Rules (.cursorrules / .cursor/rules/*.mdc)、Claude Code の CLAUDE.md と .claude/agents/、Aider の規約ファイルなど、エージェントに「常時リマインドする規約」を与える仕組みが乱立しています。これらすべてを束ねる傘が AI-era SDD です。
2.7 系譜図
この図が示すように、SDD は単一の発明ではなく、半世紀かけて醸成された複数の伝統が AI 時代に合流したもの です。次章では、その合流点で何が「共通言語」として残ったのかを整理します。
3. 共通する考え方
形式仕様、API ファースト、BDD、AI 時代の SDD ――どれも別の文脈で生まれたものですが、実装ではなく仕様を中心に据える という点で共通の哲学を持ちます。本章ではその共通項を六つの原則に整理します。
3.1 単一の真実源 (Single Source of Truth)
仕様は 真実の唯一の源 であり、コード・テスト・ドキュメント・モック・契約検証はすべてそこから派生します。OpenAPI ファイル、.feature ファイル、spec.md ―― いずれも「これが正、それ以外はその影」と扱う姿勢が SDD の根幹です。
実務上、「Single Source of Truth」が崩れる典型的な瞬間は次の三つです。
- コード優先で「あとで仕様を直す」と先送りする。
- 二重管理(PDF の仕様書 + Git の OpenAPI ファイル)。
- 生成された成果物を手で書き換える ―― 例えば
openapi-generatorの出力に直接パッチを当てる。
SDD のツールチェーンは、これらが起きにくいよう設計されています。
3.2 機械可読性 (Machine Readability)
人間用ドキュメントだけでは不十分で、機械が解釈できる構文 が必須です。
| 流派 | 機械可読仕様の形式 |
|---|---|
| 形式仕様 | TLA+, Alloy のテキスト形式(モデルチェッカが処理) |
| API ファースト | OpenAPI YAML/JSON, .proto, .graphqls |
| BDD | Gherkin(Cucumber が AST に解析) |
| AI 時代 SDD | Markdown だが構造化されたヘッダ・テーブル(LLM がパース) |
最後の AI 時代 SDD については、Markdown はやや「機械可読」と呼ぶには緩いものの、LLM が構造的に解釈できる程度には規格化されています。Spec Kit が定義する spec.md のセクション構造(Goal / Non-Goals / Scenarios / Functional Requirements / Acceptance Criteria)はその一例です。
3.3 仕様先行 (Spec Ahead of Code)
すべての SDD は 時間順序として仕様を先に置く ことを要求します。これは厳密にウォーターフォール的でなくてもよく、しばしば「仕様→実装→反映」の小さなループを高速に回します。
ループの始点と終点が 常に Spec である こと、これが「仕様先行」の本質です。
3.4 トレーサビリティ (Traceability)
要求 → 仕様 → 設計 → コード → テスト → 運用ログ、の各成果物が ID で結びつくこと。これがなければ「あの障害はどの要件由来か」「この行を消したら何が壊れるか」が答えられません。
- BDD では
@JIRA-1234のようなタグをScenarioに付ける。 - OpenAPI では
x-requirement-idなどの拡張プロパティで要件 ID を埋め込む。 - Spec Kit では
tasks.mdの各タスクがspec.mdの章番号を参照する。
3.5 実行可能性 (Executability)
仕様は人間が読める文書 であると同時に、機械が実行・検証できる べきという原則。
- Gherkin → Cucumber が動作テストとして実行。
- OpenAPI → Schemathesis や Dredd が API を自動的にプロパティテスト。
- TLA+ → TLC モデルチェッカがすべての到達状態を網羅検証。
- spec.md → Spec Kit が
/tasksで実装プランに変換、エージェントが実行。
3.6 進化耐性 (Evolvability)
仕様は固定された石板ではなく、変更履歴を持つ生きた文書。だからこそバージョン管理、差分検出、互換性検査が SDD の標準装備になります。
oasdiffで OpenAPI の破壊的変更を検出。buf breakingで Protobuf の互換性を自動検査。- Pact Broker が consumer 期待と provider 実装の差分を可視化。
3.7 まとめると
| 原則 | キーワード |
|---|---|
| Single Source of Truth | 仕様だけが正 |
| Machine Readability | 機械が解釈できる |
| Spec Ahead of Code | 時間順序で仕様が先 |
| Traceability | ID で繋がる |
| Executability | 仕様が動く |
| Evolvability | 仕様が進化する |
これらが揃って初めて SDD は機能します。たとえばコードジェネレータがあっても、Single Source of Truth が崩れていれば事故が起きます。AI エージェントが優秀でも、Traceability がなければレビュアは判断できません。これらは独立した原則ではなく、互いに補強し合うひとつの規律 なのです。
4. 形式仕様
形式仕様は SDD の最古の流派であり、最も厳密で、最も習得が難しいものです。本章では実務的に最も用例の多い TLA+ と、軽量で人気のある Design by Contract の二つを取り上げ、SDD の文脈で何が得られるかを示します。
4.1 TLA+ ―― 並行・分散システムの設計検証
TLA+ は Lamport が「ホワイトボードに書く設計図を機械が検証できる形に」する目的で作りました。アルゴリズムの状態遷移を時相論理で書き、TLC というモデルチェッカが全到達状態を網羅 して invariants を確認します。
4.1.1 簡単な例: 銀行口座の送金
二つの口座 A, B があり、合計残高は不変であってほしい。シングルプロセスではなく、複数プロセスから同時に送金が走る状況を考えます。
------------------------------ MODULE Transfer ------------------------------
EXTENDS Naturals, TLC
CONSTANTS Accounts, MaxAmount
VARIABLES balances
Init == balances = [a \in Accounts |-> 100]
Transfer(from, to, amt) ==
/\ from /= to
/\ amt > 0
/\ amt <= MaxAmount
/\ balances[from] >= amt
/\ balances' = [balances EXCEPT
![from] = balances[from] - amt,
![to] = balances[to] + amt]
Next == \E from, to \in Accounts, amt \in 1..MaxAmount : Transfer(from, to, amt)
Spec == Init /\ [][Next]_balances
\* Invariant: total balance is conserved
TotalConserved == LET total == 100 * Cardinality(Accounts)
IN (CHOOSE x \in Accounts : TRUE) \in Accounts =>
LET sum[S \in SUBSET Accounts] ==
IF S = {} THEN 0
ELSE LET p == CHOOSE x \in S : TRUE
IN balances[p] + sum[S \ {p}]
IN sum[Accounts] = total
=============================================================================
設定ファイル Transfer.cfg に Accounts = {a1, a2, a3} と MaxAmount = 50 を与えて TLC を走らせると、有限状態空間を網羅探索し TotalConserved が崩れる経路を探します。問題のあるアルゴリズム(送金の二段階更新がアトミックでない実装など)はここで反例トレースとして弾かれます。
4.1.2 AWS での運用例
Amazon は次のようなシステム設計に TLA+ を用いたと公表しています。
- DynamoDB: レプリカ間整合性プロトコル
- S3: 強整合化のためのアルゴリズム
- EBS: スナップショット
- internal lock manager: 分散ロック
「設計時にバグを潰しておくほうが、運用後に修正するより 1000 倍安い」が TLA+ 派の合言葉です。
4.2 Design by Contract の現代版
Eiffel ほど厳密でなくても、現代の言語は DbC のスピリットを取り入れています。
4.2.1 Python の icontract
from icontract import require, ensure, invariant
@invariant(lambda self: self.balance >= 0)
class Account:
def __init__(self, balance: int = 0):
self.balance = balance
@require(lambda amount: amount > 0)
@ensure(lambda self, amount, OLD: self.balance == OLD.balance + amount)
def deposit(self, amount: int) -> None:
self.balance += amount
@require(lambda self, amount: 0 < amount <= self.balance)
@ensure(lambda self, amount, OLD: self.balance == OLD.balance - amount)
def withdraw(self, amount: int) -> None:
self.balance -= amount
icontract は Hypothesis と組み合わせると、契約を満たすあらゆる入力を自動生成して検証してくれます。すなわち仕様(契約)からテストが派生する のです。
4.2.2 Rust の contracts クレート
use contracts::*;
#[invariant(self.balance >= 0)]
pub struct Account { balance: i64 }
impl Account {
#[pre(amount > 0)]
#[post(self.balance == old(self.balance) + amount)]
pub fn deposit(&mut self, amount: i64) {
self.balance += amount;
}
}
debug ビルドで契約違反を panic させ、release ビルドではコストゼロにする、というハイブリッドが現代的です。
4.3 軽量形式手法: Alloy
Alloy はリレーショナル論理 + SAT で「反例があるかを高速に探す」ことに特化しています。学習コストは TLA+ より低く、設計の妥当性を一晩寝かせる前にざっと検証する用途に適しています。
sig Account { balance : Int }
pred Transfer[from, to: Account, amt: Int, before, after: Account -> Int] {
amt > 0
before[from] >= amt
after = before
++ from -> minus[before[from], amt]
++ to -> plus[before[to], amt]
}
assert TotalConserved {
all from, to: Account, amt: Int, b1, b2: Account -> Int |
Transfer[from, to, amt, b1, b2] implies
sum a: Account | b1[a] = sum a: Account | b2[a]
}
check TotalConserved for 4
check で SAT ソルバが「アサートを破る世界」を探します。見つからなければ仕様は(指定スコープでは)健全です。
4.4 形式仕様と SDD のつながり
形式仕様は普段の CRUD 実装に直接使うものではありません。しかし、SDD の中核思想 ―― 仕様を機械が検証可能な形で前置する ―― の純粋な原型 であり、現代の OpenAPI 検証ツールや Pact、さらには AI エージェントへの仕様提示まで、すべての発想の源泉として理解しておく価値があります。
特に分散システム・並行処理・暗号プロトコルといった「コーナーケースが致命的」な領域では、TLA+ や Alloy を一週間学んで設計を一回検証するだけで、本番事故を一桁減らせる事例が多数報告されています。
5. API ファースト / 契約ファースト
実務的にもっとも普及している SDD の流派です。「コードを書く前に API スキーマを書く」――この一文が API ファーストの全てですが、その背後には大きなツールエコシステムが広がっています。
5.1 OpenAPI
REST API の事実上の標準仕様。OpenAPI 3.1 は JSON Schema 2020-12 と整合し、リクエスト・レスポンスを精密に表現できます。
5.1.1 完全な小さな仕様例: TODO API
openapi: 3.1.0
info:
title: Todo API
version: 1.0.0
description: A minimal example for SDD demonstrations.
servers:
- url: https://api.example.com/v1
paths:
/todos:
get:
operationId: listTodos
summary: List all todos
parameters:
- name: status
in: query
schema:
type: string
enum: [open, done]
responses:
'200':
description: A list of todos
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Todo'
post:
operationId: createTodo
summary: Create a new todo
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NewTodo'
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'400':
$ref: '#/components/responses/BadRequest'
/todos/{id}:
parameters:
- name: id
in: path
required: true
schema:
type: string
format: uuid
get:
operationId: getTodo
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Todo'
'404':
$ref: '#/components/responses/NotFound'
delete:
operationId: deleteTodo
responses:
'204':
description: Deleted
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
Todo:
type: object
required: [id, title, status, createdAt]
properties:
id: { type: string, format: uuid }
title: { type: string, minLength: 1, maxLength: 200 }
status: { type: string, enum: [open, done] }
createdAt: { type: string, format: date-time }
NewTodo:
type: object
required: [title]
properties:
title: { type: string, minLength: 1, maxLength: 200 }
Error:
type: object
required: [code, message]
properties:
code: { type: string }
message: { type: string }
responses:
BadRequest:
description: Invalid input
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
5.1.2 コードジェネレーション
この YAML から多くの成果物が自動生成できます。
- サーバスタブ:
openapi-generator-cli generate -i openapi.yaml -g go-server -o ./server - クライアント SDK:
openapi-generator-cli generate -i openapi.yaml -g typescript-axios -o ./sdk - モックサーバ:
prism mock openapi.yaml -p 4010 - API ドキュメント HTML:
redoc-cli build openapi.yaml - Postman コレクション:
openapi2postmanv2 -s openapi.yaml -o collection.json
Go では oapi-codegen がよく使われます。
oapi-codegen -package todo -generate types,server openapi.yaml > server.gen.go
oapi-codegen -package todo -generate types,client openapi.yaml > client.gen.go
生成された server.gen.go には Go の chi や echo 用のインターフェイスが定義され、開発者は実装本体だけを書けばよくなります。
5.1.3 検証 (Schemathesis)
Schemathesis は OpenAPI から プロパティベーステスト を自動生成し、稼働中の API を叩いてスキーマ違反を発見します。
schemathesis run --checks all https://api.example.com/v1/openapi.yaml
null safety、status code conformance、response schema conformance などを網羅的にチェックします。
5.2 gRPC + Protocol Buffers
gRPC は Google が公開した RPC フレームワーク。スキーマは .proto ファイルで、コード生成は protoc が担います。
syntax = "proto3";
package todo.v1;
service TodoService {
rpc ListTodos (ListTodosRequest) returns (ListTodosResponse);
rpc CreateTodo (CreateTodoRequest) returns (Todo);
rpc GetTodo (GetTodoRequest) returns (Todo);
rpc DeleteTodo (DeleteTodoRequest) returns (google.protobuf.Empty);
}
message Todo {
string id = 1;
string title = 2;
Status status = 3;
google.protobuf.Timestamp created_at = 4;
enum Status {
STATUS_UNSPECIFIED = 0;
STATUS_OPEN = 1;
STATUS_DONE = 2;
}
}
message ListTodosRequest { Todo.Status status_filter = 1; }
message ListTodosResponse { repeated Todo todos = 1; }
message CreateTodoRequest { string title = 1; }
message GetTodoRequest { string id = 1; }
message DeleteTodoRequest { string id = 1; }
buf ツールチェーン (buf lint, buf breaking, buf generate) は事実上の標準で、特に buf breaking は Pull Request で破壊的変更を自動検出するため CI に組み込みやすい設計です。
5.3 GraphQL SDL
GraphQL は問い合わせ言語兼スキーマ言語 (SDL: Schema Definition Language) を持ちます。
type Todo {
id: ID!
title: String!
status: TodoStatus!
createdAt: DateTime!
}
enum TodoStatus { OPEN DONE }
input NewTodoInput { title: String! }
type Query {
todos(status: TodoStatus): [Todo!]!
todo(id: ID!): Todo
}
type Mutation {
createTodo(input: NewTodoInput!): Todo!
deleteTodo(id: ID!): Boolean!
}
graphql-codegen で TypeScript の型・React Hooks・Resolver 雛形を一括生成できます。GraphQL の場合「スキーマ」がそのまま実行時のクエリ言語と接続するため、API ファーストとの相性は極めて良いです。
5.4 AsyncAPI
非同期メッセージング(Kafka, RabbitMQ, MQTT, WebSocket)向けの仕様。OpenAPI と相補的で、構文も似ています。
asyncapi: 3.0.0
info:
title: Order Events
version: 1.0.0
channels:
orderCreated:
address: orders.created
messages:
orderCreated:
$ref: '#/components/messages/OrderCreated'
operations:
publishOrderCreated:
action: send
channel:
$ref: '#/channels/orderCreated'
components:
messages:
OrderCreated:
payload:
type: object
required: [orderId, customerId, total]
properties:
orderId: { type: string, format: uuid }
customerId: { type: string, format: uuid }
total: { type: number, minimum: 0 }
5.5 契約テスト (Contract Testing): Pact
API ファーストの「契約は守られているか」を多サービス間で保証する仕組み。Consumer-Driven Contract(消費者駆動契約)が中核思想です。
5.5.1 Consumer 側
// consumer.pact.test.ts
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { eachLike, like } = MatchersV3;
const provider = new PactV3({ consumer: 'WebApp', provider: 'TodoApi' });
describe('Todo API contract', () => {
it('lists todos', async () => {
provider.given('two todos exist').uponReceiving('GET /todos').withRequest({
method: 'GET', path: '/v1/todos',
}).willRespondWith({
status: 200,
headers: { 'Content-Type': 'application/json' },
body: eachLike({
id: like('11111111-2222-3333-4444-555555555555'),
title: like('Buy milk'),
status: like('open'),
createdAt: like('2026-05-24T09:00:00Z'),
}),
});
await provider.executeTest(async (mock) => {
const res = await fetch(`${mock.url}/v1/todos`);
expect(res.status).toBe(200);
});
});
});
このテストは Pact ファイル(JSON)を生成し、Pact Broker に push されます。
5.5.2 Provider 側
// provider.pact.verify.ts
import { Verifier } from '@pact-foundation/pact';
await new Verifier({
providerBaseUrl: 'http://localhost:8080',
pactBrokerUrl: 'https://pact.example.com',
provider: 'TodoApi',
publishVerificationResult: true,
providerVersion: process.env.GIT_SHA,
}).verifyProvider();
Provider は Broker から最新の Consumer 期待を取得し、自分の実装が満たすかを検証します。これにより契約違反が起きた瞬間にどちら側が責任を負うか が明確になります。
5.6 API ファーストのワークフロー全体図
API ファーストは、「設計書」が「実行可能アセット」に変わる瞬間を体現するアプローチです。次章ではテストそのものを仕様にする BDD を見ます。
6. BDD と実行可能仕様
BDD (Behavior-Driven Development) は、TDD のテストを「ビジネス側にも読める形」に変換することから始まりました。Given/When/Then というシンプルな三段形式で、要求とテストとドキュメントが一枚の .feature ファイルに溶け合います。
6.1 Gherkin 構文の基本
Feature: User authentication
As a registered user
I want to log in with email and password
So that I can access my dashboard
Background:
Given the system is running
And the following users exist:
| email | password | role |
| alice@example.com | s3cret! | admin |
| bob@example.com | hunter2 | user |
Scenario: Successful login with valid credentials
Given I am on the login page
When I submit "alice@example.com" and "s3cret!"
Then I should see the admin dashboard
And the response should include a session cookie
Scenario Outline: Failed login due to bad credentials
Given I am on the login page
When I submit "<email>" and "<password>"
Then I should see error "<message>"
Examples:
| email | password | message |
| alice@example.com | wrong | Invalid credentials. |
| unknown@x.com | hunter2 | Invalid credentials. |
| not-an-email | hunter2 | Invalid email format.|
@security @JIRA-2451
Scenario: Account lockout after 5 failed attempts
Given I am on the login page
When I submit "alice@example.com" and "wrong" 5 times
Then I should see error "Account locked. Try again later."
And no further login attempts are accepted for 15 minutes
注目点。
- Feature: ひとつの機能。先頭の冒頭句("As a … I want to … So that …")はユーザストーリーの形式。
- Background: すべての Scenario の前に実行される共通前提。
- Scenario / Scenario Outline: 具体例。Outline は表でパラメタライズ可能。
- タグ (
@security,@JIRA-2451): 実行フィルタやトレーサビリティに使う。
6.2 Step Definitions
.feature の各ステップは、開発言語側のグルーコードでマッチングされます。Cucumber for JavaScript の例。
// features/step_definitions/auth.steps.ts
import { Given, When, Then, Before } from '@cucumber/cucumber';
import { expect } from 'chai';
import { World } from '../support/world';
Given('the system is running', async function (this: World) {
await this.app.boot();
});
Given('the following users exist:', async function (this: World, table) {
for (const row of table.hashes()) {
await this.app.users.create(row);
}
});
Given('I am on the login page', async function (this: World) {
await this.browser.visit('/login');
});
When(
'I submit {string} and {string}',
async function (this: World, email: string, password: string) {
await this.browser.fill('email', email);
await this.browser.fill('password', password);
await this.browser.click('submit');
}
);
Then('I should see the admin dashboard', async function (this: World) {
expect(await this.browser.title()).to.equal('Admin Dashboard');
});
Then('I should see error {string}', async function (this: World, message: string) {
const text = await this.browser.text('.error');
expect(text).to.equal(message);
});
World は scenario ごとに作られるコンテキストオブジェクトで、ブラウザ・DB・テスト用ヘルパなどを保持します。
6.3 Three Amigos ミーティング
BDD の真価は、シナリオを 誰が書くか にあります。Three Amigos ミーティングは三役が同席して仕様を共同執筆する場です。
| 役割 | 視点 |
|---|---|
| プロダクトオーナー / BA | 「何のためにこの機能が要るのか」「誰が使うのか」 |
| 開発者 | 「実装上、どこに穴があるか」「コーナーケースは何か」 |
| QA / テスター | 「どう壊せるか」「自動化可能か」 |
このミーティングで作られた .feature ファイルは、要求書・テスト・ドキュメント・受入基準 の四役を兼ねます。これが BDD の「Living Documentation(生きた文書)」の核心です。
6.4 SpecFlow(.NET 系)
C# 環境では SpecFlow(2024 年に Reqnroll へフォーク移行)が広く使われます。コアとなる .feature は同じ Gherkin で、Step Definition を C# で書きます。
[Binding]
public class AuthSteps
{
private readonly LoginPage _page;
public AuthSteps(LoginPage page) { _page = page; }
[When(@"I submit ""(.*)"" and ""(.*)""")]
public async Task WhenISubmit(string email, string password)
{
await _page.SubmitAsync(email, password);
}
[Then(@"I should see error ""(.*)""")]
public void ThenIShouldSeeError(string msg)
{
Assert.Equal(msg, _page.ErrorText);
}
}
6.5 Behave (Python)
# features/steps/auth.py
from behave import given, when, then
@when('I submit "{email}" and "{password}"')
def step_submit(context, email, password):
context.response = context.client.post(
'/login', data={'email': email, 'password': password}
)
@then('I should see error "{message}"')
def step_error(context, message):
assert message in context.response.text
6.6 Living Documentation の生成
Cucumber Reports、SpecFlow+ LivingDoc、Pickles などのツールは .feature ファイル群を読み物としての HTML に変換します。これにより、ステークホルダがソースリポジトリを見なくとも、最新の仕様=最新のテスト を確認できます。
npx @cucumber/cucumber --format html:reports/cucumber.html features/
これを GitHub Pages や S3 に毎ビルド更新すれば、仕様文書を別途メンテする手間が消滅 します。
6.7 BDD の落とし穴
- Gherkin が UI 操作の手順書になってしまう: 「ボタンをクリックする」「フィールドに入力する」など実装詳細が漏れたシナリオは保守困難。ビジネス語彙 に留めるのが鉄則。
- Step 重複:
When the user logs in as adminのような便利ステップを濫用すると、Step Definition のメンテが地獄化。Domain-Specific な少数の動詞に絞るべき。 - シナリオ爆発: ありとあらゆる組合せを書こうとすると CI が動かなくなる。重要な流れだけに絞り、細かいバリエーションは単体テストへ。
- Three Amigos が形骸化: 開発者だけが書いた
.featureは単なる二度手間になる。
6.8 BDD と他流派の関係
BDD は「API ファーストよりも上のレイヤ(ユーザ視点) で仕様を書く」と整理できます。実務では、API 仕様(OpenAPI)を契約レベルの仕様、Gherkin をユーザ受入レベルの仕様 として併用するのが王道です。AI 時代の SDD でも、spec.md の Acceptance Criteria に Gherkin 形式を採用することが多く、Spec Kit の公式テンプレートにも Given/When/Then が登場します。
7. AI 時代の Spec-Driven Development
2025 年以降、SDD という言葉を聞いたら多くの開発者がまず連想するのはここで述べる AI エージェント連携型のワークフロー でしょう。本章は本稿の主役として、最も詳しく扱います。
7.1 なぜいま「AI 時代の SDD」なのか
LLM の最初のブームでは、開発者は prompt → code の単発インタラクションを繰り返していました。これは小さなスニペットなら有効ですが、実プロジェクトのフィーチャー追加では破綻しがち です。理由は四つ。
- コンテキスト不足: LLM は Repo 全体を覚えていない。
- 要求の曖昧さ: 「いい感じに作って」では LLM は「いい感じ」を勝手に解釈する。
- レビュー困難: 数千行の差分を一気に出されるとヒューマンレビューが破綻する。
- 再現性がない: 同じ要求でも次の日には別の実装が出てくる。
これらを解決するために 「Spec → Plan → Tasks → Implement」 の四段階ワークフローが業界標準として急速に整備されました。GitHub Spec Kit、Amazon Kiro、Cursor の提案する「Specs」機能などが代表例です。
7.2 GitHub Spec Kit のワークフロー
Spec Kit は GitHub が 2025 年 9 月に公開した OSS。uvx か pip でインストールでき、specify init でプロジェクトに組み込みます。Claude Code、Copilot、Gemini CLI、Cursor、Windsurf などのエージェントから、共通の四つのスラッシュコマンドが使えるようになります。
uvx --from git+https://github.com/github/spec-kit specify init my-project --ai claude
cd my-project
これで .specify/ ディレクトリが生成され、テンプレート、メモリ(constitution)、エージェント別のコマンド定義が配置されます。
7.2.1 ステップ 0: Constitution
/constitution コマンドで、プロジェクト全体の不変原則を策定します。これは全フィーチャー横断のガイドライン で、エージェントが常時参照します。
# Constitution
## Principles
1. Every public function has tests.
2. No SQL in handlers; persistence goes through repositories.
3. All API responses use snake_case JSON.
4. Authentication via JWT (RS256).
5. Errors follow RFC 7807 (Problem Details).
## Tech stack (locked)
- Backend: Go 1.23, chi router
- DB: PostgreSQL 16, sqlc
- Tests: testify, httptest
- CI: GitHub Actions
## Forbidden
- ORMs (sqlc only)
- Logging via fmt.Println (must use slog)
- Panicking outside main()
7.2.2 ステップ 1: /specify
自然言語で「やりたいこと」を伝えると、エージェントが仕様書(spec.md) を生成します。
/specify Add a feature where users can mark a todo as "starred" to pin it
to the top of their list. Up to 5 stars per user.
生成される spec.md の例。
# Spec: Star a Todo
## Goal
Allow users to mark up to 5 todos as "starred" so that those todos appear
pinned at the top of their list views.
## Non-Goals
- Sharing starred lists across users.
- Sorting starred todos among themselves (FIFO is acceptable for v1).
- Notifications on star changes.
## Personas
- Authenticated end-user with at least one todo.
## User Scenarios
### Scenario 1: Star a todo (happy path)
**Given** I am authenticated and have 3 todos and 0 stars
**When** I PATCH /todos/{id} with body `{"starred": true}`
**Then** the response is 200 with `starred=true`
**And** subsequent GET /todos returns starred todos first
### Scenario 2: Hit the 5-star limit
**Given** I already have 5 starred todos
**When** I PATCH a 6th with `{"starred": true}`
**Then** I receive 409 Conflict with code `STAR_LIMIT_REACHED`
### Scenario 3: Unstar
**Given** I have a starred todo
**When** I PATCH with `{"starred": false}`
**Then** the response is 200 with `starred=false`
**And** that todo no longer appears at the top of the list.
## Functional Requirements
- FR-1 Each user has a max of 5 starred todos.
- FR-2 The Todo entity gains a `starred: boolean` field, default `false`.
- FR-3 GET /todos sort order: starred desc, created_at desc.
- FR-4 Starring is idempotent (double-starring returns 200, not 409).
## Non-Functional Requirements
- NFR-1 The PATCH endpoint p99 latency < 100 ms at 100 RPS.
- NFR-2 The star limit check must be transactional (no race window).
## Out of Scope
- UI changes (handled by web team in a separate spec).
## Open Questions
- [ ] Do we expose star count in user profile?
- [ ] Should starred state survive a soft-deleted todo restore?
ここで重要なのは、spec.md には「どう実装するか」を一切書かない ことです。What と Why に徹し、How は次のステップに譲ります。
7.2.3 ステップ 2: /plan
技術スタック・依存関係・データモデル変更などを含む plan.md を生成します。
/plan Use Go + sqlc + PostgreSQL. Add a `starred_at` timestamp instead of a
boolean so that we have natural FIFO ordering. Enforce the limit with a
SELECT ... FOR UPDATE inside the same transaction as the UPDATE.
# Plan: Star a Todo
## Architecture decisions
- Replace `starred: bool` with `starred_at: timestamp | null` for natural
FIFO ordering and to support future analytics. (FR-2 amended.)
- Star limit enforced via:
BEGIN
SELECT COUNT(*) FROM todos
WHERE user_id = $1 AND starred_at IS NOT NULL
FOR UPDATE
...
COMMIT
- All sort logic moves to SQL: `ORDER BY starred_at DESC NULLS LAST, created_at DESC`.
## Schema migrations
- 2026_05_24_add_starred_at.sql:
ALTER TABLE todos
ADD COLUMN starred_at TIMESTAMPTZ NULL;
CREATE INDEX idx_todos_user_starred
ON todos(user_id, starred_at DESC NULLS LAST, created_at DESC);
## API changes (OpenAPI delta)
- PATCH /todos/{id}: new operation; body `{"starred": boolean}`.
- GET /todos: response item gains `starred_at: string | null`.
## Files touched (estimated)
- internal/store/queries.sql (+2 queries)
- internal/handler/todo_patch.go (NEW)
- internal/handler/todo_list.go (sort change)
- api/openapi.yaml (delta)
- migrations/2026_05_24_add_starred_at.sql (NEW)
- features/star_todo.feature (NEW)
## Risks
- Race against concurrent star: addressed by FOR UPDATE.
- Index bloat from sparse `starred_at`: acceptable; partial index is overkill at our scale.
7.2.4 ステップ 3: /tasks
実装可能な 粒度の細かいタスクリスト に分解します。
# Tasks: Star a Todo
## T1 Schema migration
- Write migration `2026_05_24_add_starred_at.sql`.
- Run `make migrate-up` locally.
- Run `make migrate-down` to verify reversibility.
Acceptance: `psql \d todos` shows new column and index.
## T2 sqlc queries
- Add `MarkTodoStarred`, `UnmarkTodoStarred`, `CountUserStars` in queries.sql.
- Run `sqlc generate`.
Acceptance: generated Go signatures compile and accept `pgx.Tx`.
## T3 Handler: PATCH /todos/{id}
- New file `internal/handler/todo_patch.go`.
- Use `pgx.Tx` to wrap the count + update.
- Return 409 with code `STAR_LIMIT_REACHED` per spec FR-1.
Acceptance: unit test against in-memory store passes.
## T4 Update GET /todos sort order
- Modify `ListTodos` query to order by `starred_at DESC NULLS LAST, created_at DESC`.
Acceptance: integration test fixture covers a starred + unstarred mix.
## T5 OpenAPI delta
- Add PATCH operation, response field `starred_at`.
- Run `oasdiff` against `main` to confirm only **additive** changes.
Acceptance: `oasdiff breaking` exits 0.
## T6 BDD scenarios
- Add `features/star_todo.feature` matching spec scenarios 1–3.
- Run `cucumber` against the running test server.
Acceptance: 3/3 scenarios pass.
## T7 Load test for NFR-1
- k6 script hitting PATCH /todos at 100 RPS for 60s.
Acceptance: p99 < 100 ms.
7.2.5 ステップ 4: /implement
エージェントが上記タスクを 一つずつ こなします。Claude Code であればサブエージェントに分担させたり、各タスクの完了ごとに人間がレビューしてマージしたり、と運用は柔軟です。
7.3 Amazon Kiro
Kiro は AWS 寄りの IDE/ワークフローで、三本柱 を採用します。
| ファイル | 役割 |
|---|---|
requirements.md | EARS(Easy Approach to Requirements Syntax)形式で要求を列挙 |
design.md | アーキテクチャ・データモデル・コンポーネント図 |
tasks.md | 実装タスクのチェックリスト |
EARS は「WHEN <trigger>, THE SYSTEM SHALL <response>」という五つのテンプレートで要求を構造化する記法で、航空宇宙業界由来です。
## Requirement R-12 (Star limit)
Trigger: WHEN a user attempts to star a todo
Precondition: AND that user already has 5 starred todos
Response: THE SYSTEM SHALL reject the request with HTTP 409 and error code STAR_LIMIT_REACHED
Kiro 独自の特徴は次のとおり。
.kiro/steering/ディレクトリに常時参照されるルール群(Spec Kit の constitution に相当)。- 各フェーズ間に必ず人間レビューゲート。Kiro は次フェーズに自動進行しない。
- AWS リソース(Lambda、DynamoDB、API Gateway 等)と統合された雛形生成。
7.4 Cursor / Claude Code との統合
エージェント側にも仕様駆動の仕掛けが揃っています。
7.4.1 Cursor Rules
.cursor/rules/api-style.mdc
---
description: Always include OpenAPI updates with handler changes.
globs: ["api/**", "internal/handler/**"]
alwaysApply: false
---
- When adding or modifying an HTTP handler, also update api/openapi.yaml.
- Run `oasdiff breaking` before suggesting a commit.
- Use snake_case in JSON bodies.
7.4.2 Claude Code の CLAUDE.md とサブエージェント
リポジトリ直下の CLAUDE.md がプロジェクト全体の常時コンテキスト。.claude/agents/ 配下にサブエージェントを置くと、特定タスクで自動委譲できます。
# CLAUDE.md
This project follows Spec Kit. Before any feature work:
1. Confirm `spec.md` exists; if not, run `/specify`.
2. Confirm `plan.md` exists; if not, run `/plan`.
3. Pick a single task from `tasks.md` and implement it.
Never modify two top-level packages in one PR.
Code style: gofmt + golangci-lint with project config.
# .claude/agents/openapi-curator.yaml
name: openapi-curator
description: Updates api/openapi.yaml whenever a handler is changed.
tools: Read, Edit, Bash(oasdiff)
prompt: |
You are the OpenAPI curator. Whenever the user changes a Go handler,
reflect the corresponding change in api/openapi.yaml. Run
`oasdiff breaking api/openapi.yaml main:api/openapi.yaml` and report
any breaking changes. Never make code changes outside api/.
7.5 Constitution / Steering / Memory
これら三つは呼び名は違えど同じ役割 ―― 「全タスク横断で守るべき方針をエージェントの長期記憶に固定する」 ことです。技術的には、エージェントが各セッション開始時に強制的に読み込むファイル群、または埋め込みベクトル化されたメモリです。
良い constitution の条件:
- 明示的な禁止事項("forbidden" セクション)。
- 採用しない選択肢の理由 も書く("we avoid X because Y")。
- 100 行以内に収める(長すぎるとエージェントが端折る)。
7.6 反復精錬 (Iterative Refinement)
AI 時代の SDD は一発で完璧な spec を書く必要はありません。次の小さなループを回すのが現実的です。
「実装中に発覚した曖昧さは spec.md に戻る」という規律を守ることで、仕様だけが Single Source of Truth として 正しく進化します。
7.7 アーキテクチャ図: AI 時代 SDD の全体像
7.8 まだ未解決の課題
AI 時代 SDD は急速に成熟していますが、2026 年時点で次の課題が残ります。
- 大規模リポジトリでのスケール: spec/plan/tasks 自体が膨れたとき、エージェントのコンテキストウィンドウを超える。
- マルチエージェント協調: 複数エージェントが同じ spec を異なる解釈で実装するとマージ衝突が発生。
- 仕様の腐敗:
tasks.mdだけ更新してspec.mdを取り残すアンチパターン。 - AI ハルシネーション: 立派に見える spec が事実誤認を含む場合がある。レビューゲートで人間が読む規律が不可欠。
これらは第 12 章のアンチパターンでも扱います。
8. アーキテクチャと統合
SDD は「方法論」レベルの話なので、より具体的な設計手法 ―― DDD、TDD、マイクロサービス、CI/CD ―― と矛盾せず、むしろ 重ね合わせて強化 されるべきものです。本章では SDD と他手法の同居の作法を整理します。
8.1 SDD と DDD
DDD(Domain-Driven Design)は ドメインモデル を中心に据え、ユビキタス言語、境界づけられたコンテキスト、集約、リポジトリといった概念を提供します。SDD と DDD はレイヤーが違うため自然に重なります。
| レイヤ | DDD の貢献 | SDD の貢献 |
|---|---|---|
| 業務語彙 | Ubiquitous Language の確立 | Gherkin/spec.md にその語彙を反映 |
| サービス境界 | Bounded Context | OpenAPI/AsyncAPI でコンテキスト間契約を固定 |
| 集約 | Aggregate Root | DbC で不変条件を表現 |
| ロジック | Specification パターン | (DDD 由来の名前とぶつかるが)AI 時代 SDD で機能仕様 |
実務指針: ドメインモデルは DDD で、機能仕様は SDD で。両者は補完関係にあります。
8.2 SDD と TDD
TDD は「テスト先行」、SDD は「仕様先行」。一見似ていますが、抽象度の階段 が違います。
Spec (intent / behaviour) --→ Test (executable check) --→ Code (implementation)
↑ SDD scope ↑ TDD scope
実務的には、
- AI 時代 SDD では
/tasksがしばしば「テストを先に書け」と命じる。これは spec から派生した acceptance test。 - BDD が SDD と TDD のブリッジ。
.featureは SDD レベルの仕様だが、Step Definition は TDD レベルの細部に切り込む。 - OpenAPI + Schemathesis は仕様から自動でプロパティテストを生成 ―― これも事実上の TDD。
8.3 SDD とマイクロサービス
マイクロサービスは契約管理が命です。SDD は次の局面で必須インフラとなります。
- サービス間 API: OpenAPI / Protobuf を Single Source of Truth として、両側で型安全に。
- イベント: AsyncAPI で発火・購読の契約を明文化。
- 互換性ゲート: PR で
oasdiff/buf breakingが破壊的変更をブロック。 - 契約テスト: Pact が実装ドリフトを検出。
逆にいえば、マイクロサービスを導入したら SDD は事実上必須。さもなくば「分散モノリス」が誕生します。
8.4 SDD と CI/CD
CI/CD は SDD の自動執行装置です。後の第 13 章で詳細に扱いますが、概要としては:
- Lint: spec の文法・命名規則を検査。
- Diff: 互換性破壊を検出。
- Generate: コード・SDK・モックを自動生成。
- Verify: 仕様と実装の差分を実行時に検証。
- Publish: ドキュメントサイト・SDK パッケージを自動デプロイ。
8.5 SDD とフロントエンド
フロントエンドにも SDD は浸透しています。
- OpenAPI から TypeScript 型 + React Query Hook を自動生成 (
@hey-api/openapi-ts,orval)。 - GraphQL Codegen で
useGetTodosQuery等が降ってくる。 - MSW (Mock Service Worker) が OpenAPI モックをブラウザ内で再現。
- Storybook + spec ベースのモック でフロントを単独で開発可能に。
フロントエンドエンジニアにとって SDD の最大の価値は、バックエンドが完成する前から型安全に開発を進められる ことです。
8.6 SDD とインフラ (IaC)
- Terraform モジュールに JSON Schema を入力契約として持たせる。
- OpenAPI 拡張で AWS API Gateway や Azure API Management と直接統合。
- Crossplane / Pulumi はリソース定義そのものが「契約」になる。
「インフラの仕様駆動」は OpenAPI ほど統一されていませんが、各クラウドベンダが急速に標準化を進めています。
8.7 統合の実例: 一枚絵
この図は「仕様レイヤー」「派生レイヤー」「実行レイヤー」の三段構成を示します。SDD の本質はこの 派生レイヤーが手動メンテから解放される ことです。
9. ツール一覧
SDD の周辺には大量のツールが存在します。本章ではカテゴリ別に主要なものを整理します。
9.1 AI 時代 SDD ワークフロー
| ツール | 提供元 | 特徴 |
|---|---|---|
| GitHub Spec Kit | GitHub | OSS、/specify//plan//tasks//implement、複数エージェント対応 |
| Amazon Kiro | AWS | EARS 形式の requirements、.kiro/steering/、人間レビューゲート |
| Cursor Specs | Cursor | Cursor IDE 統合、ルールファイルとの相性◎ |
| Aider | OSS | CLI ベース、convention ファイルで仕様管理 |
| Claude Code subagents | Anthropic | .claude/agents/、CLAUDE.md、スラッシュコマンド |
| Cline (旧 Claude Dev) | OSS | VS Code 拡張、Plan/Act モードで仕様駆動を実装 |
9.2 API 仕様
| ツール | 用途 |
|---|---|
| OpenAPI 3.1 | REST API 仕様 |
| AsyncAPI 3.0 | 非同期メッセージング仕様 |
| gRPC + Protocol Buffers | RPC スキーマ |
| GraphQL SDL | GraphQL スキーマ |
| JSON Schema 2020-12 | データ構造のバリデーション |
| TypeSpec (旧 Cadl, Microsoft) | 高水準 DSL からマルチターゲット出力 |
| Smithy (AWS) | サービス定義 DSL、AWS SDK 生成基盤 |
9.3 OpenAPI エコシステム
| ツール | 機能 |
|---|---|
| Stoplight Studio | GUI エディタ |
| Swagger UI | API 試行用 UI |
| Redoc | 静的ドキュメントレンダラ |
| Spectral | OpenAPI Lint |
| oasdiff | 破壊的変更検出 |
| openapi-generator | 多言語コード生成 |
| oapi-codegen | Go 専用ジェネレータ |
| Prism | モックサーバ |
| Schemathesis | プロパティベーステスト |
| Dredd | 仕様準拠テスト |
9.4 契約テスト
- Pact ―― Consumer-Driven Contract のデファクト
- Spring Cloud Contract ―― Spring 生態系向け
- Karate ―― API テスト + 契約テストを統合した DSL
9.5 BDD フレームワーク
| 言語 | 主要フレームワーク |
|---|---|
| Java/JVM | Cucumber-JVM, JBehave |
| .NET | SpecFlow / Reqnroll |
| Python | Behave, pytest-bdd |
| Ruby | Cucumber |
| JavaScript/TypeScript | Cucumber-JS, Playwright + cucumber |
| Go | Godog |
| Rust | cucumber-rs |
9.6 形式手法
- TLA+ + TLC モデルチェッカ + Apalache(型付き拡張)
- Alloy + Alloy Analyzer
- B-Method / Event-B + Atelier B / Rodin
- Coq, Isabelle/HOL, Lean 4 ―― 定理証明系(深い形式仕様)
- Frama-C ―― C 言語の契約検証
- Dafny ―― 検証付き言語(Microsoft Research)
9.7 Design by Contract / 軽量検証
- Eiffel ―― 元祖
- icontract (Python)
- contracts crate (Rust)
- Code Contracts (.NET, 非推奨)
- JML (Java Modeling Language)
- PyContracts
- TypeScript の
zod/io-ts/ArkType―― 入力契約として - Pydantic ―― Python 側の同等品
9.8 ドキュメント / 公開
- Backstage ―― API カタログ
- SwaggerHub / Stoplight Platform ―― 商用ホスティング
- ReadMe.com ―― API ドキュメンテーションサービス
- Bump.sh ―― OpenAPI 専用ドキュメントポータル
9.9 リポジトリ規約 / エージェント設定
.cursorrules/.cursor/rules/*.mdc―― Cursor 用CLAUDE.md/.claude/―― Claude Code 用.aider.conf.yml―― Aider 用AGENTS.md―― 業界横断的に提案されている共通フォーマット(2025〜)
9.10 学習リソース
- Specification by Example (Gojko Adzic, 2011) ―― BDD の必読書
- The TLA+ Hyperbook (Lamport)
- Software Abstractions (Daniel Jackson, Alloy)
- GitHub Spec Kit リポジトリの README とサンプル
- Amazon Kiro の Getting Started ガイド
- Building Evolutionary Architectures (Neal Ford et al.) ―― 進化的アーキテクチャと SDD 親和性
このカタログは網羅的ではなく、2026 年時点で実務的に使われているものに絞っています。新興ツールは半年単位で増えていくため、最終的には自プロジェクトの要件に応じて選定するのが鉄則です。
10. 具体例: API ファーストの本格ミニプロジェクト
本章では実際に TODO API を OpenAPI から実装まで通しで作る流れを示します。第 5 章のスニペットを土台に、サーバ・クライアント・モック・テスト・CI までを統合します。
10.1 ディレクトリ構成
todo-api/
├── api/
│ └── openapi.yaml # 単一の真実源
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/ # 自動生成スタブを満たす実装
│ │ ├── todo.go
│ │ └── todo_test.go
│ └── store/ # sqlc 生成 + 実装
│ ├── queries.sql
│ └── queries.sql.go
├── gen/
│ ├── server.gen.go # oapi-codegen
│ └── types.gen.go
├── features/
│ └── todo.feature # BDD 受入テスト
├── pacts/ # Consumer 期待
├── migrations/
│ └── 0001_init.sql
├── .github/workflows/
│ └── ci.yml
├── Makefile
└── go.mod
ポイントは api/openapi.yaml だけが手書き、他は派生 という構造です。
10.2 Makefile
.PHONY: gen lint diff mock test bdd ci
gen:
\toapi-codegen -package gen -generate types,chi-server api/openapi.yaml > gen/server.gen.go
\tsqlc generate
lint:
\tspectral lint api/openapi.yaml
diff:
\toasdiff breaking origin/main:api/openapi.yaml api/openapi.yaml
mock:
\tprism mock api/openapi.yaml -p 4010
test:
\tgo test ./...
bdd:
\tgo run github.com/cucumber/godog/cmd/godog@latest features/
ci: lint diff gen test bdd
make ci ひとつで lint → 互換性検査 → 生成 → 単体 → BDD まで通ります。
10.3 cmd/server/main.go
package main
import (
"context"
"log/slog"
"net/http"
"os"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/example/todo-api/gen"
"github.com/example/todo-api/internal/handler"
"github.com/example/todo-api/internal/store"
)
func main() {
ctx := context.Background()
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
if err != nil { panic(err) }
defer pool.Close()
s := store.New(pool)
h := handler.New(s, slog.Default())
r := chi.NewRouter()
gen.HandlerFromMux(h, r)
slog.Info("listening", "addr", ":8080")
if err := http.ListenAndServe(":8080", r); err != nil {
panic(err)
}
}
gen.HandlerFromMux は OpenAPI から自動生成されたバインダで、ハンドラは gen.ServerInterface を満たす必要があります。
10.4 internal/handler/todo.go
package handler
import (
"encoding/json"
"log/slog"
"net/http"
"github.com/example/todo-api/gen"
"github.com/example/todo-api/internal/store"
"github.com/google/uuid"
)
type Todo struct {
s *store.Store
log *slog.Logger
}
func New(s *store.Store, log *slog.Logger) *Todo { return &Todo{s, log} }
// ListTodos GET /todos
func (h *Todo) ListTodos(w http.ResponseWriter, r *http.Request, params gen.ListTodosParams) {
todos, err := h.s.List(r.Context(), params.Status)
if err != nil { writeErr(w, http.StatusInternalServerError, "INTERNAL", err.Error()); return }
writeJSON(w, http.StatusOK, todos)
}
// CreateTodo POST /todos
func (h *Todo) CreateTodo(w http.ResponseWriter, r *http.Request) {
var body gen.NewTodo
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeErr(w, http.StatusBadRequest, "BAD_JSON", err.Error()); return
}
if body.Title == "" {
writeErr(w, http.StatusBadRequest, "TITLE_REQUIRED", "title is required"); return
}
todo, err := h.s.Create(r.Context(), uuid.NewString(), body.Title)
if err != nil { writeErr(w, http.StatusInternalServerError, "INTERNAL", err.Error()); return }
writeJSON(w, http.StatusCreated, todo)
}
// GetTodo GET /todos/{id}
func (h *Todo) GetTodo(w http.ResponseWriter, r *http.Request, id string) {
todo, err := h.s.Get(r.Context(), id)
if err == store.ErrNotFound {
writeErr(w, http.StatusNotFound, "NOT_FOUND", "todo not found"); return
}
if err != nil { writeErr(w, http.StatusInternalServerError, "INTERNAL", err.Error()); return }
writeJSON(w, http.StatusOK, todo)
}
// DeleteTodo DELETE /todos/{id}
func (h *Todo) DeleteTodo(w http.ResponseWriter, r *http.Request, id string) {
if err := h.s.Delete(r.Context(), id); err == store.ErrNotFound {
writeErr(w, http.StatusNotFound, "NOT_FOUND", "todo not found"); return
} else if err != nil {
writeErr(w, http.StatusInternalServerError, "INTERNAL", err.Error()); return
}
w.WriteHeader(http.StatusNoContent)
}
writeJSON writeErr は割愛しますが、レスポンス形式を Error スキーマと整合させるための薄い helper です。
10.5 BDD: features/todo.feature
Feature: Todo CRUD
Background:
Given the API is running
Scenario: Create and retrieve a todo
When I POST /v1/todos with body
"""
{"title": "Buy milk"}
"""
Then the response status is 201
And the response body has field "id"
And the response body field "title" equals "Buy milk"
Scenario: Reject empty title
When I POST /v1/todos with body
"""
{"title": ""}
"""
Then the response status is 400
And the response body field "code" equals "TITLE_REQUIRED"
Scenario: 404 for missing todo
When I GET /v1/todos/00000000-0000-0000-0000-000000000000
Then the response status is 404
10.6 Pact Consumer テスト(Web 側)
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
const { eachLike, like, uuid, iso8601DateTime } = MatchersV3;
const provider = new PactV3({ consumer: 'TodoWeb', provider: 'TodoApi' });
it('lists todos', async () => {
provider
.given('two open todos exist')
.uponReceiving('GET /todos?status=open')
.withRequest({ method: 'GET', path: '/v1/todos', query: { status: 'open' } })
.willRespondWith({
status: 200,
body: eachLike({
id: uuid('11111111-2222-3333-4444-555555555555'),
title: like('Buy milk'),
status: like('open'),
createdAt: iso8601DateTime('2026-05-24T09:00:00Z'),
}),
});
await provider.executeTest(async (mock) => {
const r = await fetch(`${mock.url}/v1/todos?status=open`);
expect(r.status).toBe(200);
const body = await r.json();
expect(body[0].title).toBe('Buy milk');
});
});
10.7 GitHub Actions CI
name: ci
on: [push, pull_request]
jobs:
spec:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- uses: stoplightio/spectral-action@v0.8.13
with: { file_glob: 'api/openapi.yaml' }
- name: Detect breaking changes
run: |
docker run --rm -v $PWD:/work tufin/oasdiff breaking \
/work/api/openapi.yaml@${{ github.event.pull_request.base.sha }} \
/work/api/openapi.yaml
build:
needs: spec
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.23' }
- run: make gen
- run: git diff --exit-code # generated files must be committed
- run: go test ./...
bdd:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d
- run: make bdd
pact-verify:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker compose up -d
- name: Verify Pact
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
run: ./scripts/pact-verify.sh
ここまでで、
- 仕様変更は PR で互換性自動検証
- ハンドラ実装は
gen.ServerInterfaceを満たす義務 - 受入は BDD と Pact の両輪で確認
- ドキュメントは Redoc が自動更新
という、仕様だけが手書き なフルスタック SDD パイプラインが完成します。
11. 具体例: Spec Kit でフィーチャーを追加する
第 7 章で示した Spec Kit のワークフローを、実際の小規模フィーチャーで通しでなぞります。題材は 「TODO をスター付きにできる」 機能。第 7 章の spec.md plan.md tasks.md を再利用します。
11.1 セットアップ
第 10 章のリポジトリにすでに OpenAPI ベースの実装があるところに、Spec Kit を追加します。
cd todo-api
uvx --from git+https://github.com/github/spec-kit specify init . --ai claude --merge
--merge で既存リポジトリに .specify/ を上書きせず追加。Claude Code を起動すると /constitution /specify /plan /tasks /implement が使えるようになります。
11.2 Constitution の整備
まずプロジェクト固有の方針を /constitution で固定します。第 7 章の例をベースに、TODO API 固有のルールを追加します。
# Constitution (todo-api)
## Single source of truth
- api/openapi.yaml is canonical.
- Never hand-edit gen/*.gen.go or sqlc-generated files.
## Stack (locked)
- Go 1.23, chi router, pgx, sqlc, godog, testify.
- Postgres 16.
## Style
- snake_case JSON, camelCase Go fields with json tags.
- Errors RFC 7807-ish: `{ "code": "...", "message": "..." }`.
- Logging via slog (never fmt.Println).
## Testing
- Every handler change accompanied by a feature scenario.
- Pact contract files live under pacts/ and are pushed in CI.
## Forbidden
- ORMs.
- Direct SQL strings in handlers.
- Breaking OpenAPI changes without a major version bump.
11.3 /specify 実行
/specify Users want to mark a todo as starred. Pinned at the top of the
list. Maximum 5 stars per user. Idempotent. Need a separate timestamp so
later we can sort multiple starred todos in the order they were starred.
エージェントが第 7.2.2 節で示した spec.md を生成します。レビュアは次を確認します。
- Goal が一文で言えるか?
- Non-Goals に「やらないこと」が明記されているか?
- Acceptance Scenario が Gherkin 形式で書かれているか?
- Open Questions が空になっているか? (残っていればここで議論)
レビュー後 git add specs/star-a-todo/spec.md && git commit -m "spec: star a todo" で確定。
11.4 /plan 実行
/plan Use a starred_at TIMESTAMPTZ NULL on todos. Enforce limit with
SELECT COUNT FOR UPDATE inside a transaction. Update GET /todos sort
order. Update OpenAPI with PATCH /todos/{id}.
第 7.2.3 節の plan.md が降ってきます。ここでアーキテクトが次をチェックします。
- テックスタックは constitution と整合か?
- OpenAPI 変更は backward compatible か? ―― Yes(追加のみ、
oasdiffを通す前提) - Race condition の議論はあるか? ―― 「FOR UPDATE で対処」と明記されている
- マイグレーションの可逆性は? ――
DROP COLUMNで戻せる
11.5 /tasks 実行
第 7.2.4 節の tasks.md が生成されます。タスクを T1〜T7 まで分割し、各タスクに Acceptance が付いていることが鍵です。
11.6 /implement 実行 ―― T1: マイグレーション
エージェントは tasks.md の最初のタスクを取り上げます。
-- migrations/2026_05_24_add_starred_at.up.sql
ALTER TABLE todos
ADD COLUMN starred_at TIMESTAMPTZ NULL;
CREATE INDEX idx_todos_user_starred
ON todos(user_id, starred_at DESC NULLS LAST, created_at DESC);
-- migrations/2026_05_24_add_starred_at.down.sql
DROP INDEX IF EXISTS idx_todos_user_starred;
ALTER TABLE todos DROP COLUMN IF EXISTS starred_at;
make migrate-up && make migrate-down && make migrate-up で可逆性確認。Acceptance OK → 次のタスクへ。
11.7 T2: sqlc クエリ
-- queries.sql
-- name: CountUserStars :one
SELECT COUNT(*) FROM todos
WHERE user_id = $1 AND starred_at IS NOT NULL;
-- name: MarkTodoStarred :one
UPDATE todos
SET starred_at = NOW()
WHERE id = $1 AND user_id = $2 AND starred_at IS NULL
RETURNING *;
-- name: UnmarkTodoStarred :one
UPDATE todos
SET starred_at = NULL
WHERE id = $1 AND user_id = $2 AND starred_at IS NOT NULL
RETURNING *;
sqlc generate で型安全な Go メソッドが降ってきます。
11.8 T3: PATCH ハンドラ
// internal/handler/todo_patch.go
package handler
import (
"encoding/json"
"errors"
"net/http"
"github.com/jackc/pgx/v5"
"github.com/example/todo-api/internal/store"
)
type patchBody struct {
Starred *bool `json:"starred"`
}
const maxStars = 5
func (h *Todo) PatchTodo(w http.ResponseWriter, r *http.Request, id string) {
userID := r.Header.Get("X-User-Id") // simplified
var body patchBody
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeErr(w, http.StatusBadRequest, "BAD_JSON", err.Error()); return
}
if body.Starred == nil {
writeErr(w, http.StatusBadRequest, "NO_OP", "starred field required"); return
}
err := h.s.WithTx(r.Context(), func(tx pgx.Tx) error {
if *body.Starred {
n, err := h.s.CountUserStarsTx(r.Context(), tx, userID)
if err != nil { return err }
if n >= maxStars {
return errStarLimit
}
_, err = h.s.MarkTodoStarredTx(r.Context(), tx, id, userID)
if errors.Is(err, store.ErrNoRows) { return nil } // idempotent
return err
}
_, err := h.s.UnmarkTodoStarredTx(r.Context(), tx, id, userID)
if errors.Is(err, store.ErrNoRows) { return nil }
return err
})
if errors.Is(err, errStarLimit) {
writeErr(w, http.StatusConflict, "STAR_LIMIT_REACHED", "max 5 stars per user"); return
}
if err != nil { writeErr(w, http.StatusInternalServerError, "INTERNAL", err.Error()); return }
h.GetTodo(w, r, id) // return current shape
}
var errStarLimit = errors.New("star limit reached")
人間レビューでは idempotency が errors.Is(err, store.ErrNoRows) で実現されている こと、limit チェックがトランザクション内 であることを確認します。
11.9 T5: OpenAPI 差分
# api/openapi.yaml (PATCH を追加)
paths:
/todos/{id}:
patch:
operationId: patchTodo
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
starred: { type: boolean }
required: [starred]
responses:
'200':
description: Updated
content:
application/json:
schema: { $ref: '#/components/schemas/Todo' }
'409':
description: Star limit reached
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
components:
schemas:
Todo:
type: object
required: [id, title, status, createdAt]
properties:
id: { type: string, format: uuid }
title: { type: string, minLength: 1, maxLength: 200 }
status: { type: string, enum: [open, done] }
createdAt: { type: string, format: date-time }
starredAt:
type: [string, "null"]
format: date-time
oasdiff breaking を通すと「破壊的変更なし(追加のみ)」と判定され、CI ゲートを通過します。
11.10 T6: BDD シナリオ
Feature: Star a todo
Background:
Given the API is running
And user "alice" exists
Scenario: Star a todo
Given alice has 0 starred todos and 3 unstarred
When alice PATCHes a todo with body
"""
{"starred": true}
"""
Then the response status is 200
And the response body field "starredAt" is not null
And listing todos returns the starred one first
Scenario: Star limit
Given alice has 5 starred todos
When alice PATCHes another todo with body
"""
{"starred": true}
"""
Then the response status is 409
And the response body field "code" equals "STAR_LIMIT_REACHED"
Scenario: Idempotent star
Given alice has 1 starred todo X
When alice PATCHes X with body
"""
{"starred": true}
"""
Then the response status is 200
11.11 T7: 負荷試験 (k6)
// k6/star.js
import http from 'k6/http';
import { check } from 'k6';
export const options = {
scenarios: {
star: {
executor: 'constant-arrival-rate',
rate: 100, timeUnit: '1s',
duration: '60s',
preAllocatedVUs: 50,
},
},
thresholds: { 'http_req_duration{expected_response:true}': ['p(99)<100'] },
};
export default function () {
const id = `${__ENV.TODO_ID}`;
const r = http.patch(
`${__ENV.BASE_URL}/v1/todos/${id}`,
JSON.stringify({ starred: true }),
{ headers: { 'Content-Type': 'application/json', 'X-User-Id': 'alice' } }
);
check(r, { 'is 200 or 409': (x) => x.status === 200 || x.status === 409 });
}
k6 run k6/star.js を CI で走らせ、p99 < 100 ms を確認したら NFR-1 の Acceptance も通過。
11.12 リフレクションと spec への還流
実装中に「X-User-Id ヘッダ依存をやめて JWT クレームから取りたい」という気付きを得たとします。これは constitution に書かれていなかった 実装詳細です。次のいずれかを行います。
- constitution に追記 ―― 全フィーチャー横断で JWT を使うと決める。
- plan.md に追記 ―― 当該フィーチャーのみで対応。
決して コードだけを変更して spec/plan を放置しない。これが SDD の一丁目一番地。
11.13 まとめ
このミニプロジェクトで示したのは:
- Spec Kit の四段階が 小さなフィーチャー追加にも有効 であること
- レビュアにとって
spec.mdplan.mdを読むのが PR レビューの第一歩 になること - 各タスクに 明示的な Acceptance があるおかげで進捗が客観的に確認できること
- 仕様駆動と既存 OpenAPI/BDD/Pact エコシステムは 重ね合わせて運用できる こと
12. アンチパターン
SDD は強力ですが、運用を誤ると 「仕様が手間を増やすだけのドキュメント」 に堕します。本章では実務でよく観測される失敗パターンを、原因と治療法とともに整理します。
12.1 Spec Rot(仕様の腐敗)
症状: コードは更新されているのに、spec.md や OpenAPI が古い状態で放置されている。
原因: 「仕様を直すのが面倒」「PR レビュアが仕様の整合性を見ない」「CI ゲートが甘い」。
治療:
- CI で
oasdiffbuf breakingpact-brokerを 必須ジョブ にする。 - PR テンプレートに
[ ] spec.md updatedのチェックボックスを入れる。 - 受入テストが Gherkin 形式の場合、シナリオの diff を必須にする。
12.2 Over-Specification(過剰仕様化)
症状: 一週間かけて 80 ページの spec を書き、実装に入れない。
原因: 「仕様を完璧にしてから実装すべき」という誤った理想化。
治療:
- spec はまず一画面 (1 ページ) に収める。
- 詳細は plan / tasks に降ろす。
- 反復精錬を前提にする。「仕様は捨てる前提で書け、ただしバージョン管理せよ」。
12.3 Spec Without Execution(実行されない仕様)
症状: Word/Confluence に仕様を書いて満足し、実装と乖離していく。
原因: 機械可読性を欠いた仕様は 誰にも検証されない から。
治療:
- すべての spec は Git に置く。
- 仕様の一部に Gherkin・OpenAPI・JSON Schema など機械可読パートを必ず含める。
- 仕様文書は テストランナーが読む対象 にする。
12.4 AI Hallucinated Specs(AI 幻覚仕様)
症状: エージェントが書いた spec が、存在しないライブラリ・誤った API・ありえない要件を含んでいる。
原因: LLM の生成性質。一見もっともらしい嘘をつく。
治療:
- 必ず人間レビューゲートを設ける。
- spec の参照(ライブラリ名・社内サービス名・API 名)は 検証可能なリンク にする(Backstage、内部 Wiki、依存リポジトリへの link)。
- 知識ベースを RAG として与える。
12.5 Premature Locking(早すぎる契約固定)
症状: まだ実験的なフィーチャーに対し、外部公開 OpenAPI を細かく書き、後から大幅変更が必要になる。
原因: 「API ファースト = 最初から全部決める」という誤解。
治療:
- 実験段階では
/internal/v0などの安定保証なし スコープを設ける。 - 安定 API は
oasdiff breakingでガード。 - α 版の仕様は
x-experimental: trueなどの拡張で明示。
12.6 Generated Code Hand-Edits(生成コード手修正)
症状: openapi-generator の出力をその場で書き換え、次回の再生成で消える/コンフリクトする。
原因: 生成器の制約への我慢が切れる。
治療:
- 生成コードは原則変更禁止。差分が必要なら テンプレートをカスタマイズ する。
// Code generated; DO NOT EDIT.ヘッダがあるファイルを CI でgit diff --exit-codeする。
12.7 Spec Theater(仕様演劇)
症状: 立派な spec が書かれているが、実装は spec を読まずに作られている。
原因: spec が組織の「儀礼」になり、実務とは別動線になっている。
治療:
- spec を入力にしてコード生成・テスト生成を行い、見ない選択肢を物理的に消す。
- レビューチェックリストの第一項目を「spec とコードが一致するか」にする。
- spec から生成された SDK を使ってテストを書く(自分の足を撃てなくなる)。
12.8 Big-Bang Spec Update(一括仕様更新)
症状: 数週間後に巨大な PR で OpenAPI が大改訂される。レビュアが追えない。
原因: 仕様変更を後回しにしてまとめて行う。
治療:
- 仕様変更は 小さく頻繁に PR に分ける。
oasdiffの出力を PR コメントに自動投稿し、変更点を可視化。- Spec Kit の
/specifyを1フィーチャー1ファイルで運用する。
12.9 Constitution Bloat(憲法肥大)
症状: CLAUDE.md や constitution が 500 行を超え、エージェントが半分しか読まない。
原因: 全部書こうとする欲。
治療:
- 100 行以内を目標に。
- 詳細は別ファイル
docs/style-guide.md等に切り出し、必要時のみ参照させる。 - ルールごとに「なぜ」を一行添える。理由のないルールは破られやすい。
12.10 トレーサビリティの欠如
症状: 障害発生時に「どの要件が原因か」が辿れない。
原因: spec → plan → tasks → commit → deploy のチェーンが切れている。
治療:
spec.mdには不変 ID を振る(FR-12等)。- commit message と PR description で要件 ID を引用。
- ログにも
requirement_idをフィールドとして含める(観測性とのリンク)。
12.11 「仕様=設計書」誤解
症状: spec.md にクラス図やシーケンス図を書き込んでしまう。
原因: spec と design の境界を誤解。
治療:
- spec = What/Why、plan = How(ハイレベル)、コード = How(詳細)と位置づけを明確化。
- 図解は plan.md に置く。
12.12 まとめ
これらアンチパターンの底流にあるのは「仕様は手段であって目的ではない」という認識の欠如です。仕様は 実装・テスト・運用を駆動するために あるのであり、そこから生まれない仕様は捨てるべきです。SDD を「ドキュメント主義の復活」ではなく「実行可能アセットの再定義」として運用することが、これらの落とし穴を避ける根本原則です。
13. CI/CD 連携
SDD は「仕様が動くこと」が要諦であり、その自動執行装置が CI/CD です。本章では仕様駆動の CI パイプラインに組み込むべきジョブ群を整理し、GitHub Actions と GitLab CI を例に実装スニペットを示します。
13.1 仕様 CI の七つの工程
各工程の意義と代表ツールは以下のとおり。
- Lint Spec: 命名規則・必須プロパティ・スタイル違反 (Spectral, vacuum, buf lint)
- Detect Breaking Changes:
oasdiff breaking、buf breaking - Generate Artifacts: SDK・サーバスタブ・モック (openapi-generator, oapi-codegen, protoc)
- Verify Generated Diff Clean: 「生成は手書きと一致するか?」 (
git diff --exit-code) - Run Unit Tests: 通常の単体テスト
- Run Spec-Conformance Tests: Schemathesis、Dredd、BDD、Pact
- Publish: Redoc、SDK npm/Go module、Pact Broker
13.2 GitHub Actions 完全例
name: spec-driven-ci
on:
pull_request:
push:
branches: [main]
jobs:
spec-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Spectral lint
uses: stoplightio/spectral-action@v0.8.13
with:
file_glob: 'api/openapi.yaml'
spectral_ruleset: '.spectral.yaml'
spec-breaking:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: oasdiff breaking changes
run: |
docker run --rm -v "$PWD:/work" tufin/oasdiff:latest breaking \
"/work/api/openapi.yaml@${{ github.event.pull_request.base.sha }}" \
"/work/api/openapi.yaml" \
--fail-on ERR
- name: PR comment
if: failure()
uses: marocchino/sticky-pull-request-comment@v2
with:
message: |
Breaking change detected in `api/openapi.yaml`.
Bump the major version or revert the change.
codegen-clean:
runs-on: ubuntu-latest
needs: spec-lint
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: '1.23' }
- run: make gen
- run: |
if ! git diff --exit-code; then
echo 'Generated code is out of date. Run `make gen` and commit.'
exit 1
fi
unit-tests:
runs-on: ubuntu-latest
needs: codegen-clean
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- run: go test ./...
schemathesis:
runs-on: ubuntu-latest
needs: unit-tests
services:
api: { image: ghcr.io/example/todo-api:pr-${{ github.event.pull_request.number }}, ports: ['8080:8080'] }
steps:
- uses: actions/checkout@v4
- run: pipx install schemathesis
- run: schemathesis run --checks all --base-url http://localhost:8080 api/openapi.yaml
pact-publish:
runs-on: ubuntu-latest
needs: unit-tests
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- run: |
npx @pact-foundation/pact-cli publish ./pacts \
--consumer-app-version "$GITHUB_SHA" \
--branch "${GITHUB_REF##*/}" \
--broker-base-url "$PACT_BROKER_URL" \
--broker-token "$PACT_BROKER_TOKEN"
env:
PACT_BROKER_URL: ${{ secrets.PACT_BROKER_URL }}
PACT_BROKER_TOKEN: ${{ secrets.PACT_BROKER_TOKEN }}
docs-publish:
runs-on: ubuntu-latest
needs: unit-tests
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: npx @redocly/cli build-docs api/openapi.yaml -o public/index.html
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./public
13.3 Spectral ルールセット例
# .spectral.yaml
extends: ['spectral:oas']
rules:
operation-operationId-unique: error
operation-operationId: error
operation-tag-defined: error
no-$ref-siblings: error
oas3-valid-media-example: error
custom-snake-case-property:
description: Properties must be snake_case
severity: error
given: '$..properties[*]~'
then:
function: pattern
functionOptions:
match: '^[a-z][a-z0-9_]*$'
13.4 buf 設定例 (gRPC/Protobuf)
# buf.yaml
version: v2
modules:
- path: proto
lint:
use: [DEFAULT]
except: [PACKAGE_VERSION_SUFFIX]
breaking:
use: [FILE]
.github/workflows/ci.yml 内では:
- uses: bufbuild/buf-setup-action@v1
- run: buf lint
- if: github.event_name == 'pull_request'
run: buf breaking --against ".git#branch=main"
13.5 Pact Provider Verification をリリース前ゲートに
release-gate:
runs-on: ubuntu-latest
steps:
- name: can-i-deploy
run: |
npx @pact-foundation/pact-cli can-i-deploy \
--pacticipant TodoApi \
--version "$GITHUB_SHA" \
--to-environment production \
--broker-base-url "$PACT_BROKER_URL"
can-i-deploy は、対象環境に存在する全 consumer の期待を「現在の provider 実装が満たすか」を Pact Broker に問い合わせ、不整合があればリリースをブロックします。
13.6 GitLab CI スニペット
spec_lint:
image: stoplight/spectral
script:
- spectral lint api/openapi.yaml
spec_breaking:
image: tufin/oasdiff
rules: [{ if: $CI_PIPELINE_SOURCE == "merge_request_event" }]
script:
- oasdiff breaking
api/openapi.yaml@$CI_MERGE_REQUEST_TARGET_BRANCH_SHA
api/openapi.yaml
--fail-on ERR
13.7 観測性との接続
CI が通っただけでは終わりません。本番でも仕様駆動を貫くなら、
- 構造化ログに
requirement_idを含める(slog.With("req_id", "FR-12"))。 - OpenAPI / AsyncAPI から OpenTelemetry の attribute schema を生成。
- メトリクスダッシュボードに「仕様カバレッジ」(spec scenario が本番でどれだけ実行されたか)。
13.8 まとめ
CI/CD 統合の真髄は、「人間が忘れたら直ちに失敗する」 ようにすること。仕様変更が本番リリースに自動的に伝播し、不整合が即座に検出される――この一点を満たせば、SDD は組織のメンタリティではなく、機械の構造として定着します。
14. メリットとデメリット
SDD はあらゆる場面で銀の弾丸ではありません。本章では実務観点でのメリットとデメリットを公平に列挙し、適用可否の判断材料を提供します。
14.1 メリット
14.1.1 開発速度の長期的な向上
短期的には仕様作成のオーバーヘッドが発生しますが、半年〜一年スパンで見ると、
- API ドキュメント手書きが消滅
- モック・SDK が常に最新
- テスト雛形が自動生成
- AI エージェントが迷子にならない
これらの累積効果で総開発時間が短縮されることが、複数の事例研究(GitHub の Spec Kit 事例、Stripe の API ファースト報告等)で示されています。
14.1.2 品質と信頼性
- 契約テストにより破壊的変更が PR で止まる
- 形式仕様(TLA+ 等)で設計時にバグが消える
- Living Documentation により仕様乖離が常時可視化
- AI 生成コードが scope 内に留まる
14.1.3 オンボーディングの高速化
新人がコードを読む前に spec を読むだけで全体像を把握可能。Gherkin のシナリオは技術背景の浅い QA・PM にも理解可能。
14.1.4 並行開発の解放
フロントとバックを 同じ仕様 に対して並行開発できる。モックサーバさえ立てれば UI 開発はバックを待たない。マイクロサービスチーム間の調整コストも下がる。
14.1.5 ベンダロックインの軽減
OpenAPI/Protobuf/GraphQL は標準仕様であり、特定言語・特定クラウドに縛られない。SDK 生成によりクライアント言語の追加が「コマンド一発」で可能。
14.1.6 AI との相性
これは 2025 年以降の最大のメリット。仕様駆動の組織は AI エージェントを安全に使える ―― 仕様という人間管理可能な「制約」を介して AI を統制できるため、生成コードの責任所在が明確。
14.2 デメリット
14.2.1 立ち上げコスト
- ツールチェーン構築 (CI、Lint、Generator、Mock) に数日〜数週
- チームへの教育(特に Gherkin、形式仕様)
- リポジトリ構造の合意形成
小さな PoC・短命なスクリプトには見合わない。
14.2.2 仕様作成の認知負荷
「コードで書けば一瞬」のものを「仕様 → 実装」と二段階にすることへの心理的抵抗。経験ある開発者ほど抵抗感が強い傾向。
14.2.3 仕様腐敗のリスク
第 12 章で触れたとおり、CI ゲートが不十分だと仕様だけが古くなり、二重メンテのコストだけが残る。
14.2.4 過剰仕様化への誘惑
特に契約系(OpenAPI)を最初から完璧に書こうとすると、実装フェーズで仕様変更が頻発し、結局後追いになる。
14.2.5 ツール・概念の学習曲線
- TLA+ や Alloy は数週間学ばないと書けない
- OpenAPI 3.1 の機能(
oneOf/anyOf/discriminator等)は奥深い - BDD のシナリオ作法(ビジネス語彙に留める等)は文化的訓練が要る
- AI 時代 SDD はツール乱立で標準化が追いついていない
14.2.6 一部ドメインでは不向き
- 超実験的プロトタイプ: 仕様より「いじって反応を見る」が重要
- ML パイプライン: 仕様の概念がデータ駆動と噛み合わない部分がある
- GUI ヘビーなフロント: ピクセル単位の挙動は仕様化しにくい
14.2.7 組織的抵抗
「仕様を書くのは BA / PM の仕事だった」「開発者は実装するもの」という旧来の役割分担が残っていると、SDD は受け入れられにくい。Three Amigos のような共同執筆文化への移行が前提。
14.3 適用可否の判断フローチャート
14.4 損益分岐点の目安
経験則として、SDD の累積コスト < ベネフィットになる条件は
- 3 名以上のチーム
- 6 ヶ月以上のライフサイクル
- API か CLI のような「他者に消費される境界」を持つプロダクト
これら三つを満たすなら積極導入、満たさないなら 形だけ取り入れる(OpenAPI の自動生成を 30 分で導入する程度) で十分です。
14.5 結論
SDD は「正しく適用された場合に限り 強力なツール」です。万能ではないが、特に分散システム、公開 API、AI 駆動開発、規制業界では事実上の必須技術になりつつあります。次章ではこれを他手法と比較し、棲み分けを明確化します。
15. 他手法との比較
SDD はしばしば DDD、TDD、vibe coding などと比較されます。本章ではそれぞれとの違い・棲み分けを明確にします。
15.1 SDD vs DDD
| 観点 | DDD | SDD |
|---|---|---|
| 主眼 | ドメインモデル設計 | 仕様の機械可読化 |
| 中心成果物 | ユビキタス言語、Bounded Context、Aggregate | spec.md / OpenAPI / .feature |
| 抽象度 | 業務概念 | 機能契約 |
| 主導役 | ドメインエキスパート | ステークホルダ全員 |
両者は 完全に補完関係。DDD で領域を切り、その境界を SDD で固定する、というのが定石です。
15.2 SDD vs TDD
| 観点 | TDD | SDD |
|---|---|---|
| 起点 | テスト | 仕様 |
| 粒度 | クラス・関数単位 | フィーチャー・サービス単位 |
| 順序 | Red → Green → Refactor | Spec → Plan → Tasks → Code |
| ステークホルダ | 開発者中心 | 開発者 + ビジネス |
SDD は TDD を 包含する 上位概念に近い。Acceptance Test を spec から派生させ、その内部でユニットテストを TDD で回す、という二段階運用が一般的。
15.3 SDD vs BDD
第 6 章で詳述したとおり、BDD は SDD の一形態です。
- BDD は「仕様 = 受入テスト」と狭く定義 したアプローチ。
- SDD はより広く、形式仕様、API 契約、AI ワークフローまで含む。
実務では「BDD は SDD の中の受入レイヤ」と理解すれば良いでしょう。
15.4 SDD vs Vibe Coding
| 観点 | Vibe Coding | SDD |
|---|---|---|
| 入力 | 雰囲気のプロンプト | 構造化された spec.md |
| 速度 | 初動は速い | 初動は遅い |
| 品質 | ばらつき大 | 安定 |
| レビュー | 困難 | spec を見れば把握可能 |
| 規模 | 小〜中 | 中〜大 |
| AI との関係 | AI に任せる | AI を仕様で統制 |
棲み分け: 短い実験的タスク(数行〜100 行)には vibe、それを超えるなら SDD。プロダクションコードは原則 SDD。
15.5 SDD vs Documentation-Driven Development (DDD の別名)
DDD は「書きながら設計する」軽量手法ですが、機械可読性・実行可能性が弱いため SDD のサブセットとは言いがたい。「人間に読める文書」と「機械が実行する仕様」は別物です。
15.6 SDD vs Model-Driven Architecture (MDA)
OMG が 2001 年に提唱した MDA は、UML から自動コード生成を目指した壮大な試み。多くは挫折し、現代に残るのは限定的(Eclipse EMF、Acceleo 等)。SDD は MDA の精神的後継者 ですが、UML ではなく軽量・実用的な仕様言語(OpenAPI、Gherkin、Markdown)を採用した点で異なります。
15.7 SDD vs Type-Driven Development
Haskell・OCaml・Rust 文化圏で語られる「型で仕様を表現する」手法。SDD と相補的で、
- 型は実装内部の不変条件を保証
- SDD は外部契約を保証
両者を併用するのが理想。Rust + OpenAPI、TypeScript + Zod + OpenAPI などが好例。
15.8 比較サマリ
このチャートの示す通り、SDD ファミリーは 「高い仕様厳格度」と「適度な実装自由度」 のスイートスポットを目指します。形式仕様は厳格すぎて実装自由度が低く、vibe coding は逆。SDD はその中間で、生産性と信頼性のバランス を取ろうとする立場です。
16. まとめ
本稿では Specification-Driven Development を、形式仕様、API ファースト、BDD、AI 時代 SDD、Specification パターン の五つの顔から多角的に概観しました。これらは単一の発明ではなく、半世紀のソフトウェア工学の歴史の中で各時代の必要に応じて発生した手法群が、2025〜2026 年の AI 駆動開発という収束点で 一つの規律 として再編成されつつある、という大きな流れです。
16.1 本稿の主要メッセージ
-
SDD は単一の方法論ではなく、共通原則をもつ「家族」である Single Source of Truth、Machine Readability、Spec Ahead of Code、Traceability、Executability、Evolvability ―― これらが全流派を貫く規律です。
-
AI 時代 SDD が新しい主役になった GitHub Spec Kit、Amazon Kiro が代表する
/specify → /plan → /tasks → /implementワークフローは、LLM の不安定さを「人間管理可能な仕様」で抑え込む実用解として急速に普及しました。 -
OpenAPI / BDD / Pact は AI 時代 SDD と統合可能 既存の API ファースト・BDD パイプラインの上に Spec Kit を重ねるのは自然で、2026 年現在の標準スタックと言えます。
-
CI/CD への組込みが SDD を「文化」から「機械」へ昇華させる Spec Lint、Breaking Change 検出、生成コードの差分検査、契約テスト、
can-i-deployゲートなどを CI に必須ジョブとして配置することで、SDD は属人化しなくなります。 -
適用範囲を見極めること 小規模・短命なプロジェクトには重い。3 名以上・6 ヶ月以上・外部境界を持つプロダクトでこそ威力を発揮します。
16.2 これから始めたい人への実践的な手順
- 既存リポジトリに OpenAPI を 1 ファイル追加(30 分で済むことが多い)。
oasdiff breakingを CI に組み込む(PR レビューが激変)。openapi-generatorかoapi-codegenで SDK 生成を Makefile 化。features/ディレクトリと小さな.featureファイルを 1 つ用意、Cucumber を CI に組込み。- GitHub Spec Kit を
specify init . --mergeで導入 し、新フィーチャーから順に/specifyを使ってみる。 - Constitution /
CLAUDE.mdを 50 行で書く。完璧を目指さない。 - 3 ヶ月後、計測:仕様腐敗率、PR レビュー時間、本番事故件数。改善が見えなければ運用を見直す。
16.3 これからの SDD
2026 年現在、AI 時代 SDD はまだ若く、以下の方向で急速に進化しています。
- マルチエージェント協調: spec/plan/tasks をエージェント間で受け渡しする標準プロトコル(Model Context Protocol 等)。
- 仕様のバージョニング標準: Spec Kit や Kiro が個別に持つフォーマットを共通仕様化する動き。
- 観測性との統合: spec scenario が本番でどれだけ実行されたかを可視化するダッシュボード。
- 形式仕様との架橋: Markdown spec から TLA+ や Alloy を自動生成する研究プロトタイプ。
- 規制対応 SDD: 医療・金融・自動運転で監査可能な「証跡付き」spec が要求される。
16.4 結びに
ソフトウェア開発の歴史は、「人間が書ける文書」と「機械が動かせる成果物」の距離を縮める旅 だったと言えます。SDD はその距離が AI という新しいランナー によってかつてないほど縮まった瞬間に、もう一度仕様を中心に据え直す ―― 半世紀越しの正統な揺り戻しです。
仕様は重荷ではなく、人間と機械の 共通言語 です。本稿が、その共通言語をプロジェクトに導入する具体的な道筋を見つける助けになれば幸いです。
Specs are the new code. Code is the new artifact.
付録 A: 用語集
SDD を学ぶ上で頻出する用語を、一箇所にまとめます。
| 用語 | 説明 |
|---|---|
| Specification (仕様) | システムが満たすべき性質・契約・振る舞いを記述したもの。実装ではない。 |
| Single Source of Truth | ある事実について「正」となる唯一の出典。SDD では仕様がそれにあたる。 |
| Living Documentation | コードと同期し続け、常に最新を反映するドキュメント。BDD の .feature ファイル、自動生成された Redoc HTML 等。 |
| Contract Testing | 二者間の API 契約が双方とも守られていることを、双方独立に検証する手法。Pact が代表。 |
| Consumer-Driven Contract | 契約の起点を消費者(クライアント)に置く。提供者(サーバ)は消費者の期待を満たすよう実装する。 |
| EARS | Easy Approach to Requirements Syntax。"WHEN ... THE SYSTEM SHALL ..." 形式の要求記法。Kiro が採用。 |
| Three Amigos | BDD で要求を書くときに集まる三役(PO/Dev/QA)。 |
| Constitution / Steering | 全フィーチャー横断のガイドライン。AI エージェントが常時参照。 |
| Spec Rot | 仕様が更新されないまま実装だけが進み、両者が乖離する現象。 |
| Vibe Coding | 雰囲気で AI にコードを書かせるスタイル。SDD の対極。 |
| Property-Based Testing | 仕様(契約)を満たす任意の入力を生成して検証するテスト技法。Schemathesis、Hypothesis。 |
| Refinement | 抽象的な仕様から具体的な実装へ段階的に詳細化する形式手法の概念。 |
| TLA+ / TLC | Lamport の時相論理仕様言語と、その有限モデルチェッカ。 |
| Alloy | 軽量形式手法。リレーショナル論理 + SAT。 |
| Design by Contract | 関数・クラスに前条件・事後条件・不変条件を付ける手法。Eiffel が源流。 |
| OpenAPI | REST API の業界標準仕様記述言語。3.1 が JSON Schema 2020-12 と整合。 |
| AsyncAPI | 非同期メッセージング (Kafka/MQTT 等) 向けの仕様記述言語。 |
| Protocol Buffers / Protobuf | Google 製のバイナリシリアライゼーション + IDL。gRPC の基盤。 |
| GraphQL SDL | GraphQL のスキーマ定義言語。 |
| JSON Schema | JSON 構造のバリデーション仕様。OpenAPI のスキーマ部分の基盤。 |
| Spectral | OpenAPI/AsyncAPI 用の Linter。 |
| oasdiff | OpenAPI の互換性比較ツール。 |
| Spec Kit | GitHub OSS、/specify /plan /tasks /implement ワークフロー。 |
| Kiro | Amazon の AI 駆動 IDE/ワークフロー。 |
付録 B: よくある質問
Q1. SDD を導入するために、まず何から始めるべきですか?
A. プロジェクトの状況によりますが、最小コストで効果が大きいのは 「OpenAPI を 1 ファイル書く」 + 「oasdiff breaking を CI に追加する」 です。これだけで PR レビューでの「うっかり破壊的変更」の多くが防げます。次に SDK 自動生成を Makefile 化し、その次に AI 時代 SDD(Spec Kit)を導入する、という段階的アプローチをお勧めします。
Q2. AI 時代 SDD は OpenAPI ベースの API ファーストと矛盾しませんか?
A. しません。むしろ補完関係です。Spec Kit の /plan の中で「OpenAPI を更新する」というタスクを生成し、/tasks の Acceptance に oasdiff breaking を含めれば、両者は自然に統合されます。第 11 章の例がまさにその構成です。
Q3. spec.md に図表をどこまで書くべきですか?
A. What と Why に必要な図 に限るべきです。フローチャート(ユーザ操作の流れ)、状態遷移図(ビジネスロジックの状態)あたりまで。クラス図やシーケンス図は実装方針なので plan.md に置きます。
Q4. 形式仕様(TLA+ など)は本当に実務で使われていますか?
A. 限定的ですが使われています。AWS(DynamoDB, S3, EBS)、Microsoft(Azure Cosmos DB)、Intel(CPU マイクロアーキテクチャ)、MongoDB(レプリケーション)などが公開事例。並行性・分散・暗号 が主戦場で、CRUD 中心のアプリでは滅多に使いません。
Q5. BDD のシナリオが多すぎて CI が遅くなります。どうすれば?
A. シナリオを 3 階層に分けるのが定石です。
- Smoke (毎 PR、5 シナリオ程度)
- Acceptance (マージ後、20-50 シナリオ)
- Full Regression (夜間バッチ、全シナリオ)
タグで分類しておけば実行制御は簡単です。
Q6. spec.md は誰が書くのですか?
A. 理想は PM/PO/エンジニアの共著。AI 時代 SDD では、人間が /specify のプロンプトを書き、AI がドラフトを生成し、人間がレビューする という分担が一般的です。レビューを省略してはいけません。
Q7. SDD で生成されたコードはレビューしなくていいんですか?
A. 生成器の出力自体はレビュー対象外 ですが、「生成器が正しく動いた結果か」は確認する必要があります。CI に git diff --exit-code を入れて、生成物が必ず最新であることを保証するのが定石です。
Q8. Spec Kit と Kiro の選び方は?
A. AWS 中心の組織で人間レビューゲートが厳しい運用が必要なら Kiro。クラウド非依存で柔軟に複数エージェントを使い分けたいなら Spec Kit。両者は思想が近く、ファイル形式の違いは将来的に標準化される可能性があります。
Q9. 仕様の英語と日本語、どちらで書くべきですか?
A. チームの母語 が原則。ただし spec を AI に渡すなら 英語のほうが LLM の精度が高い 傾向にあります。両立解として「日本語で書き、AI 側に英訳した仕様も保管する」運用も増えています。
Q10. SDD の習得にどのくらいかかりますか?
A. 流派によります。
- OpenAPI: 数日〜1 週間
- BDD: 1〜2 週間(Three Amigos 文化の浸透含む)
- Spec Kit: 数日でツールに慣れる、1〜2 ヶ月で運用が安定
- TLA+/Alloy: 数週間〜数ヶ月
最初の三つは投資対効果が大きく、形式仕様は領域選びが重要です。
付録 C: ケーススタディ ―― ある中規模 SaaS の SDD 導入 18 ヶ月
ここでは架空の中規模 SaaS(ユーザ約 5 万、エンジニア 25 名、4 サービス)を想定し、SDD を段階的に導入した 18 ヶ月の道のりを描きます。実在企業の複数事例をベースに合成した教訓的な物語です。
C.1 0 ヶ月目: 出発点
スタートアップから成長期へ移行した SaaS では典型的な状況。
- バックエンド 4 サービス(モノリス由来でじわじわ分割中)
- API 仕様は Confluence ページに手書き、最終更新は半年前
- Web 開発時に「この API どう動くんだっけ?」とバックエンドエンジニアにいちいち Slack
- 障害の半数が「契約のずれ」由来(フロントが古いレスポンス形式を期待)
- AI ツール導入の機運が高まりつつあるが、バラバラに各自が Cursor / Claude Code を使っている
C.2 1〜2 ヶ月目: API ファースト導入
最も小さく効果が大きいフェーズから着手。
- 4 サービスのうち最大の「Account Service」に OpenAPI を導入
oapi-codegenで Go サーバスタブと TypeScript SDK を生成oasdiff breakingを PR 必須チェックに- Redoc を内部ポータルにデプロイ
結果: フロントの開発速度体感で 30% 改善。「Slack で API 聞く」やりとりが激減。CI で 2 件の破壊的変更を未然に防止。
C.3 3〜4 ヶ月目: 残り 3 サービスへ展開 + Pact
横展開フェーズ。
- 他の 3 サービスにも OpenAPI を導入
- Web と各サービス間に Pact 契約テストを設置
- Pact Broker を Kubernetes にホスト
can-i-deployをリリースゲートに追加
結果: マイクロサービス間の不整合起因の本番事故が四半期で 0 件に。
教訓: Pact 導入には学習コストがかかった(特に provider state の概念)。最初の 1 ヶ月は contractor が形だけ書いて運用が形骸化しかけた。3 ヶ月目に「Pact が落ちたらリリースをブロック」というルールに切り替えてから本気度が出た。
C.4 5〜6 ヶ月目: BDD 導入
「機能要件のずれ」を捉えるフェーズ。
- ペイメント機能の改修を機に BDD を試行
- Three Amigos(PM、開発リード、QA)が週 1 で 1 時間集まり .feature を書く
- godog で実 API に対してシナリオを実行
結果: ペイメント周りの仕様議論が文書化され、リリース後の追加要望が減少。一方、最初の数週は シナリオが UI 操作の手順書になりがち で、何度も修正を要した。
教訓: BDD のシナリオは「ボタンを押す」ではなく「課金を試行する」のように ビジネスドメイン語彙 で書く。これを習得するのに 2 ヶ月。
C.5 7〜9 ヶ月目: AI 時代 SDD(Spec Kit)試験導入
AI ツールの統制フェーズ。
- Claude Code を社内標準に
- 一部チームで Spec Kit を試験運用
CLAUDE.mdと.claude/agents/を整備- Constitution に「snake_case JSON、JWT 認証、sqlc のみ」等を明記
結果: AI が生成するコードのスタイルが統一され、レビュー時間が短縮。ハルシネーションによる虚構ライブラリ参照は 第 1 月に 5 件 → 第 3 月で 0 件 に減少(constitution に「依存ライブラリは package.json/go.mod に存在するもののみ」と明記したことが効いた)。
教訓: 最初の 1 ヶ月は「Spec Kit 真面目に使う組」と「従来通り vibe で書く組」が混在し、リポジトリのスタイルが分裂。3 ヶ月目に「新規フィーチャーは必ず Spec Kit 経由」とルール化することで統一。
C.6 10〜12 ヶ月目: CI ゲートの強化
仕様駆動を「機械化」するフェーズ。
- Spectral ルールセットを社内標準として整備(snake_case 強制、operationId 必須など)
oasdiff breakingの閾値を「警告」から「ブロック」に格上げ- 生成コードの
git diff --exit-codeを必須に - Pact
can-i-deployを本番リリースゲートに昇格
結果: CI が一時的に厳しくなり、開発者の不満も出たが、3 週間で適応。本番事故率がさらに低下。
教訓: 厳しい CI ゲートは開発者の最初の反発を覚悟して導入する必要がある。「なぜこのゲートを入れたのか」を Slack の固定メッセージとして残し、議論の往復を抑える。
C.7 13〜15 ヶ月目: TLA+ で分散ロックを設計
新しいフィーチャー(複数ユーザでの同期編集)の実装にあたり、分散ロックの設計を TLA+ で検証。
- シニアエンジニア 1 名が 2 週間 TLA+ を学習
- ロック獲得アルゴリズムを TLA+ で書き、TLC でモデルチェック
- 当初の設計に デッドロック発生経路 が見つかり修正
結果: 本番デプロイ後 6 ヶ月、関連の障害ゼロ。「TLA+ でデッドロックを潰しておかなければ間違いなく事故っていた」というのが事後検証の結論。
教訓: 全社的に TLA+ を広げる必要はない。ホットスポット(並行性・分散)に対してピンポイントで使う運用がコスト対効果に優れる。
C.8 16〜18 ヶ月目: 観測性と仕様の統合
最終フェーズ。
- 構造化ログに
requirement_idフィールドを追加 - Datadog ダッシュボードで「spec scenario あたりの本番リクエスト数」を可視化
- 一部の仕様シナリオが本番で 0 回しか実行されていないことが判明 → デッドコードとして削除
- カバレッジの低い API エンドポイントを廃止候補としてリストアップ
結果: コードベースが 8% 縮小。仕様 → 実装 → 運用のループが完成。
C.9 18 ヶ月後の総括
| 指標 | 開始時 | 18 ヶ月後 |
|---|---|---|
| 平均 PR レビュー時間 | 2.4 時間 | 1.1 時間 |
| 契約ずれ起因の本番事故 | 月 3.2 件 | 月 0.2 件 |
| 新人がフィーチャー追加できるまでの日数 | 21 日 | 9 日 |
| API ドキュメントの最終更新からの日数(中央値) | 180 日 | 0 日(自動生成) |
| AI 生成コードのレビュー却下率 | 計測不能 | 12% |
数字以外で大きかったのは 「文化の変化」。Slack で「API どうなってる?」が消え、「spec.md 見てくれ」になった。スパゲティになっていた契約理解が、リポジトリ内の構造化された資産に変わった。
C.10 失敗から得た重要な学び 5 つ
- CI ゲートは「徐々に厳しく」する。 最初から厳しすぎると反発、緩すぎると形骸化。3 ヶ月単位で段階的に強化。
- AI 時代 SDD はチーム単位で導入する。 個人がバラバラに使うと統制が取れない。
- 形式仕様はホットスポットだけ。 全社展開は失敗するが、ピンポイントなら絶大な効果。
- Constitution は短く、理由付きで。 長すぎると読まれない。各ルールに「なぜ」を 1 行。
- 観測性まで含めて初めて閉じる。 仕様 → コード → ログ → ダッシュボード、の輪。
このケーススタディは特定の企業を指すものではありませんが、SDD 導入を検討するチームが「次の 18 ヶ月でどんな景色になるか」をイメージする 手助けになれば幸いです。