📖 はじめに — 新アーキテクチャへの挑戦は、負債の発生源である(それでいい)
CTS-EC のバッチ基盤は、Queue / Parent / Chunk 構造の SAGA + Python Job という分散アーキテクチャに挑戦した。AI マッピング・埋め込み生成など重い処理を、コードとインフラ(Cloud Run Jobs・GCS・Pub/Sub)にまたがって編成する構成だ。
最初から負債ゼロでこんなものを設計できる技術者は、ほぼいない。実際、負債は出た — しかも定量で観測できる形で。本章はその発散を契約で止め、再発を常設機械ガードで封じ、それでも機械で拾えない残余を第II部のクロスファミリーレビューに配線するまでの、第I部でいちばん立体的な実録である。
📉 定量が異常を告げた — Fix 率 44%、同型 SAGA の 1.8 倍
SAGA + Python Job の組は 5 つある(埋め込み生成・カテゴリ展開・カテゴリマッピング・カラー AI・商品モールマッピング = PMM)。このうち最後発の PMM SAGA が、新規作成から 29 日で 24 commits / 8 fix(Fix 率 44%) — 先行の同型 SAGA の 1.8 倍という突出した修正密度を示した。
そしてある日、デグレが続発する。障害の解剖結果は「発散」の見本市だった。
| 症状 | 発散の内容 |
|---|---|
| Cloud Run Job が exit(1) | 存在しないメソッドの呼び出し。他 SAGA はアップロード関数が統一されているのに、PMM だけ 2 系統が同居していた |
| Batch API の deserialize 失敗 | 他 SAGA は gs:// フル URI、PMM だけ相対パスを渡していた |
| SAGA queue が Running のまま滞留 | Create Chunk ハンドラに埋め込み生成 + 類似検索(N 件 × 3N 回)を実装し、DB のステートメントタイムアウトを構造的に超過 |
| 親 SAGA の異常遷移 | Terminal 状態ガードが PMM にだけ無い(他 SAGA にはある) |
| GCS に孤児ファイル | blob パスの prefix が変遷し、他 SAGA の規約に揃っていない |
| ロジックの二重実装 | 1 件処理(Single)と N 件処理(Batch)で、プロンプト組立から判定ロジックまで完全に別実装 |
根本原因も特定されている。「参照実装を確認してから作る」という運用ルールが承認されたわずか 3 日後に PMM の実装が始まり、ルールが運用に乗らないまま独自実装が進行した。そして既存の標準は Queue/Parent/Chunk の構造契約しか定めておらず、「Create Chunk に重い処理を書かない」「Single と Batch でロジックを共有する」といった運用レベルの契約が明文化されていなかった。
第3章で宣言した型 3 の実例だ。「PMM は品質が悪い気がする」ではなく Fix 率 44%・同型比 1.8 倍という数字が ADR 起票を正当化した。数字がなければ、これは「たまたま忙しかった」で流れていた。
📜 同型性契約 — 障害から帰納した C-1〜C-10
対処は「PMM を直す」では終わらせない。障害の一つひとつを全 SAGA 共通の契約に帰納した。代表的なものを挙げる。
- C-1: Create Chunk ハンドラの禁止行為 — 外部 API 呼出・埋め込み生成・類似検索・キャッシュフィルタ・N+1 SELECT を明示的に禁止。Create Chunk は「対象 ID リストを取得してチャンクに割るだけ」の軽量処理に徹する。重い処理は Chunk 処理側で行う(statement_timeout 障害からの帰納)
- C-2: Single / Batch 経路の SSoT 化 — プロンプト組立・AI 応答の意味論的判定・信頼度閾値は同一関数を共有。Batch 固有なのは Batch API 呼出と N 件ステップ管理だけ(二重実装からの帰納)
- C-7: 親 SAGA の Terminal 状態ガード — 終了状態からの再遷移を全 SAGA で構造的に禁止
- C-3 / C-6: GCS パス・Pub/Sub メッセージの命名規則 — 「他と揃っていない」を規約違反として検出可能にする
契約を同型構造の上に置くと、「軽くあるべき場所」と「重くてよい場所」の線引きが一目になる。

