From a Single Course to a Methodology: How I "Validated" DDD and CQRS
TL;DR: I started a hands-on Udemy course on Java microservices, CQRS, and Event Sourcing just to figure out how Domain-Driven Design (DDD) actually looks in code. Instead of passively copying the instructor's repository, I co-constructed a lifecycle standard with Gemini first, used Shopify's Toxiproxy and ArchUnit to audit the course codebase under real network faults, proved three catastrophic ledger bugs ( $500.00 split-brain drift, -\60.00 concurrent overdrafts, and +\250.00 replay inflation), remediated the architecture in Go 1.22 and PostgreSQL 16, and crystallized the entire workflow into the
tdpm-sdd-governanceAntigravity plugin.
1. The Starting Point: A Hands-On Project and a Question
It all started with a practical engineering question: How do you actually apply Domain-Driven Design (DDD) in a real distributed system?
Most books on DDD either stay at altitude—talking about ubiquitous language and bounded contexts without showing how packets hit the wire—or dive straight into code annotations without explaining why the boundaries exist in the first place. To bridge that gap, I picked up a hands-on Udemy project—"Java Microservices: CQRS & Event Sourcing with Kafka"—consisting of 65 Java source files split across cqrs.core, account.common, account.cmd, and account.query, backed by MongoDB (:27017) as the write-side Event Store, Apache Kafka (:9092) as the event bus, and PostgreSQL (:5432) as the read-side projection store.
As I worked through the codebase, something felt off. The project had all the recognizable building blocks—AccountAggregate, BaseEvent, CommandDispatcher, EventStoreRepository—yet the boundaries between layers felt porous. Why was business validation happening inside command handlers? Why were four separate Kafka topics (OpenAccountEvent, FundsDepositedEvent, FundsWithdrawnEvent, CloseAccountEvent) firing independently for the same bank account?
When I paused to work through these questions with Gemini—asking how DDD is practiced in modern production engineering—the fog cleared. I had been looking at Tactical DDD in isolation while missing Strategic DDD entirely:
- Strategic DDD (The Macro Boundary Layer): Before a single struct, class, or database table is created, Strategic DDD defines what belongs together and why. It classifies business capabilities into Core, Supporting, and Generic subdomains, locks a Ubiquitous Language Glossary, and establishes Bounded Contexts with explicit upstream/downstream contracts (Anti-Corruption Layers, Open Host Services, and Published Languages).
- Tactical DDD (The Micro Implementation Layer): Only after a Bounded Context is sealed do patterns like Aggregates, Entities, Value Objects, Domain Events, and Repository Ports make sense. An Aggregate is not just a data holder with getters and setters; it is a transactional consistency boundary that enforces domain invariants internally.
Without Strategic DDD to draw the perimeter, Tactical DDD degenerates into ceremonial boilerplate.
2. A Deeper Driver: Auditing Instead of Passively Learning
There was a deeper reason I didn't want to treat this course as a copy-paste tutorial. For over a year, I had been tracking and practicing API contract methodologies—specifically OpenAPI 3.1 for synchronous boundaries and AsyncAPI 3.0 for event-driven streams. Everything I was studying pointed toward a single horizon: laying the architectural foundation for Agentic Development.
When you work with AI coding agents, the bottleneck is no longer typing speed; it is specification precision and boundary governance. If you tell an AI agent to "build a bank account microservice," it will happily generate thousands of lines of plausible-looking code that passes happy-path unit tests while harboring fatal distributed systems bugs.
So I flipped the traditional learning workflow on its head:
- Co-construct the standard first: Instead of starting from the instructor's code, I worked with the LLM to synthesize Strategic DDD, Tactical DDD, and OpenAPI/AsyncAPI contract rules into a rigorous, phase-gated engineering lifecycle.
- Validate my learning by auditing others: Once that standard existed, I turned around and used it as an inspection lens to audit the instructor's 65-file Java CQRS codebase.

If my newly constructed methodology was genuinely useful—and not just academic jargon—it should be able to pinpoint exact structural flaws in a working codebase that already passed all of its unit tests.
3. Validation Beyond Mere Theory: Chaos Engineering with Toxiproxy
In software architecture, theoretical critique is cheap. Anyone can look at a repository and say, "This lacks a Transactional Outbox" or "Your aggregate isn't encapsulating its invariants." To prove that these flaws mattered in practice, I built an automated verification harness around the Java codebase combining ArchUnit (for static AST boundary inspection), JUnit 5 concurrency barriers, and Shopify's Toxiproxy inside Testcontainers (for deterministic TCP fault injection between Spring Boot, MongoDB, and Kafka).
That empirical audit exposed the chasm between a project that merely "runs" and a system that is truly "resilient."

