Strategic Domain-Driven Design — Subdomain classification (Core/Supporting/Generic), Context Mapping (7 relationship patterns), Bounded Context design, and Event Storming. Language-agnostic. Use when designing system boundaries, decomposing a monolith, or planning a new multi-service architecture.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
Strategic DDD answers what to build and where to invest — before tactical DDD answers how to model it.
Use this sequence when starting strategic DDD on a codebase or new product area:
Output: timeline of events grouped into rough subdomain clusters.
For each cluster: Core / Supporting / Generic? If you can't agree, it's probably Supporting.
One context = one consistent ubiquitous language. Ask for each cluster:
Output: Bounded Context diagram with explicit names and owners.
For every pair of contexts that communicate:
| If... | Then... |
|---|---|
| New product, small team | Monolith with Bounded Context packages |
| Team > 8 engineers or independent deploy needed | Extract Core Domains to own services |
| Core Domain changes daily | Own service + own DB (no shared DB) |
| Supporting Domain, low change rate | Stay in monolith, own package |
Stop here before coding. The diagram is the deliverable, not the code.
Not all parts of the system deserve the same investment. Classify every subdomain before modeling it.
The competitive advantage — what makes your product different from competitors. Customers pay for this.
Prediction Market Platform:
Core → Market creation rules, resolution logic, payout calculation
Core → Risk engine, position limits, liquidity model
Necessary for the business to function, but not differentiating. Competitors have similar needs.
Prediction Market Platform:
Supporting → User profiles, KYC workflow, notification preferences
Supporting → Reporting dashboard, admin tools, market moderation
A solved problem. Every company needs it; no company differentiates on it.
Prediction Market Platform:
Generic → Authentication & authorization (Auth0, Keycloak)
Generic → Payment processing (Stripe, Adyen)
Generic → Email delivery (SendGrid, SES)
Generic → File storage (S3, GCS)
| Question | Core | Supporting | Generic |
|---|---|---|---|
| Unique competitive advantage? | Yes | No | No |
| Would a competitor's copy hurt us? | Severely | Slightly | Not at all |
| Off-the-shelf solution exists? | No | Rarely | Yes |
| DDD investment justified? | Always | Sometimes | Never |
| Team ownership | Senior, stable | Any | Ops/integrations |
A Bounded Context is an explicit boundary within which a domain model is defined and applicable. The same word can mean different things in different contexts — that is expected and correct.
@startuml
!include <C4/C4_Container>
System_Boundary(oc, "Order Context") {
Container(oc_cust, "Customer", "Value Object", "shipping address")
Container(oc_prod, "Product", "Value Object", "name, SKU, weight")
Container(oc_ord, "Order", "Aggregate Root", "lines, delivery date")
}
System_Boundary(pc, "Payment Context") {
Container(pc_cust, "Customer", "Value Object", "billing address + payment methods")
Container(pc_prod, "Product", "Value Object", "price, tax category")
Container(pc_inv, "Invoice", "Aggregate Root", "line items, VAT, due date")
}
note as N
"Customer" means something different
in each context. This is correct, not a bug.
end note
@enduml
Within a Bounded Context:
publish() — not activate() or setStatusToActive()Outside a Bounded Context:
| Situation | Recommendation |
|---|---|
| Starting a new product | Monolith with clear module boundaries = Bounded Contexts as packages |
| Growing team, independent deployments needed | Extract contexts to separate services |
| Core Domain under heavy development | Own service — isolated deployment, own DB |
| Supporting Domain, low change rate | Can stay in monolith or share service with similar contexts |
Don't extract too early. A well-structured monolith with clear Bounded Context boundaries is easier to split later than a poorly designed microservice mesh.
A Context Map documents how Bounded Contexts relate to each other. There are 7 canonical patterns:
Both contexts evolve together; teams coordinate closely. Changes in one require coordination with the other.
@startuml
!include <C4/C4_Context>
System_Boundary(b, "Same Team / Tightly Coordinated") {
System(oc, "Order Context")
System(fc, "Fulfillment Context")
}
Rel(oc, fc, "Partnership", "bidirectional coordination")
@enduml
When: Two contexts under the same team, or teams with high mutual trust and aligned roadmaps. Risk: Creates coupling — if teams diverge, migrate to Customer/Supplier.
Two contexts share a subset of the domain model (code). Both teams must agree before changing the shared part.
@startuml
!include <C4/C4_Context>
System(oc, "Order Context")
System(pc, "Payment Context")
System(sk, "Shared Kernel", "Money, Currency, OrderId types")
Rel(oc, sk, "uses")
Rel(pc, sk, "uses")
@enduml
When: Small, stable shared concepts (value objects, typed IDs) that would diverge badly if duplicated. Risk: Coordination overhead. Keep the kernel as small as possible. Anti-pattern: Sharing JPA entities or ORM models — share domain types only.
Upstream produces, downstream consumes. Downstream has influence over upstream's roadmap (it's a customer).
@startuml
!include <C4/C4_Context>
System(pc, "Payment Context", "Upstream / Supplier")
System(oc, "Order Context", "Downstream / Customer")
Rel_D(pc, oc, "Customer/Supplier", "downstream has negotiating power")
@enduml
When: One context depends on another, but the consumer has negotiating power. Downstream gets: A say in what the upstream exposes, SLAs, planned changes communicated early.
Upstream has no incentive to accommodate downstream. Downstream accepts the upstream model as-is — no translation.
@startuml
!include <C4/C4_Context>
System_Ext(sp, "Shipping Provider API", "Upstream — external, no negotiation possible")
System(oc, "Order Context", "Downstream / Conformist")
Rel_D(sp, oc, "Conformist", "downstream absorbs upstream model as-is")
@enduml
When: Integrating with a dominant external system (legacy ERP, government API) where you have zero influence. Cost: Downstream's model is polluted by upstream's concepts. Accept this consciously. Alternative: Add an ACL if pollution is too painful.
Downstream translates between upstream's model and its own domain model. Upstream's concepts never appear in downstream's domain.
@startuml
!include <C4/C4_Container>
System_Boundary(oc_b, "Order Context") {
Container(oc, "Domain Model", "Pure Domain", "clean, no external concepts")
Container(acl, "Anti-Corruption Layer", "Outbound Adapter", "translates external model to domain types")
}
System_Ext(pp, "Payment Provider API", "External model")
Rel_D(oc, acl, "calls")
Rel_D(acl, pp, "translates to/from")
@enduml
When:
Implementation: The ACL is an outbound adapter. It maps external DTOs/responses to your domain types.
// ACL in adapter/out/client/ — translates external model to domain model
class StripePaymentAdapter implements PaymentPort {
initiatePayment(order: Order, amount: Money): Promise<PaymentResult> {
// Translate: domain Order → Stripe PaymentIntent request
// Call Stripe API
// Translate: Stripe response → domain PaymentResult
}
}
Upstream publishes a well-documented, stable service protocol that multiple downstreams consume. Upstream invests in the protocol, not in custom integrations per consumer.
@startuml
!include <C4/C4_Context>
System(md, "Market Data Context", "Upstream / Open Host Service")
System(oc, "Order Context")
System(rc, "Reporting Context")
System(ac, "Analytics Context")
Rel_D(md, oc, "OHS", "stable, versioned API / event schema")
Rel_D(md, rc, "OHS", "stable, versioned API / event schema")
Rel_D(md, ac, "OHS", "stable, versioned API / event schema")
@enduml
When: One context serves many consumers. The upstream defines the protocol once; consumers conform to it. Combined with Published Language: The protocol is expressed in a shared, explicit format.
A well-documented, shared information exchange model (schema) that multiple contexts use. Not tied to one context's internal model.
@startuml
!include <C4/C4_Context>
System(pl, "AsyncAPI / Protobuf / JSON Schema", "Published Language — versioned schemas")
System(oc, "Order Context")
System(mc, "Market Context")
System(rc, "Reporting Context")
Rel_D(pl, oc, "consumed by")
Rel_D(pl, mc, "consumed by")
Rel_D(pl, rc, "consumed by")
@enduml
When: Event-driven systems where producer and consumer should be fully decoupled. Examples: Protobuf, AsyncAPI, Avro schemas, CloudEvents format. Combined with OHS: OHS defines the service, Published Language defines the data format.
| Pattern | Relationship | Translation needed? | Coupling |
|---|---|---|---|
| Partnership | Collaborative, symmetric | No | High |
| Shared Kernel | Shared code subset | No | Medium |
| Customer/Supplier | Upstream/downstream, negotiated | Optional | Medium |
| Conformist | Upstream dominant, no negotiation | No (absorbs upstream model) | Medium |
| Anti-Corruption Layer | Upstream hostile/incompatible | Yes (full translation) | Low |
| Open Host Service | Upstream publishes stable protocol | Protocol layer | Low |
| Published Language | Shared schema/format | Via schema | Very low |
Event Storming is a workshop technique for discovering domain boundaries collaboratively with domain experts.
Goal: Discover all domain events in the system. Identify hotspots and Bounded Contexts.
Materials: Unlimited wall space, sticky notes in 5 colors:
Process:
Output: A visual map of the entire domain + identified boundaries
Goal: Design the flow within one Bounded Context in detail.
Add to the board:
Result: A precise flow:
@startuml
:Actor\nsees Read Model;
:issues Command;
:Aggregate validates;
:emits Domain Event;
:Policy reacts;
@enduml
Goal: Define the actual aggregate boundaries and domain model for implementation.
For each aggregate identified:
Output: Aggregate definitions ready for tactical DDD implementation.
1. Identify subdomains
└─ Core / Supporting / Generic?
2. For Core subdomains:
└─ Run Event Storming to discover boundaries
└─ Define Bounded Contexts + Ubiquitous Language
└─ Apply tactical DDD (Aggregates, Value Objects, Domain Events)
3. For Supporting subdomains:
└─ Simple service with Active Record or CRUD is fine
└─ No need for full DDD
4. For Generic subdomains:
└─ Buy SaaS / use open source
└─ Wrap with Anti-Corruption Layer
5. Map relationships between contexts:
└─ Choose Context Map pattern for each pair
└─ Document in a Context Map diagram
@startuml
!include <C4/C4_Context>
System(oc, "Order Context")
System_Ext(stripe, "Stripe", "Payment — Generic subdomain")
System(ic, "Inventory Context")
System(rc, "Reporting Context")
System(ac, "Analytics Context")
System_Ext(auth, "Auth0", "Auth — Generic subdomain")
Rel(oc, stripe, "ACL")
Rel(ic, oc, "Customer/Supplier", "Order is downstream")
Rel(ic, rc, "OHS + Published Language")
Rel(ic, ac, "OHS + Published Language")
Rel(auth, oc, "Generic / Conformist")
Rel(auth, ic, "Generic / Conformist")
Rel(auth, rc, "Generic / Conformist")
@enduml
No explicit Bounded Contexts. Everything imports everything. The "context" is the entire system. Every new feature requires understanding the whole codebase.
Fix: Identify natural clusters of domain events (Event Storming), draw boundaries, enforce them with package/module structure or service boundaries.
Contexts exist on paper but communicate via shared databases, direct ORM queries across service boundaries, or God Objects referenced everywhere.
Fix: Each Bounded Context owns its data. Communication only via APIs or events. Never shared tables.
Treating the core competitive advantage as a commodity to be bought or outsourced.
Example: A fintech building a risk engine using a generic third-party scoring service — the scoring IS the product.
Applying full DDD tactical patterns (Aggregates, Domain Events, Event Sourcing) to a simple CRUD admin panel.
Fix: Match investment to subdomain type. Supporting domains can use Active Record, thin service layers, simple CRUD.
ddd-java (Java) and ddd-typescript (TypeScript)hexagonal-java, hexagonal-typescriptspringboot-patterns (Spring Events / Kafka section)