1つの実践講座から方法論へ:私がDDDとCQRSを「検証」したプロセス
要約(TL;DR):ドメイン駆動設計(DDD)が実際のコードでどう 구현されるのかを確かめるため、私はJavaマイクロサービスとCQRSを扱うUdemyの実践講座を手に取りました。しかし講師のコードをそのまま写経するのではなく、まずGeminiと共に高次元の開発ライフサイクル標準を構築し、ShopifyのToxiproxyとArchUnitを用いて教材コードにネットワーク障害注入監査を実施しました。その結果、3つの致命的な台帳バグ( $500.00の二重書き込み不整合、-\60.00の並行過振り出し競合、+\250.00のイベント再送による架空残高)を実証し、Go 1.22とPostgreSQL 16で抜本的に再設計した上で、この検証プロセス全体を**
tdpm-sdd-governance** Antigravityプラグインとして結晶化させました。
1. 出発点:1つの実践プロジェクトと素朴な疑問
すべては、きわめて実践的なエンジニアリング上の疑問から始まりました。「ドメイン駆動設計(DDD)は、実際の分散システムにおいてどのように実装されるべきなのか?」
DDDに関する書籍や記事の多くは、2つの極端に振れがちです。ユビキタス言語や境界づけられたコンテキストといった抽象概念ばかりを語ってネットワーク越しの実装に踏み込まないか、あるいは境界が存在する理由を説明しないままエンティティやアノテーションの書き方に終始するかのどちらかです。そのギャップを埋めるため、私はUdemyの実践コース——『Java Microservices: CQRS & Event Sourcing with Kafka』——を題材に選びました。このリポジトリは、cqrs.core、account.common、account.cmd、account.queryの4モジュール・全65個のJavaファイルで構成され、書込側のイベントストアにMongoDB(:27017)、イベントバスにApache Kafka(:9092)、読取側のプロジェクションDBにPostgreSQL(:5432)を採用していました。
しかし、コードを読み進めるうちに違和感が募っていきました。AccountAggregate、BaseEvent、CommandDispatcher、EventStoreRepositoryといったおなじみの構成要素は揃っているものの、レイヤー間の境界が驚くほど曖昧だったのです。「なぜ残高不足のバリデーションがAggregate内部ではなくCommandHandlerに書かれているのか?」「なぜ同一口座のイベントが4つの別々のKafkaトピック(OpenAccountEvent、FundsDepositedEvent、FundsWithdrawnEvent、CloseAccountEvent)に分割され、因果順序の保証なしに送信されているのか?」
手を止めてGeminiと対話を重ね、「現代のプロダクション開発においてDDDはどう実践されているのか」を掘り下げたとき、視界が一気に開けました。私はそれまで**戦術的DDD(Tactical DDD)のパーツだけを見ており、その前提となる戦略的DDD(Strategic DDD)**の視点を欠いていたのです。
- 戦略的DDD(マクロな境界設計レイヤー —— コードを書く前に定義する):クラスやテーブルを1つでも作る前に、「何を同じ境界にまとめ、何を分離するか」を定義します。ビジネス領域をコアドメイン(Core)、サポートドメイン(Supporting)、**汎用ドメイン(Generic)**に分類し、**ユビキタス言語(Ubiquitous Language)の用語集を固定し、腐敗防止層(ACL)や公開ホストサービス(OHS)を通じて境界づけられたコンテキスト(Bounded Context)**間の契約を明確化します。
- 戦術的DDD(ミクロな実装レイヤー —— 戦略的境界に従属する):境界が確定して初めて、集約(Aggregate)、エンティティ、値オブジェクト(Value Object)、ドメインイベント、リポジトリポートが意味を持ちます。集約とは単なるデータの入れ物ではなく、ビジネス上の不変条件(Invariants)を外部から隔離して保護するトランザクション整合性の境界そのものです。
戦略的DDDによる境界定義を飛ばして戦術的DDDのパターンだけを並べても、ただの冗長なボイラープレートにしかなりません。
2. より深い原動力:受動的に学ぶのではなく、基準を作って「監査」する
この講座を単なる「写経チュートリアル」として終わらせたくなかったのには、もう1つ深い理由がありました。私はこの1年以上の間、同期APIのOpenAPI 3.1や非同期イベント駆動のAsyncAPI 3.0といったAPIコントラクト(契約)手法を継続的に追いかけ、実践してきました。そのすべては、**「AIエージェントによる開発(Agentic Development)の時代に向けたアーキテクチャ基盤を築くこと」**という一貫した目的に繋がっていました。
AIコーディングエージェントと協業する時代において、エンジニアのボトルネックはもはやタイピング速度ではなく、仕様の精緻さと境界ガバナンスの設計力にあります。もしAIに対して単に「CQRSで銀行口座マイクロサービスを作って」と指示すれば、AIは一見もっともらしい数千行のコードを生成し、正常系の単体テストを通過させるでしょう。しかしその裏には、分散システム特有の致命的なデータ不整合バグが潜んでいます。
そこで私は、従来の学習プロセスを完全に逆転させました。
- まずLLMと共に高次元の開発ライフサイクル標準を構築する:講師のコードから出発するのではなく、戦略的DDD、戦術的DDD、そしてOpenAPI/AsyncAPI契約手法を統合したフェーズゲート型の開発標準を先に作り上げました。
- 自ら構築した基準で他者のコードを「監査(Audit)」し、学びを検証する:構築した標準をものさしとして使い、講師の65ファイルのJava CQRSコードベースを徹底的に監査しました。