そして契約は文書で終わらせない。検査スクリプトが 5 SAGA を機械検査し、正当な要件差分は例外台帳(YAML)に justification つきで登録、SAGA 関連ファイルの編集時にはスキルが検査を自動起動する。第I部の型 1(default-deny + 例外全量列挙)が、アーキテクチャ規約に適用された形だ。
ここで面白い伏線を 1 つ。C-1 が名指しで禁止した「Create Chunk 内のキャッシュ類似検索」は、元をたどればパフォーマンス改善として導入された機能だった。その顛末は第9章(パフォーマンス編)で回収する — 最適化は、置き場所を間違えると障害になる。
🛡️ 再発ゲート — 最頻バグを常設の default-deny で封じる
契約で発散は止まった。次の課題は「同じ型のバグが、忘れた頃に再発する」ことだ。CTS-EC で実際に繰り返された 2 つの罠を、常設のアーキテクチャテスト(手書き Reflection・全 28 Fact)で封じた。
P0-1: DefaultTenant トラップ
マルチテナント構成で、ハンドラが IDocumentSession を直接 inject するとテナント文脈なしのセッションを掴み、書込が silent に失敗する罠。過去 2 回発生した実績がある。対策は default-deny — 全ハンドラ(6 アセンブリ横断・フレームワークの探索設定と機械突合)のセッション注入を検出して原則 FAIL とし、テナント文脈が保証されるものだけを allowlist 64 件(各 entry に justification 付き) で許可した。
ここでも型 2(ゼロベースラインが兄弟負債を炙り出す)が発動する。64 件を列挙する過程で、テナント保証を確認しきれないハンドラも複数見つかり、「要是正」として台帳に載り追跡・解消の対象になった。ガードを敷く作業は、それ自体が監査になる。
P0-2: OCC(楽観的並行性制御)の配線漏れ
もう 1 つの再発型は、並行更新の喪失だ。実際に 3 並列の Command が同じ古い集約を読み、後勝ち(LWW)でデータの一部が欠損する障害が起きている(根治は商品 ID 単位のパーティションキュー)。さらに調査すると、OCC の配線が書込経路で大きく不足しており、「1 商品 1 Command という構造的前提」だけで守られていた危うさも判明した。書込経路の是正を行った上で、全ドキュメントマッピングに対する default-deny テストを敷き、「OCC 配線のない書込対象」を構造的に検出する形にした。
🎯 過剰設計を実態調査で捨てる — flaky テストと「1 行の根治」
本章でいちばん教訓的なのは、実は失敗しなかった話だ。DB 統合テストが時々落ちる(flaky)問題への恒久対策として、当初は「database-per-worker(テストワーカーごとに DB を分離して並列競合を根絶する)」という立派な計画が立っていた。
着手前の実態調査で、前提が崩れる。
- テストフレームワークは xUnit v2 だった(v3 前提の計画だった)
- DB を使う 46 のテストクラスは単一 Collection で直列実行されており、並列競合はそもそも構造的に存在しない
- flaky の真因は並列性ではなく、スキーマの遅延生成(lazy DDL) — cold start 時に最初のアクセスがテーブル生成と競合していた
正しい対処は、テスト基盤のリセット処理直後にスキーマを事前生成する 実質 1 行の修正(F.4) だった。適用後、該当スイートで 10/10 連続 Green を実証。さらに「スキーマ自動生成を使う経路は F.4 も持つこと」を検証するガードテストまで敷き、将来の F.4 忘れも構造的に検出する。
database-per-worker を実装していたら、数日を費やして存在しない問題を解決していた。型 4(過剰設計を実態調査で捨てる)の最良の実例であり、賢い設計より「実態はどうなっているか」を先に確かめる価値の証明だ。なお正直な注記もある — cold start の完全な再現はローカルでは難しいため、この項目の「解消済」宣言は CI 環境で 10 回連続 0 失敗を実証してからに保留されている。実装完了と実証完了を区別する規律は、ここでも守られている。
🌉 第II部への橋 — 機械で拾えない残余 3 経路
28 個のアーキテクチャテストを敷いてなお、ADR は「拾えないもの」を明記している。
- transitive 注入 — helper 経由で間接的にセッションへ到達する経路(直接 inject 検査の網外)
- 非ジェネリックな
Store(object)— 実行時型で決まる書込は静的検出不能 - 将来追加される SAGA 状態型 — OCC 要否判断を経ずに増える新型
重要なのは、この 3 経路を「注意しましょう」で終わらせず、クロスファミリーレビューの verifier チェックリストに正式登録したことだ。機械ゲート(第一の網)が自分の網の穴を申告し、別ファミリーの AI レビュー(第二の網)がその穴を毎回確認する。検知した場合は allowlist でなく production 是正へ回す、という処理方針まで決めてある。
第I部で積み上げてきた機械ガードと、第II部(クロスファミリー検証)は、この ADR の 1 節で物理的に接続されている。二層防御は思想ではなく、配線である。
🧾 まとめ
- 新アーキテクチャの発散は定量(Fix 率 44%・同型比 1.8 倍)で捕捉し、障害の解剖を全 SAGA 共通の契約 C-1〜C-10 に帰納する
- 契約は検査スクリプト + 例外台帳(justification 必須)+ 編集時自動起動で機械化する
- 再発する罠(テナント文脈・OCC)は default-deny アーキテクチャテスト + allowlist 全量列挙で常設封鎖。列挙の過程が未確認のハンドラを炙り出す
- flaky 対策の database-per-worker 計画は実態調査で撤回し、真因(lazy DDL)を 1 行で根治。実装完了と実証完了は区別する
- 機械検出不能な残余 3 経路は、クロスファミリー verifier のチェックリストへ配線 — 第I部と第II部はここで繋がる
次章はフロントエンドに舞台を移す — Atomic Design リファクタ編へ。
📚 シリーズ記事(ステアリング駆動開発・実践編)
序章
第I部: 技術的負債ドメイン別の実録(総点検ガイド + 8 ドメイン・全 9 回)
- リファクタリング総点検ガイド — 7 つの観点と進め方
- DDD/SOLID/BC 編 — god class 一掃と境界の機械ガード
- SAGA 編(本記事)
- Atomic Design リファクタ編 — 47 page 新規移植
- Storybook × a11y 実機編
- テスト品質・網羅性編
- Python ジョブ群編
- パフォーマンス編
- セキュリティ編
第II部: 検証エンジン(クロスファミリー検証・全 6 回)
- 総論 — マルチ LLM の 2 系統と見取り図
- 裏取り編 — +18.1pt 論文の検証
- 設計編 — finder/verifier 分業と逆順禁止
- TDD×ハーネス編 — テスト保護と三層ゲート
- 実装編 — Claude Code の中から Codex を動かす
- モデル戦略編 — ティア割当と「買うか組むか」
終章
- クロスファミリー検証の最終型 —「正しく作る」から「自分で正しさを測り直す」へ — 検証エンジンに「計測」の層をはめ、自己修正ループを閉じる
番外編
- CI テスト 29 分 → 5.6 分 — 検証エンジンの平時運用実録 — 実測が有力仮説を殺し、退行前より速くなった 1 日
- GPT-5.6 sol 切替の当日実録 — もう一つのデフォルト追従 — CLI 更新が黙って替える finder と、当日中の再計測
- 遊休 90% の枠に仕事を振る — Codex 上流調査と verifier バッチ化 — 逆順禁止の境界を ADR で確定し、読む仕事を遊休枠へ移した 1 日
- ハーネスが単一プロジェクトを卒業する — プラグイン化と二層配布 — 4 プロジェクト展開への切り出しと、version + SHA ピンによる非強制追従
- 69分で5リリース — Opus 5 当日対応が暴いた共通ハーネスの死角 — 新モデル対応を起点に、配布・文書・CI の回帰を二消費者で検出した 69 分
関連記事
- イベントソーシング / CQRS — SAGA の背景概念
- CTS-EC 商品マスタ構想 — 本 SAGA 群が支えるドメイン