rust-ddd
Skill0 starsChanged 8 months agoMITView on GitHub
Rust domain-driven design — aggregates, entities, value objects, domain events, repository traits, and use case organization. Use when modeling a rich business domain, deciding where business rules live, choosing between service structs and aggregate methods, separating domain models from database types, or structuring features with DDD building blocks. Also use when working with bounded contexts, applying the repository pattern, or designing domain error types in Rust.
In this repository16 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- rust-apiSkill
- rust-architectureSkill
- rust-asyncSkill
- rust-ci-deploySkill
- rust-databaseSkill
- rust-errorsSkill
- rust-macrosSkill
- rust-ownershipSkill
- rust-perfSkill
- rust-qualitySkill
- rust-serdeSkill
- rust-testsSkill
- rust-tracingSkill
- rust-typesSkill
- rust-unsafeSkill
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 48
Rust is not Java/C#: No DI container (wire deps explicitly with Arc<dyn Trait>).
- Very specific: line 69
Value objects are newtypes: Never pass raw String, i64, or Uuid for domain concepts.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Domain-Driven Design
Core Question
How should this business domain be modeled so that Rust's type system and ownership enforce the rules at compile time?
DDD in Rust is not a port of Java or C#. There is no DI
container, no mediator, no exception hierarchy. Rust's private
fields, &mut self, newtype pattern, and exhaustive match
give you compile-time enforcement that other languages achieve
only through runtime discipline.
Quick Decisions
| Situation | Reach For | Why |
| Wrapping a primitive with rules | Newtype value object | Validated at construction, zero-cost, type-safe |
| Identity matters, not just value equality | Entity with ID field | Compared by ID, not by all fields |
| Cluster of entities with invariants | Aggregate with private fields + command methods | &mut self = single writer, ownership = encapsulation |
| Something happened that other parts care about | Domain event enum | Decouples side effects, compiler forces exhaustive handling |
| Persisting/loading aggregates | Repository trait in feature, impl in infrastructure | Domain code stays framework-free |
| Orchestrating a use case | One file per use case (handler + DTOs) | Thin orchestration, no business logic in handler |
| Shared types across features (Money, IDs) | shared/models.rs | One source of truth, no feature coupling |
| Mapping domain errors to HTTP | shared/errors.rs with From impls | Domain stays framework-free |
| Feature-specific domain errors | thiserror enum per feature | Each feature owns its failure modes |
| Loading aggregate from DB | reconstitute() associated function | Bypasses validation for trusted data, no spurious events |
| Simple CRUD, no invariants | Standard VSA slice — skip DDD | DDD adds cost; use only when domain is rich |
| Use case needs caller identity or tenant scope | Pass RequestContext to handler and repo | Domain stays framework-free; context is a plain struct in shared/ |
Key Principles
- Rust is not Java/C#. No DI container (wire deps
explicitly with
Arc<dyn Trait>). No mediator (Axum extractors ARE DI, the Router IS the dispatcher). No exceptions (errors areResult<T, E>values). - Ownership IS encapsulation. Private fields on an
aggregate mean outside code cannot break invariants.
&mut selfon command methods means one writer at a time. The compiler enforces what Java achieves withsynchronized. - Enums are sum types, not labels. Rust enums are tagged
unions — each variant can carry different data. They are the
same construct as
OptionandResult: algebraic sum types with monadic combinators (.map(),.and_then(),?).OrderStatus,OrderEvent,OrderError— all enums. The compiler forces exhaustive handling viamatch. When state determines which operations are valid, consider making the aggregate itself an enum (typestate) so invalid transitions are compile errors, not runtimeResults. See references/building-blocks.md § "Enums Beyond Labels". - Value objects are newtypes. Never pass raw
String,i64, orUuidfor domain concepts.OrderId(Uuid),Quantity(u32),Money { amount, currency }— validated at construction, zero-cost at runtime. - Domain models are pure.
models.rs,events.rs,error.rs,repository.rshave zero imports from axum, sqlx, or any web/ORM framework. Infrastructure implements the traits. - Aggregates collect events. Command methods push events
to an internal
Vec. The use case handler callstake_events()after saving, then publishes them. The aggregate never knows about the event bus. reconstitute()loads from DB. Apub(crate)associated function that builds an aggregate without validation or event generation. Only infrastructure code calls it.- File-per-use-case replaces god services. Each use case is a free async function in its own file with only the dependencies it needs. No 500-line service struct.
Usage Scenarios
Scenario 1: "I'm modeling an order with line items, totals,
and status transitions"
→ Build an Order aggregate with private fields. Status
transitions are command methods (place(), cancel()) that
return Result and push domain events. Line items are entities
owned by the aggregate.
Scenario 2: "I have Money, Currency, Email used across
multiple features"
→ Put these in shared/models.rs as value objects with
validated constructors. Each feature imports them but owns its
own aggregates and business logic.
Scenario 3: "My service struct has 500 lines and 10 injected dependencies" → Split into file-per-use-case. Move business logic from service methods to aggregate command methods. Each use case handler takes only the 2-3 dependencies it needs.
Scenario 4: "I need to save an aggregate but domain code
shouldn't know about sqlx"
→ Define a repository trait in the feature (repository.rs).
Implement it in infrastructure/persistence/ with separate
DB row structs. Map DB rows to domain objects via
reconstitute().
Scenario 5: "My model.rs has 400 lines with aggregate,
entities, and value objects"
→ Promote to models.rs + models/ folder. Split into
models/order.rs, models/order_item.rs,
models/value_objects.rs. Imports from outside the feature
don't change.
Reference Index
| Reference | Read When |
| references/building-blocks.md | Implementing aggregates, entities, value objects, domain events, domain errors — full code examples |
| references/infrastructure.md | Repository traits and implementations, DB row mapping, use case files, error mapping, shared types, module wiring |
| references/anti-patterns.md | Reviewing DDD code for common mistakes — anemic models, god services, leaking DB types |
Cross-References
| When | Check |
| Project folder structure, slice layout, module conventions | rust-architecture → Quick Decisions |
| Cross-feature communication strategies | rust-architecture → references/cross-slice.md |
| Newtype patterns, parse-don't-validate, typestate | rust-types → Quick Decisions |
thiserror enum design, error propagation with ? | rust-errors → Quick Decisions |
| Async repository traits, tokio patterns | rust-async → Quick Decisions |
| Request context propagation, tenant-scoped repos | rust-architecture → references/request-context.md |
| Public API design, naming conventions | rust-api → Quick Decisions |
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