Defect A: The Non-Atomic Dual-Write Split-Brain ( $500.00)
In the course's AccountEventStore.java (lines 44–48), persisting a domain event to MongoDB and publishing it to Kafka happen as two sequential, uncoordinated network I/O calls:
// AccountEventStore.java:44-48 — Sequential Dual-Write without an Outbox
EventModel persistedEvent = eventStoreRepository.save(eventModel); // 1. Commits to MongoDB (:27017)
if (!persistedEvent.getId().isEmpty()) {
eventProducer.produce(event.getClass().getSimpleName(), event); // 2. Publishes to Kafka (:9092)
}
In a clean local Docker network, this succeeds 99.9% of the time. What happens during a transient broker blip? Using ToxiproxyContainer, I routed the Spring Boot Kafka producer through a TCP proxy (:8666) and executed kafkaProxy.setConnectionCut(true) right after an account opened with $1,000.00, then submitted a $500.00 DepositFundsCommand:
- MongoDB committed
FundsDepositedEventatversion = 1. Replaying the write-side Event Store showed a balance of $1,500.00. KafkaTemplate.send()threw an exception across the severed TCP socket. Zero messages reached Kafka.- The PostgreSQL read projection remained permanently frozen at $1,000.00:
Simulating a modest 0.1% transient network fault rate over 60 days produced $24,500.00 in silent cumulative ledger drift—with zero built-in reconciliation mechanism.
Defect B: Tactical DDD Invariant Leakage & Concurrent Overdrafts (-$60.00)
When I ran our ArchUnit AST suite against the command service, it immediately flagged AccountCommandHandler.java (line 30). Instead of enforcing the minimum-balance invariant inside AccountAggregate, the check leaked out into the application handler, while AccountAggregate.withdrawFunds() blindly accepted any amount:
// AccountCommandHandler.java:30 — Business invariant leaked out of the Aggregate!
var aggregate = eventSourcingHandler.getById(command.getId());
if (command.getAmount() > aggregate.getBalance()) {
throw new IllegalStateException("Withdrawal declined, insufficient funds!");
}
aggregate.withdrawFunds(command.getAmount()); // AccountAggregate has ZERO internal guard!
Worse still, MongoDB's eventStore collection had no compound unique index on (aggregateIdentifier, version). By synchronizing two concurrent $80.00 withdrawal threads against a $100.00 account using a CyclicBarrier(2), both threads read balance = 100.0, both passed the handler's 80.0 > 100.0 check, and both appended withdrawal events—driving the account balance to -\60.00.
Defect C: Non-Idempotent Replay & Phantom Funds (+$250.00)
Kafka guarantees at-least-once delivery, meaning consumers will receive duplicate events whenever a consumer group rebalances. Yet AccountEventHandler.java (lines 30–39) updated PostgreSQL using blind relative addition without tracking processed event IDs or checking sequence numbers. Re-delivering a single $250.00 FundsDepositedEvent inflated a $1,250.00 account to $1,500.00 in phantom funds.
(And those weren't the only issues: the audit also caught double IEEE-754 floating-point representation for currency, a missing setBalance() call in AccountEventHandler.on(FundsWithdrawnEvent) that left read-side withdrawals broken, and topic-per-event fanout across four Kafka topics that destroyed causal ordering.)
Closing the Loop in Go 1.22 & PostgreSQL 16
To prove our architecture standard could eliminate every one of these failure modes, I re-architected the system in Go 1.22+ and PostgreSQL 16 (D:\Work\Go-CQRS-Microservices):
- Transactional Outbox (
event_store+event_outbox): Domain events and outbox relay records commit inside a single PostgreSQL ACID transaction, paired withLISTEN/NOTIFY(outbox_wakeup) andFOR UPDATE SKIP LOCKEDworkers. Even when Toxiproxy severs Kafka mid-flight, converges to $0.00 as soon as connectivity returns. - Sealed Aggregate in Integer Cents (
int64) + Database OCC:AccountAggregatekeepsbalanceCents int64private, rejects invalid withdrawals internally, and relies onUNIQUE (aggregate_id, version)in PostgreSQL so concurrent conflicting writes fail deterministically with409 Conflict. - Double-Barrier Idempotent Projection: The read-side consumer inserts
event_idintoprocessed_events(ON CONFLICT DO NOTHING, executing in 243.1 ns/op with0 B/op) and enforcesWHERE last_version = $version - 1onbank_accounts.
4. A Clearer Conclusion: Why Generic SDD Fails (and How to Prevent Over-Engineering)
Going through this full cycle—from Strategic DDD down to Toxiproxy chaos tests and a clean-sheet Go rewrite—gave me a much sharper view of current AI engineering workflows.
Today, many developers reach for off-the-shelf Spec-Driven Development (SDD) frameworks (such as Superpowers or OpenSpec) hoping they will magically make AI coding agents reliable. Those tools work fine for simple feature tickets, but when confronted with complex distributed architectures like CQRS, generic SDD frameworks fall apart.
Why? Because without first establishing system boundaries via Strategic DDD, a spec framework is just an empty shell. If you write a specification without knowing whether a module is a Core Subdomain or a Generic CRUD utility, one of two disasters happens:
- Under-engineering the Core: You write a superficial API spec that misses aggregate invariants, optimistic concurrency, and outbox atomicity—reproducing the exact bugs we proved with Toxiproxy.
- Over-engineering the Periphery: You apply full-blown CQRS, Event Sourcing, Kafka topics, and projection tables to a simple user-settings form—drowning the project in accidental complexity.
Once you pair Strategic DDD boundaries with a hard Tactical Pattern Selection Gate (Bridge 2.5b) right inside your SDD workflow, both failure modes disappear. Not every problem deserves CQRS. In our framework, every Bounded Context must pass through a strict three-tier funnel before a single line of domain code is stubbed:
| Tactical Paradigm | Target Subdomain Profile | When to Select | Mandatory Architectural Primitives | What It Explicitly Forbids |
|---|---|---|---|---|
CQRS_EVENT_SOURCING |
Core Subdomain with high read/write scaling asymmetry & regulatory audit requirements | Financial ledgers, temporal state reconstruction, complex multi-step domain invariants | Sealed Aggregate (int64 cents) + Immutable event_store + Transactional event_outbox + Double-Barrier Idempotent Projector |
Direct UPDATE mutations on state without events; unguarded dual-writes to Kafka |
STATE_BASED_OCC |
Core / Supporting Subdomain with concurrent write contention & immediate Read-Your-Own-Writes (RYW) need | Quota management, approval state machines, inventory reservation | Encapsulated Aggregate Root + ACID state table with explicit version column (WHERE version = $expected) |
Unnecessary Event Store replay loops or asynchronous projection lag where RYW is required |
VERTICAL_SLICE_CRUD |
Supporting / Generic Subdomain with 1:1 read/write symmetry & simple validation | User notification settings, branch directory lookup, static metadata CRUD | Lean Handler SQL query / repository slice | Strictly forbids Aggregates, Command Buses, Event Stores, and Outbox tables (Zero Over-Engineering) |

By forcing every feature through Bridge 2.5b, DDD stops being an excuse to over-engineer and becomes the sharpest knife for cutting away unnecessary ceremony.
5. The Bigger Picture: The Process of Exploration Is the Output
Looking back, the biggest reward of this journey wasn't merely fixing a Java repository or writing a fast Go microservice. It was realizing that my four-step exploration process could itself be compiled into reusable software.
I took the exact workflow I used to validate DDD and CQRS and packaged it into tdpm-sdd-governance—an open-source Google Antigravity plugin (available on GitHub at github.com/aruruka/tdpm-sdd-governance) that enforces a 6-Phase BDD-First Spec-Driven Development Lifecycle (P0 P5) backed by cryptographic SHA-256 locks in .agents/state/phase-lock.json and a deterministic PreToolUse interceptor hook:
- Phase 0 (
P0— Brownfield Discovery & Triage,@pm-planner): Audits existing codebases and classifies every behavior into Preserve, Invert/Remediate, or Extend (specs/bdd/legacy-parity-triage.md). Zero code files allowed. - Phase 1 (
P1— Strategic DDD & Context Map,@pm-planner+@td-auditor): Locks the Ubiquitous Language Glossary, Subdomain classifications, and Bounded Context relationships (docs/architecture/boundary-map.md,specs/ddd/arch-rules.yaml). - Phase 2 (
P2— Iterative Socratic BDD & Bridge 2.5b Gate,@pm-planner+@td-auditor): Requires at least two rounds of Socratic Example Mapping with the human architect, maps the Event Modeling timeline, locks the Bridge 2.5b Tactical Pattern (CQRS_EVENT_SOURCING,STATE_BASED_OCC, orVERTICAL_SLICE_CRUD), and produces zero-leakage Gherkin.featurespecs. - Phase 3 (
P3— Tactical DDD Stubs & Ports,@td-auditor): Compiles domain types and Aggregate roots where every method body is restricted by AST gatekeepers to a single-line stub:panic("SDD_STUB: implementation locked until Phase 5b") - Phase 4 (
P4— Interface & Storage Contracts,@td-auditor+@pm-planner): Locks OpenAPI 3.1, AsyncAPI 3.0, PostgreSQL DDL migrations, and the pre-implementation UAT Playbook before any business logic is written. - Phase 5 (
P5— Red Green Chaos Verification,@td-auditor): Executes 5a (Red Harness) against the panic stubs, 5b (Green Inside-Out Implementation) without mutating locked Phase 1–4 contracts, and 5c (Toxiproxy Chaos & Benchmark Cutover).
The entire plugin is open-source under the MIT license:
- GitHub Repository:
github.com/aruruka/tdpm-sdd-governance - Quick Install:
# Clone into your Antigravity plugin directory and sync dependencies 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
In the era of Agentic Development, the engineer's core value is shifting from writing module code by hand to acting as a Technical Director and Product Manager—defining boundaries, questioning assumptions, injecting chaos to verify reality, and encoding those hard-won lessons into deterministic guardrails.
Start with a real project. Build your standard with AI. Audit reality with chaos engineering. And turn your exploration process itself into the tool that builds the next system.
