ddd-toolkit
English | 日本語
A Rust toolkit for developers who want to introduce Domain-Driven Design (DDD) contracts and derives into their application: express identity, value validation, aggregate event queues, and domain events without writing the corresponding trait implementations by hand. It complements your existing storage and runtime choices; it is not a framework that automatically implements persistence, transactions, or delivery guarantees.
Experimental, 0.x: APIs may change. See the existing compatibility and breaking-change policy. Feedback on APIs, documentation, and use cases is welcome via GitHub Issues.
Start with the four derives and quick start, continue with the order tutorial, then explore Advanced when you need additional contracts. Use the public contract map to find examples and contract tests by API name, including sync/async modes and feature requirements.
Package and toolchain
Domain-Driven Design traits and helpers, re-exported from
ddd-toolkit-core 0.4.5,
plus the four derives from
ddd-toolkit-macro 0.5.2.
All public items are available at ddd_toolkit::.
Tested with the latest stable Rust (edition 2024). No minimum supported Rust version (MSRV) is specified.
This repository is a Cargo workspace containing the facade, core, and macro crates. They remain separate crates.io packages, developed and tested together. See CONTRIBUTING.md and release instructions.
Installation
For the quick start, use the default features (derive is enabled):
[]
= "0.4"
For traits and helpers without derive macros:
[]
= { = "0.4", = false }
Derives work with the facade as the only toolkit dependency. Generated paths
prefer a direct ddd-toolkit-core dependency when present, otherwise they use
the facade's re-exports. Both core and facade dependencies may be renamed.
Facade 0.3.0 adopts core/macro 0.4.0 and contains breaking API changes from facade 0.2. See the migration guide below and CHANGELOG.md.
Four derives
| Derive | Start with |
|---|---|
ValueObject |
Values with equality and optional validation |
Entity |
Identity-based equality and hashing via #[entity(id)] |
AggregateRoot |
Pending domain events in a #[aggregate(events)] queue |
DomainEvent |
A domain event type implementing the event contract |
Quick start
After adding the dependency with derive enabled as shown above, save this as
src/main.rs and run cargo run:
;
ValueObject derives Debug, Clone, PartialEq, Eq, and Hash;
do not derive these again. Unvalidated structs use new; validated structs use
try_new. Named/multi-field structs and data-carrying enums are also supported.
Entity derives equality and hashing using only the #[entity(id)] field.
Next: runnable examples
New to DDD? Continue with the order tutorial: express an order's business rules in code, then save the aggregate and send a notification. The guide maps terminology to code and explains reading order. From a checkout, start with:
Then try CQRS for command/query handlers, Specification and
Policy (cargo run -p ddd-toolkit --example cqrs --features derive --locked).
Both use only ddd_toolkit:: APIs and check results with assertions.
For retries, event sourcing, and outbox examples, see Advanced.
Advanced
Use these contracts as needed; they are not prerequisites for the four derives:
Repository/AsyncRepository,DomainEventPublisher/AsyncDomainEventPublisher, anddispatch_events/dispatch_events_async.- CQRS command/query handlers and lifecycle contexts.
- Event stores, revision types, aggregate replay, and snapshots.
- Event consumption, projections, checkpoints, and transactional outbox contracts.
- Specifications, domain services/policies/factories, process managers, units of work, and event upcasters.
Secret<T>for redacted formatting and zeroization on drop.
Core defines contracts, not storage adapters, clock or ID generators, an async
runtime, or automatic transaction/delivery guarantees. Implement adapters in your
application. Async ports return boxed Send futures without choosing a runtime.
A typical Box::pin implementation allocates on the heap for a non-zero-sized
future, and polling dyn Future uses dynamic dispatch. Type erasure supports
dyn port usage, and explicit lifetime/Send bounds express borrowing and
thread-safety requirements. These are trade-offs, not measured performance
problems. Runtime independence does not allow !Send futures: they do not fit
the current signatures, even on a single-threaded executor. See the
core async contract for details.
Advanced examples
From a checkout, run these assertion-checked examples using only ddd_toolkit:: APIs:
| Example | Demonstrates | Command |
|---|---|---|
| Delivery retries (advanced) | Aggregate, repository, dispatch failure and duplicate retry | cargo run -p ddd-toolkit --example dispatch_retry --features derive --locked |
| Event sourcing | ExpectedRevision, upcasting, replay and snapshots | cargo run -p ddd-toolkit --example event_sourcing --features derive --locked |
| Reliable delivery | Async outbox, idempotent projection and checkpoints | cargo run -p ddd-toolkit --example reliable_delivery --features derive --locked |
All require derive (enabled by default); no extra dependencies or async runtime
are needed. Adapters are in-memory demonstrations, not durable storage. Retry may
duplicate delivery; atomic transactions, persistence and claim recovery belong to
production adapters. The async demo polls only immediately-ready futures, not I/O.
Secret values
Aggregate event publication
use ;
;
;
Repository persistence and event publication are separate. Repository::save
borrows the aggregate immutably and does not automatically drain pending events.
dispatch_events drains events on success; on publication failure it restores
all pulled events. Already published events are not rolled back, so retrying
may deliver duplicates. Application adapters define transaction guarantees.
Features
| Feature | Default | Enables |
|---|---|---|
derive |
yes | ValueObject, Entity, DomainEvent, AggregateRoot derives; no direct core dependency required |
serde |
no | Core's serde re-export and serde derive support for #[value_object(serde)]; independent of derive |
To serialize a value object, enable the facade's serde feature and opt in on
the type:
;
No direct serde dependency is required for these generated implementations.
Validated deserialization runs the validator and maps failure to
value object validation failed. Enums use externally tagged variants.
When combining serde and validation in one attribute, place the serde flag
last (#[value_object(validate = check, error = Error, serde)]): macro 0.4.1
rejects a flag followed by other options.
Single-field tuple enum variants use Serde's newtype format: Code(u32) emits
{"Code":7} and round-trips. A container field retains its own shape, so
Codes(Vec<u32>) emits {"Codes":[7,8]}. Unit, named, and multi-field tuple
variants retain their existing formats.
Migrating serialized enums from macro 0.4
Facade 0.4.0 / macro 0.5.0 intentionally change the output format, not in a patch
release. Older serializers emitted an extra tuple wrapper ({"Code":[7]}),
although their deserializers already expected the newtype format. Before loading
saved data, remove exactly one outer array layer for each affected single-field
tuple variant: {"Code":[7]} → {"Code":7}, or {"Codes":[[7,8]]} →
{"Codes":[7,8]}. Use your schema/version metadata to identify old records; do
not unwrap all arrays. There is no automatic legacy-format fallback because
container-valued fields can make the two representations ambiguous. Coordinate
producer/consumer upgrades or pin facade 0.3 / macro 0.4 until migration is ready.
The same newtype-vs-tuple change applies to other Serde formats; migrate them
using their format-specific APIs.
Macro 0.5.1 generates shorthand fields in named value-object constructors,
including validated constructors and named enum variants. No
clippy::redundant_field_names allowance is needed. See the
core/macro compatibility contract for supported
versions and serde feature requirements.
Secret<T> is available without a feature flag and is not automatically serialized.
Calling Secret::into_inner transfers zeroization responsibility to the caller.
Migrating from core/macro 0.3
- Replace
domain,port,adapter,mock, andapplicationmodule paths with root-level imports. Built-in adapters/test doubles andUseCaseare gone. - Replace
EntityId/EnumVoderives with suitableValueObjecttypes;Wrappedis replaced byValueObject'sas_inner/into_innermethods. - Replace
#[vo(validate = "function")]with#[value_object(validate = function, error = YourError)].ValidationErroris gone; choose your own error type. - Replace generated
TryFrom/From/Displayusage with constructors and explicit application implementations where needed. - Replace
SecretVo/#[vo(zeroize)]withSecret<T>. - Replace
record/take_eventswithrecord_event/pull_events. - Replace
Load/Save/DeletewithRepositoryorAsyncRepository, andEventDispatcherwith event publisher contracts.PortErroris gone; adapters choose their own error types. - Remove
chrono,uuid, andzeroizefeature requests. - Opt in to serde per value object; a facade-only dependency supports derives.
More details are in the API docs. The
facade-tests integration suite covers custom repositories,
async ports, multi-aggregate flows, publication failure/retry, derives, serde,
and secrets through the facade.
Changelog
See CHANGELOG.md for release history.
License
Licensed under either of Apache License, Version 2.0 or MIT license, at your option.