Skip to main content

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}