もし自分が組み立てた方法論が単なる机上の空論ではなく本物の実用性を持っているなら、「すでに単体テストがすべて通っている教材コード」の中から、構造的な欠陥を正確に炙り出せるはずです。
3. 机上の理論を超えた実証:Toxiproxyによるカオスエンジニアリング監査
アーキテクチャの世界において、言葉だけの批判は簡単です。「トランザクショナルOutboxがない」「集約が不変条件をカプセル化していない」と指摘するだけなら誰にでもできます。それらの欠陥が現実の障害時にどれほど致命的な結果をもたらすかを実証するため、私はJavaコードベースに対してArchUnit(AST静的構造解析)、JUnit 5の並行バリア(CyclicBarrier)、そしてTestcontainers上で動作する**Shopify製TCPプロキシ「Toxiproxy」**を組み合わせた自動検証ハーネスを構築しました。
この実証監査によって、**「単体テストが通って動くだけのプロジェクト」と「真にレジリエント(耐障害性のある)なシステム」**の間にある決定的な断絶が浮き彫りになりました。

欠陥 A:非アトミックな二重書き込み(Dual-Write)によるスプリットブレイン( $500.00)
教材の AccountEventStore.java(44〜48行目)では、MongoDBへのイベント保存とKafkaへのメッセージ送信が、何のトランザクション連携もない2回の順次ネットワークI/Oとして実装されていました。
// AccountEventStore.java:44-48 —— Outboxを持たない非アトミックな二重書き込み
EventModel persistedEvent = eventStoreRepository.save(eventModel); // 1. MongoDB (:27017) へコミット
if (!persistedEvent.getId().isEmpty()) {
eventProducer.produce(event.getClass().getSimpleName(), event); // 2. Kafka (:9092) へ送信
}
ローカルのクリーンなDockerネットワーク内では、このコードは99.9%成功します。では、一瞬のネットワーク瞬断が起きたらどうなるでしょうか? 私は ToxiproxyContainer を使ってSpring BootからKafkaへの通信をTCPプロキシ(:8666)経由にルーティングし、初期残高 $1,000.00 で口座を開設した直後に kafkaProxy.setConnectionCut(true) でTCP接続を遮断した上で、$500.00 の入金コマンド(DepositFundsCommand)を実行しました。
- MongoDB側は
version = 1のFundsDepositedEventを正常にコミットし、イベントストア上の残高は $1,500.00 になりました。 - 直後の
KafkaTemplate.send()は切断されたソケット上で例外をスローし、Kafkaには 0件 のメッセージしか届きませんでした。 - PostgreSQLの読取プロジェクション側の残高は、永久に $1,000.00 のまま取り残されました。
仮に本番環境でわずか 0.1% の一時的なネットワーク障害が発生したとすると、60日間で累積 **$24,500.00 ものサイレントな台帳乖離(ドリフト)**が発生する計算になります。
欠陥 B:ドメイン不変条件の漏洩と並行過振り出し競合(-\60.00)
ArchUnit による静的解析を実行すると、即座に AccountCommandHandler.java(30行目)のルール違反が検出されました。「残高以上の出金を禁止する」という最も重要なドメイン不変条件が AccountAggregate の外にあるCommandHandlerに漏洩しており、AccountAggregate.withdrawFunds() 自体には残高チェックが一切存在しなかったのです。
// AccountCommandHandler.java:30 —— ドメイン不変条件が集約の外に漏洩している!
var aggregate = eventSourcingHandler.getById(command.getId());
if (command.getAmount() > aggregate.getBalance()) {
throw new IllegalStateException("Withdrawal declined, insufficient funds!");
}
aggregate.withdrawFunds(command.getAmount()); // AccountAggregate内部には防御ロジックがゼロ!
さらに悪いことに、MongoDBの eventStore コレクションには (aggregateIdentifier, version) の複合ユニークインデックスすら設定されていませんでした。CyclicBarrier(2) を用いて残高 $100.00 の口座に対して2つのスレッドから同時に $80.00 の出金リクエストを送信すると、両方のスレッドが balance = 100.0 を読み取って 80.0 > 100.0 のチェックを通過し、双方がイベントを書き込んだ結果、口座残高は **-\60.00(不正なマイナス残高)**へと転落しました。
欠陥 C:非べき等なイベント再送による「架空資金」の発生(+\250.00)
Kafkaの配信保証は基本的に At-Least-Once(少なくとも1回) であり、コンシューマグループのリバランス時には重複メッセージが届きます。しかし AccountEventHandler.java(30〜39行目)にはイベントIDの重複排除テーブルもバージョン連番検証もなく、単に現在残高へイベント金額を加算していました。テストで $250.00 の FundsDepositedEvent を1回再送しただけで、$1,250.00 だったはずの口座残高が $1,500.00 に膨れ上がり、$250.00 の架空資金が生み出されました。
(ほかにも、金額計算への double 浮動小数点数の使用、FundsWithdrawnEvent ハンドラでの setBalance() 呼び出し忘れによる出金反映漏れ、4トピック分割によるイベント順序逆転などが次々と検出されました。)
Go 1.22 と PostgreSQL 16 による抜本的リエンジニアリング
私たちが構築したアーキテクチャ標準によってこれらの障害モードを完全に根絶できることを証明するため、システム全体を Go 1.22+ と PostgreSQL 16 で再設計しました(D:\Work\Go-CQRS-Microservices)。
- トランザクショナルOutbox(
event_store+event_outbox):ドメインイベントの追記とOutboxレコードの挿入を単一のPostgreSQL ACIDトランザクション内でコミットし、LISTEN/NOTIFY(outbox_wakeup)およびFOR UPDATE SKIP LOCKEDワーカーで非同期中継します。ToxiproxyでKafkaを遮断しても、回線復旧後に は確実に $0.00 に収束します。 - セント単位の整数型カプセル化集約(
int64)+ DBレベル楽観的ロック(OCC):AccountAggregateはプライベートフィールドbalanceCents int64を保持して内部で不変条件を検証し、PostgreSQLのUNIQUE (aggregate_id, version)制約によって並行競合を確実に409 Conflictとして排除します。 - 二重バリアべき等プロジェクション:読取側コンシューマは
processed_eventsテーブル(ON CONFLICT DO NOTHING、ベンチマーク実測値 243.1 ns/op・メモリ割り当て0 B/op)とWHERE last_version = $version - 1の連番チェックにより、重複配信を完全に無害化します。
4. より明確な結論:なぜ汎用SDDフレームワークは複雑系で破綻するのか(そして過剰設計をどう防ぐか)
戦略的DDDからToxiproxyカオス監査、そしてGoによる再構築までを一気通貫でやり抜いたことで、現在のAI駆動開発における課題がはっきりと見えてきました。
現在、多くの開発者が Superpowers や OpenSpec といった既存の**スペック駆動開発(Spec-Driven Development: SDD)**フレームワークを導入し、仕様書を書けばAIエージェントが正確なコードを書いてくれると期待しています。しかし、単純な機能追加ならともかく、CQRSのような複雑な分散アーキテクチャに直面した途端、汎用的なSDDフレームワークは機能不全に陥ります。
理由は明確です。戦略的DDDによってシステムの境界を定義しない限り、SDDフレームワークは中身のない抜け殻に過ぎないからです。 対象のモジュールがコアドメインなのか汎用CRUDなのかを区別せずにSpecを書けば、次の2つの失敗のどちらかに陥ります。
- コア領域の設計不足(Under-Engineering):表面的なAPIフィールドだけを定義したSpecになり、集約の不変条件や楽観的ロック、Outboxのアトミック性が抜け落ち、Toxiproxyで暴き出したような致命的バグをAIが再生産します。
- 周辺領域の過剰設計(Over-Engineering):単なる「ユーザー通知設定」のような単純なCRUD画面にまでCQRS、イベントソーシング、Kafkaトピック、非同期プロジェクションを適用してしまい、不要な複雑性でプロジェクトを窒息させます。
逆に、SDDのワークフローに戦略的DDDの境界定義を組み込み、その直後に**戦術パターン選定ゲート(Bridge 2.5b)**を設ければ、複雑系の品質担保と過剰設計の防止を同時に達成できます。すべての問題にCQRSが必要なわけではありません。私たちのフレームワークでは、1行のコードを書く前にすべてのコンテキストを以下の3段階ファネルで振り分けます。
| 戦術アーキテクチャパターン | 対象サブドメインの特性 | 選定基準(いつ使うか) | 強制されるアーキテクチャ要素 | 明確に禁止されるアンチパターン |
|---|---|---|---|---|
CQRS_EVENT_SOURCING |
読み書きの非対称性が高く、厳格な監査証跡が求められるコアサブドメイン | 金融台帳、任意の時点への状態再生、複雑な多段階ドメイン不変条件 | カプセル化集約(int64セント単位)+ 不変 event_store + event_outbox + 二重バリアべき等プロジェクタ |
イベントを介さない直接の UPDATE;OutboxなしのKafka二重書き込み |
STATE_BASED_OCC |
並行書き込み競合があるが、即時のRead-Your-Own-Writes(RYW)一貫性が必要なコア/サポート領域 | 与信枠管理、承認ワークフロー、在庫引当ステータス管理 | カプセル化集約ルート + 明示的な version 列を持つACID状態テーブル(WHERE version = $expected) |
RYWが必要な場面での不要なイベントストア再生や非同期プロジェクション遅延 |
VERTICAL_SLICE_CRUD |
読み書きが1対1で対称なサポート/汎用サブドメイン(Generic) | ユーザー設定画面、支店ディレクトリ検索、静的マスタ管理 | シンプルな垂直スライス(Handler SQL / Repository直結) | 集約ルート、コマンドバス、イベントストア、Outboxの導入を厳禁とする(過剰設計ゼロ) |

