Rust ddd
Curated Rust skill files for Claude Code: ownership, async, errors, types, architecture, DDD, and more
npx -y skills add dawidpereira/rust-skills --skill rust-dddAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
7.1 KB, as published. Nobody here has run it
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 |