atomr_patterns/ddd/aggregate.rs
1//! [`AggregateRoot`] — the transactional consistency boundary.
2//!
3//! Layers DDD identity + invariants over
4//! [`atomr_persistence::Eventsourced`]. Every aggregate is event-sourced;
5//! `AggregateRoot` adds:
6//!
7//! * a typed [`AggregateRoot::Id`] (the identity DDD requires),
8//! * a default [`AggregateRoot::check_invariants`] hook for global
9//! post-apply checks the framework runs at strategic points
10//! (recommended: after every command's events are applied).
11//!
12//! The associated `Command` is required to implement
13//! [`crate::Command`] with a matching `AggregateId`, so the framework
14//! can route commands without dynamic dispatch.
15
16use std::hash::Hash;
17
18use atomr_persistence::Eventsourced;
19
20/// DDD aggregate root. One per consistency boundary; every command and
21/// every event passes through one of these.
22///
23/// **Note on bounds.** The matching constraints
24/// `<Self as Eventsourced>::Command: Command<AggregateId = Self::Id>`
25/// and `<Self as Eventsourced>::Event: DomainEvent` are *not* expressed
26/// as a supertrait `where`-clause here, because Rust's MSRV-stable
27/// trait machinery propagates such clauses awkwardly through every
28/// usage site. The patterns that actually consume these bounds (e.g.
29/// [`crate::cqrs::CqrsPattern`]) re-state them at their own builder /
30/// impl sites. Do implement [`crate::Command`] for your `Command`
31/// type and [`crate::DomainEvent`] for your `Event` type.
32pub trait AggregateRoot: Eventsourced {
33 /// The aggregate's identity type.
34 type Id: Clone + Eq + Hash + Send + Sync + 'static;
35
36 /// This instance's id.
37 fn aggregate_id(&self) -> &Self::Id;
38
39 /// Optional invariant check, run after applying a command's events
40 /// to the in-memory state. Returning `Err` causes the framework to
41 /// surface a [`crate::PatternError::Invariant`] *after* the events
42 /// were persisted — i.e. it's a post-condition, not a guard. Use it
43 /// to detect bugs in command handlers, not to gate writes (gate
44 /// writes by returning `Err` from `command_to_events` instead).
45 /// Default: always `Ok`.
46 fn check_invariants(_state: &Self::State) -> Result<(), Self::Error> {
47 Ok(())
48 }
49
50 /// Encode `State` for snapshot persistence. Returning `None`
51 /// disables snapshotting for this aggregate even if a snapshot
52 /// store is configured at the pattern level — recovery falls back
53 /// to journal replay only. Returning `Some(Err(_))` surfaces
54 /// [`crate::PatternError::Codec`].
55 fn encode_state(_state: &Self::State) -> Option<Result<Vec<u8>, String>> {
56 None
57 }
58
59 /// Decode a snapshot payload back into `State`. Must be the
60 /// inverse of [`Self::encode_state`]. Required iff `encode_state`
61 /// is implemented.
62 fn decode_state(_bytes: &[u8]) -> Result<Self::State, String> {
63 Err("decode_state not implemented".into())
64 }
65}