Bridge 2.5b を通過させることで、DDDは「システムを複雑にする言い訳」ではなく、「不要なボイラープレートを切り落とすための最も鋭いメス」へと変わります。
5. より大きな視点:探求のプロセスそのものがアウトプットである
振り返ってみると、この取り組みの最大の成果は、Javaのバグを暴いたことでもGoのマイクロサービスを書いたことでもありません。**「DDDとCQRSを検証するために辿った4段階の探求プロセスそのものを、再利用可能なソフトウェアとしてパッケージ化できる」**と気づいたことでした。
私はこの検証ワークフローを tdpm-sdd-governance というオープンソースの Google Antigravity プラグインとして実装しました(GitHubリポジトリ:github.com/aruruka/tdpm-sdd-governance)。.agents/state/phase-lock.json による SHA-256暗号化フェーズロックと PreToolUse フックを備え、AIエージェントに対して**6フェーズのBDD優先スペック駆動開発ライフサイクル(P0 P5)**を強制します。
- フェーズ 0(
P0—— ブラウンフィールド調査とトリアージ、担当:@pm-planner):既存のコードベースや設計書を監査し、すべての振る舞いを Preserve(維持)、Invert/Remediate(反転修正)、Extend(拡張) に分類します(specs/bdd/legacy-parity-triage.md)。この段階ではコードの作成を一切禁止します。 - フェーズ 1(
P1—— 戦略的DDDとコンテキストマップ、担当:@pm-planner+@td-auditor):ユビキタス言語用語集、サブドメイン分類、コンテキスト間の関係性を確定します(docs/architecture/boundary-map.md、specs/ddd/arch-rules.yaml)。 - フェーズ 2(
P2—— ソクラテス式対話型BDDと Bridge 2.5b ゲート、担当:@pm-planner+@td-auditor):人間のアーキテクトと最低2ラウンドの具体例マッピング(Example Mapping)対話を行い、イベントモデリングを経て Bridge 2.5b 戦術パターン(CQRS_EVENT_SOURCING/STATE_BASED_OCC/VERTICAL_SLICE_CRUD)をロックし、インフラ詳細を含まない純粋なGherkin.feature仕様書を作成します。 - フェーズ 3(
P3—— 戦術的DDDスタブとポート定義、担当:@td-auditor):確定した.featureから集約ルートやコマンド、イベント、ポートの型シグネチャを生成します。ASTゲートにより、この段階のすべてのメソッド本体は次の1行スタブに制限されます。panic("SDD_STUB: implementation locked until Phase 5b") - フェーズ 4(
P4—— インターフェース契約とDBスキーマの確定、担当:@td-auditor+@pm-planner):実装コードを書く前に、OpenAPI 3.1、AsyncAPI 3.0、PostgreSQL DDLマイグレーション、および UATプレイブックを確定・ロックします。 - フェーズ 5(
P5—— Red Green カオス検証、担当:@td-auditor):スタブに対する失敗テストの実装(5a Red Harness)、ロックされたP1〜P4の契約を変更せずにドメイン中心から外側へ向かう実装(5b Green Inside-Out)、そしてToxiproxyカオス試験とベンチマーク検証(5c Verification)を実行します。
本プラグインはMITライセンスのもと、GitHubで完全公開されています:
- GitHub リポジトリ:
github.com/aruruka/tdpm-sdd-governance - クイックインストール:
# Antigravity プラグインディレクトリにクローンして依存関係を同期 git clone https://github.com/aruruka/tdpm-sdd-governance.git ~/.gemini/config/plugins/tdpm-sdd-governance uv --directory ~/.gemini/config/plugins/tdpm-sdd-governance sync
Agentic Development(AIエージェント駆動開発)の時代において、人間のエンジニアの価値は「モジュールを1行ずつ手書きすること」から、**テクニカルディレクター(TD)兼プロダクトマネージャー(PM)**として「境界を定義し、前提を疑い、カオス実験で現実を検証し、そこから得た知見を確定的なガードレールとして仕組み化すること」へと移行しています。
1つの実践プロジェクトから始めよう。AIと共に基準を築こう。カオスエンジニアリングで現実を監査しよう。そして、あなた自身の「探求のプロセス」そのものを、次のシステムを生み出す武器へと変えていこう。
