arc_core/aggregate.rs
1//! # Aggregate Module
2//!
3//! Core abstractions for domain aggregates and commands in event sourcing.
4//!
5//! ## Overview
6//!
7//! This module provides the traits needed to implement aggregates following the
8//! Event Sourcing and CQRS patterns. Aggregates are the fundamental building blocks
9//! that encapsulate business logic, enforce invariants, and produce events.
10//!
11//! ## Design Philosophy
12//!
13//! **Complexity is Opt-In**: You can use aggregates in two ways:
14//!
15//! 1. **Simple Path**: Define enums for commands/events, implement the trait
16//! 2. **Complex Path**: Add rich domain logic, validation, and business rules
17//!
18//! Both approaches use the same infrastructure and are first-class citizens.
19//!
20//! ## Core Concepts
21//!
22//! ### Commands
23//!
24//! Commands represent **intent** to change state. They are imperative (e.g., `CreateUser`,
25//! `UpdateProfile`) and can be rejected if business rules aren't satisfied.
26//!
27//! - Commands are validated before producing events
28//! - Commands may produce zero events (validation failure)
29//! - Commands may produce multiple events (complex operations)
30//! - Commands from one aggregate must be atomic
31//!
32//! ### Events
33//!
34//! Events represent **facts** about things that have happened. They are past tense
35//! (e.g., `UserCreated`, `ProfileUpdated`) and cannot be rejected once produced.
36//!
37//! - Events are immutable once written
38//! - Events are the source of truth
39//! - Events are used to reconstruct aggregate state
40//! - Events are published to event bus for subscribers
41//!
42//! ### Aggregates
43//!
44//! Aggregates are consistency boundaries that:
45//!
46//! - Encapsulate domain logic and business rules
47//! - Validate commands and produce events
48//! - Apply events to update internal state
49//! - Can be reconstructed from their event stream
50//! - Enforce invariants within their boundary
51//!
52//! ## Quick Start
53//!
54//! ### 1. Define Your Domain Events
55//!
56//! ```rust
57//! use serde::{Deserialize, Serialize};
58//!
59//! #[derive(Debug, Clone, Serialize, Deserialize)]
60//! pub enum UserEvent {
61//! UserCreated {
62//! id: String,
63//! name: String,
64//! email: String,
65//! },
66//! ProfileUpdated {
67//! name: String,
68//! },
69//! EmailChanged {
70//! email: String,
71//! },
72//! }
73//! ```
74//!
75//! ### 2. Define Your Commands
76//!
77//! ```rust
78//! use arc_core::aggregate::Command;
79//! use serde::{Deserialize, Serialize};
80//!
81//! #[derive(Debug, Clone, Serialize, Deserialize)]
82//! pub enum UserCommand {
83//! CreateUser {
84//! id: String,
85//! name: String,
86//! email: String,
87//! },
88//! UpdateProfile {
89//! id: String,
90//! name: String,
91//! },
92//! ChangeEmail {
93//! id: String,
94//! email: String,
95//! },
96//! }
97//!
98//! impl Command for UserCommand {
99//! fn aggregate_id(&self) -> &str {
100//! match self {
101//! UserCommand::CreateUser { id, .. } => id,
102//! UserCommand::UpdateProfile { id, .. } => id,
103//! UserCommand::ChangeEmail { id, .. } => id,
104//! }
105//! }
106//! }
107//! ```
108//!
109//! ### 3. Define Your Aggregate
110//!
111//! ```rust
112//! use arc_core::aggregate::Aggregate;
113//! use arc_core::event::{Event, NewEvent};
114//! use thiserror::Error;
115//!
116//! # use serde::{Deserialize, Serialize};
117//! # use arc_core::aggregate::Command;
118//! #
119//! # #[derive(Debug, Clone, Serialize, Deserialize)]
120//! # pub enum UserEvent {
121//! # UserCreated { id: String, name: String, email: String },
122//! # ProfileUpdated { name: String },
123//! # EmailChanged { email: String },
124//! # }
125//! #
126//! # #[derive(Debug, Clone, Serialize, Deserialize)]
127//! # pub enum UserCommand {
128//! # CreateUser { id: String, name: String, email: String },
129//! # UpdateProfile { id: String, name: String },
130//! # ChangeEmail { id: String, email: String },
131//! # }
132//! #
133//! # impl Command for UserCommand {
134//! # fn aggregate_id(&self) -> &str {
135//! # match self {
136//! # UserCommand::CreateUser { id, .. } => id,
137//! # UserCommand::UpdateProfile { id, .. } => id,
138//! # UserCommand::ChangeEmail { id, .. } => id,
139//! # }
140//! # }
141//! # }
142//! #
143//! #[derive(Debug, Error)]
144//! pub enum UserError {
145//! #[error("User already exists")]
146//! AlreadyExists,
147//! #[error("User not found")]
148//! NotFound,
149//! #[error("Invalid email format")]
150//! InvalidEmail,
151//! }
152//!
153//! #[derive(Default)]
154//! pub struct UserAggregate {
155//! id: Option<String>,
156//! name: Option<String>,
157//! email: Option<String>,
158//! version: i64,
159//! created: bool,
160//! }
161//!
162//! #[async_trait::async_trait]
163//! impl Aggregate for UserAggregate {
164//! type Command = UserCommand;
165//! type Event = UserEvent;
166//! type Error = UserError;
167//!
168//! fn aggregate_type() -> &'static str {
169//! "User"
170//! }
171//!
172//! fn version(&self) -> i64 {
173//! self.version
174//! }
175//!
176//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
177//! match command {
178//! UserCommand::CreateUser { id, name, email } => {
179//! // Enforce invariant: user cannot be created twice
180//! if self.created {
181//! return Err(UserError::AlreadyExists);
182//! }
183//!
184//! // Validate email
185//! if !email.contains('@') {
186//! return Err(UserError::InvalidEmail);
187//! }
188//!
189//! // Produce event
190//! Ok(vec![Event::new(NewEvent {
191//! aggregate_type: "User",
192//! aggregate_id: &id,
193//! sequence: self.version + 1,
194//! event_type: "UserCreated",
195//! payload: serde_json::json!({
196//! "id": id,
197//! "name": name,
198//! "email": email,
199//! }),
200//! })])
201//! }
202//! UserCommand::UpdateProfile { id, name } => {
203//! if !self.created {
204//! return Err(UserError::NotFound);
205//! }
206//!
207//! Ok(vec![Event::new(NewEvent {
208//! aggregate_type: "User",
209//! aggregate_id: &id,
210//! sequence: self.version + 1,
211//! event_type: "ProfileUpdated",
212//! payload: serde_json::json!({ "name": name }),
213//! })])
214//! }
215//! UserCommand::ChangeEmail { id, email } => {
216//! if !self.created {
217//! return Err(UserError::NotFound);
218//! }
219//!
220//! if !email.contains('@') {
221//! return Err(UserError::InvalidEmail);
222//! }
223//!
224//! Ok(vec![Event::new(NewEvent {
225//! aggregate_type: "User",
226//! aggregate_id: &id,
227//! sequence: self.version + 1,
228//! event_type: "EmailChanged",
229//! payload: serde_json::json!({ "email": email }),
230//! })])
231//! }
232//! }
233//! }
234//!
235//! fn apply(&mut self, event: &Event) {
236//! self.version = event.sequence;
237//!
238//! match event.event_type.as_str() {
239//! "UserCreated" => {
240//! self.id = Some(event.payload["id"].as_str().unwrap().to_string());
241//! self.name = Some(event.payload["name"].as_str().unwrap().to_string());
242//! self.email = Some(event.payload["email"].as_str().unwrap().to_string());
243//! self.created = true;
244//! }
245//! "ProfileUpdated" => {
246//! self.name = Some(event.payload["name"].as_str().unwrap().to_string());
247//! }
248//! "EmailChanged" => {
249//! self.email = Some(event.payload["email"].as_str().unwrap().to_string());
250//! }
251//! _ => {}
252//! }
253//! }
254//! }
255//! ```
256//!
257//! ### 4. Use Your Aggregate
258//!
259//! ```rust,no_run
260//! # use arc_core::aggregate::Aggregate;
261//! # use arc_core::event::{Event, NewEvent};
262//! # use thiserror::Error;
263//! # use serde::{Deserialize, Serialize};
264//! # use arc_core::aggregate::Command;
265//! #
266//! # #[derive(Debug, Clone, Serialize, Deserialize)]
267//! # pub enum UserEvent {
268//! # UserCreated { id: String, name: String, email: String },
269//! # ProfileUpdated { name: String },
270//! # EmailChanged { email: String },
271//! # }
272//! #
273//! # #[derive(Debug, Clone, Serialize, Deserialize)]
274//! # pub enum UserCommand {
275//! # CreateUser { id: String, name: String, email: String },
276//! # UpdateProfile { id: String, name: String },
277//! # ChangeEmail { id: String, email: String },
278//! # }
279//! #
280//! # impl Command for UserCommand {
281//! # fn aggregate_id(&self) -> &str {
282//! # match self {
283//! # UserCommand::CreateUser { id, .. } => id,
284//! # UserCommand::UpdateProfile { id, .. } => id,
285//! # UserCommand::ChangeEmail { id, .. } => id,
286//! # }
287//! # }
288//! # }
289//! #
290//! # #[derive(Debug, Error)]
291//! # pub enum UserError {
292//! # #[error("User already exists")]
293//! # AlreadyExists,
294//! # #[error("User not found")]
295//! # NotFound,
296//! # #[error("Invalid email format")]
297//! # InvalidEmail,
298//! # }
299//! #
300//! # #[derive(Default)]
301//! # pub struct UserAggregate {
302//! # id: Option<String>,
303//! # name: Option<String>,
304//! # email: Option<String>,
305//! # version: i64,
306//! # created: bool,
307//! # }
308//! #
309//! # #[async_trait::async_trait]
310//! # impl Aggregate for UserAggregate {
311//! # type Command = UserCommand;
312//! # type Event = UserEvent;
313//! # type Error = UserError;
314//! # fn aggregate_type() -> &'static str { "User" }
315//! # fn version(&self) -> i64 { self.version }
316//! # async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
317//! # Ok(vec![])
318//! # }
319//! # fn apply(&mut self, event: &Event) {}
320//! # }
321//! #
322//! # async fn example() {
323//! // Create a command
324//! let command = UserCommand::CreateUser {
325//! id: "user-123".to_string(),
326//! name: "Alice".to_string(),
327//! email: "alice@example.com".to_string(),
328//! };
329//!
330//! // Create aggregate (typically loaded from event store)
331//! let aggregate = UserAggregate::default();
332//!
333//! // Handle command
334//! let events = aggregate.handle(command).await.unwrap();
335//!
336//! // Events would be persisted to event store and published to event bus
337//! assert_eq!(events.len(), 1);
338//! assert_eq!(events[0].event_type, "UserCreated");
339//! # }
340//! ```
341//!
342//! ## Testing Your Aggregates
343//!
344//! Aggregates are easy to test because they're pure functions (commands → events → state).
345//!
346//! ```rust
347//! # use arc_core::aggregate::Aggregate;
348//! # use arc_core::event::{Event, NewEvent};
349//! # use thiserror::Error;
350//! # use serde::{Deserialize, Serialize};
351//! # use arc_core::aggregate::Command;
352//! #
353//! # #[derive(Debug, Clone, Serialize, Deserialize)]
354//! # pub enum UserEvent {
355//! # UserCreated { id: String, name: String, email: String },
356//! # }
357//! #
358//! # #[derive(Debug, Clone, Serialize, Deserialize)]
359//! # pub enum UserCommand {
360//! # CreateUser { id: String, name: String, email: String },
361//! # }
362//! #
363//! # impl Command for UserCommand {
364//! # fn aggregate_id(&self) -> &str {
365//! # match self {
366//! # UserCommand::CreateUser { id, .. } => id,
367//! # }
368//! # }
369//! # }
370//! #
371//! # #[derive(Debug, Error)]
372//! # pub enum UserError {
373//! # #[error("User already exists")]
374//! # AlreadyExists,
375//! # #[error("Invalid email format")]
376//! # InvalidEmail,
377//! # }
378//! #
379//! # #[derive(Default)]
380//! # pub struct UserAggregate {
381//! # id: Option<String>,
382//! # version: i64,
383//! # created: bool,
384//! # }
385//! #
386//! # #[async_trait::async_trait]
387//! # impl Aggregate for UserAggregate {
388//! # type Command = UserCommand;
389//! # type Event = UserEvent;
390//! # type Error = UserError;
391//! # fn aggregate_type() -> &'static str { "User" }
392//! # fn version(&self) -> i64 { self.version }
393//! # async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
394//! # match command {
395//! # UserCommand::CreateUser { id, name, email } => {
396//! # if self.created {
397//! # return Err(UserError::AlreadyExists);
398//! # }
399//! # if !email.contains('@') {
400//! # return Err(UserError::InvalidEmail);
401//! # }
402//! # Ok(vec![Event::new(NewEvent {
403//! # aggregate_type: "User",
404//! # aggregate_id: &id,
405//! # sequence: self.version + 1,
406//! # event_type: "UserCreated",
407//! # payload: serde_json::json!({ "id": id, "name": name, "email": email }),
408//! # })])
409//! # }
410//! # }
411//! # }
412//! # fn apply(&mut self, event: &Event) {
413//! # self.version = event.sequence;
414//! # if event.event_type == "UserCreated" {
415//! # self.id = Some(event.aggregate_id.clone());
416//! # self.created = true;
417//! # }
418//! # }
419//! # }
420//! #
421//! #[tokio::test]
422//! async fn test_user_creation() {
423//! // Given: A new user aggregate
424//! let aggregate = UserAggregate::default();
425//!
426//! // When: Creating a user
427//! let command = UserCommand::CreateUser {
428//! id: "user-123".to_string(),
429//! name: "Alice".to_string(),
430//! email: "alice@example.com".to_string(),
431//! };
432//!
433//! let events = aggregate.handle(command).await.unwrap();
434//!
435//! // Then: UserCreated event is produced
436//! assert_eq!(events.len(), 1);
437//! assert_eq!(events[0].event_type, "UserCreated");
438//! assert_eq!(events[0].aggregate_id, "user-123");
439//! }
440//!
441//! #[tokio::test]
442//! async fn test_invalid_email() {
443//! // Given: A new user aggregate
444//! let aggregate = UserAggregate::default();
445//!
446//! // When: Creating a user with invalid email
447//! let command = UserCommand::CreateUser {
448//! id: "user-456".to_string(),
449//! name: "Bob".to_string(),
450//! email: "not-an-email".to_string(), // Invalid!
451//! };
452//!
453//! let result = aggregate.handle(command).await;
454//!
455//! // Then: Error is returned
456//! assert!(result.is_err());
457//! }
458//!
459//! #[tokio::test]
460//! async fn test_cannot_create_twice() {
461//! // Given: An existing user (reconstructed from events)
462//! let mut aggregate = UserAggregate::default();
463//! let event = Event::new(NewEvent {
464//! aggregate_type: "User",
465//! aggregate_id: "user-789",
466//! sequence: 1,
467//! event_type: "UserCreated",
468//! payload: serde_json::json!({
469//! "id": "user-789",
470//! "name": "Charlie",
471//! "email": "charlie@example.com"
472//! }),
473//! });
474//! aggregate.apply(&event);
475//!
476//! // When: Trying to create the user again
477//! let command = UserCommand::CreateUser {
478//! id: "user-789".to_string(),
479//! name: "Charlie".to_string(),
480//! email: "charlie@example.com".to_string(),
481//! };
482//!
483//! let result = aggregate.handle(command).await;
484//!
485//! // Then: Error is returned
486//! assert!(result.is_err());
487//! }
488//! ```
489//!
490//! ## Advanced Patterns
491//!
492//! ### Multiple Events from One Command
493//!
494//! Sometimes a command should produce multiple events atomically:
495//!
496//! ```rust,ignore
497//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
498//! match command {
499//! OrderCommand::PlaceOrder { order_id, items } => {
500//! // Validate stock
501//! // ...
502//!
503//! // Produce multiple events
504//! Ok(vec![
505//! Event::new(NewEvent {
506//! aggregate_type: "Order",
507//! aggregate_id: &order_id,
508//! sequence: self.version + 1,
509//! event_type: "OrderPlaced",
510//! payload: ...,
511//! }),
512//! Event::new(NewEvent {
513//! aggregate_type: "Order",
514//! aggregate_id: &order_id,
515//! sequence: self.version + 2,
516//! event_type: "InventoryReserved",
517//! payload: ...,
518//! }),
519//! Event::new(NewEvent {
520//! aggregate_type: "Order",
521//! aggregate_id: &order_id,
522//! sequence: self.version + 3,
523//! event_type: "PaymentRequested",
524//! payload: ...,
525//! }),
526//! ])
527//! }
528//! }
529//! }
530//! ```
531//!
532//! ### Conditional Events
533//!
534//! Commands may produce zero events if preconditions aren't met:
535//!
536//! ```rust,ignore
537//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
538//! match command {
539//! UserCommand::MarkAsActive { id } => {
540//! // If already active, no event needed
541//! if self.is_active {
542//! return Ok(vec![]);
543//! }
544//!
545//! Ok(vec![Event::new(NewEvent {
546//! aggregate_type: "User",
547//! aggregate_id: &id,
548//! sequence: self.version + 1,
549//! event_type: "UserActivated",
550//! payload: ...,
551//! })])
552//! }
553//! }
554//! }
555//! ```
556//!
557//! ### Complex Validation
558//!
559//! Use the aggregate state to enforce complex business rules:
560//!
561//! ```rust,ignore
562//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
563//! match command {
564//! AccountCommand::Withdraw { amount } => {
565//! // Check balance
566//! if self.balance < amount {
567//! return Err(AccountError::InsufficientFunds);
568//! }
569//!
570//! // Check withdrawal limit
571//! if self.daily_withdrawals + amount > self.daily_limit {
572//! return Err(AccountError::DailyLimitExceeded);
573//! }
574//!
575//! Ok(vec![Event::new(NewEvent {
576//! aggregate_type: "Account",
577//! aggregate_id: &self.id,
578//! sequence: self.version + 1,
579//! event_type: "Withdrawn",
580//! payload: ...,
581//! })])
582//! }
583//! }
584//! }
585//! ```
586
587use crate::event::Event;
588#[cfg(test)]
589use crate::event::NewEvent;
590use async_trait::async_trait;
591use std::error::Error;
592
593/// Trait for commands that can be dispatched to aggregates.
594///
595/// Commands represent intent to change aggregate state. They are imperative
596/// (e.g., `CreateUser`, `UpdateProfile`) and can be rejected if business rules
597/// aren't satisfied.
598///
599/// # Design Principles
600///
601/// - **Imperative naming**: Use verbs (Create, Update, Delete, Activate)
602/// - **Validation happens in aggregates**: Commands may be rejected
603/// - **Identify target**: Commands must know which aggregate instance to operate on
604/// - **Serializable**: Commands should be serializable for command sourcing
605///
606/// # Example
607///
608/// ```rust
609/// use arc_core::aggregate::Command;
610/// use serde::{Deserialize, Serialize};
611///
612/// #[derive(Debug, Clone, Serialize, Deserialize)]
613/// pub enum UserCommand {
614/// CreateUser { id: String, name: String, email: String },
615/// UpdateProfile { id: String, name: String },
616/// DeleteUser { id: String },
617/// }
618///
619/// impl Command for UserCommand {
620/// fn aggregate_id(&self) -> &str {
621/// match self {
622/// UserCommand::CreateUser { id, .. } => id,
623/// UserCommand::UpdateProfile { id, .. } => id,
624/// UserCommand::DeleteUser { id } => id,
625/// }
626/// }
627/// }
628/// ```
629pub trait Command: Send + Sync {
630 /// Get the aggregate instance ID this command targets.
631 ///
632 /// The command bus uses this to load the correct aggregate instance
633 /// from the event store.
634 ///
635 /// # Example
636 ///
637 /// ```rust
638 /// # use arc_core::aggregate::Command;
639 /// # use serde::{Deserialize, Serialize};
640 /// #
641 /// # #[derive(Debug, Clone, Serialize, Deserialize)]
642 /// # pub enum UserCommand {
643 /// # CreateUser { id: String, name: String },
644 /// # }
645 /// #
646 /// # impl Command for UserCommand {
647 /// # fn aggregate_id(&self) -> &str {
648 /// # match self {
649 /// # UserCommand::CreateUser { id, .. } => id,
650 /// # }
651 /// # }
652 /// # }
653 /// #
654 /// let command = UserCommand::CreateUser {
655 /// id: "user-123".to_string(),
656 /// name: "Alice".to_string(),
657 /// };
658 ///
659 /// assert_eq!(command.aggregate_id(), "user-123");
660 /// ```
661 fn aggregate_id(&self) -> &str;
662}
663
664/// Trait for domain aggregates in event sourcing.
665///
666/// Aggregates are consistency boundaries that encapsulate business logic,
667/// enforce invariants, and produce events in response to commands.
668///
669/// # Lifecycle
670///
671/// 1. **Load**: Reconstruct aggregate from event stream using `from_events()`
672/// 2. **Command**: Handle command with `handle()` to produce new events
673/// 3. **Store**: Persist events to event store (done by command bus)
674/// 4. **Apply**: Apply new events to update aggregate state using `apply()`
675/// 5. **Publish**: Publish events to event bus (done by command bus)
676///
677/// # Design Principles
678///
679/// - **State is private**: Aggregate state should not be exposed outside
680/// - **Commands produce events**: `handle()` validates and produces events
681/// - **Events update state**: `apply()` updates internal state deterministically
682/// - **Pure functions**: `handle()` has no side effects (no I/O, no mutations)
683/// - **Deterministic apply**: Same events always produce same state
684/// - **Default implementation**: Aggregate must implement `Default` for initial state
685///
686/// # Type Parameters
687///
688/// - `Command`: The command type this aggregate handles (must implement `Command`)
689/// - `Event`: The domain event type (typically an enum, must be serializable)
690/// - `Error`: The error type for validation failures (must implement `std::error::Error`)
691///
692/// # Example
693///
694/// See the module-level documentation for a complete example.
695#[async_trait]
696pub trait Aggregate: Send + Sync + Default {
697 /// The command type this aggregate handles
698 type Command: Command;
699
700 /// The domain event type (your custom enum)
701 type Event;
702
703 /// The error type for validation failures
704 type Error: Error + Send + Sync + 'static;
705
706 /// Get the aggregate type name (e.g., "User", "Order", "Account").
707 ///
708 /// This is used for event metadata and debugging. Should be a static string
709 /// that uniquely identifies this aggregate type in your domain.
710 ///
711 /// # Example
712 ///
713 /// ```rust
714 /// # use arc_core::aggregate::{Aggregate, Command};
715 /// # use arc_core::event::{Event, NewEvent};
716 /// # use thiserror::Error;
717 /// #
718 /// # #[derive(Debug)]
719 /// # struct DummyCommand;
720 /// # impl Command for DummyCommand {
721 /// # fn aggregate_id(&self) -> &str { "dummy" }
722 /// # }
723 /// #
724 /// # #[derive(Debug, Error)]
725 /// # #[error("dummy error")]
726 /// # struct DummyError;
727 /// #
728 /// # #[derive(Default)]
729 /// # struct UserAggregate;
730 /// #
731 /// # #[async_trait::async_trait]
732 /// # impl Aggregate for UserAggregate {
733 /// # type Command = DummyCommand;
734 /// # type Event = ();
735 /// # type Error = DummyError;
736 /// #
737 /// fn aggregate_type() -> &'static str {
738 /// "User"
739 /// }
740 /// #
741 /// # fn version(&self) -> i64 { 0 }
742 /// # async fn handle(&self, _: Self::Command) -> Result<Vec<Event>, Self::Error> { Ok(vec![]) }
743 /// # fn apply(&mut self, _: &Event) {}
744 /// # }
745 /// #
746 /// assert_eq!(UserAggregate::aggregate_type(), "User");
747 /// ```
748 fn aggregate_type() -> &'static str;
749
750 /// Get the current version (sequence number) of this aggregate.
751 ///
752 /// The version represents how many events have been applied to this aggregate.
753 /// It starts at 0 for a new aggregate and increments with each event.
754 ///
755 /// This is used for optimistic concurrency control - the event store checks
756 /// that the version hasn't changed since the aggregate was loaded.
757 ///
758 /// # Example
759 ///
760 /// ```rust
761 /// # use arc_core::aggregate::{Aggregate, Command};
762 /// # use arc_core::event::{Event, NewEvent};
763 /// # use thiserror::Error;
764 /// #
765 /// # #[derive(Debug)]
766 /// # struct DummyCommand;
767 /// # impl Command for DummyCommand {
768 /// # fn aggregate_id(&self) -> &str { "dummy" }
769 /// # }
770 /// #
771 /// # #[derive(Debug, Error)]
772 /// # #[error("dummy error")]
773 /// # struct DummyError;
774 /// #
775 /// # #[derive(Default)]
776 /// # struct UserAggregate { version: i64 }
777 /// #
778 /// # #[async_trait::async_trait]
779 /// # impl Aggregate for UserAggregate {
780 /// # type Command = DummyCommand;
781 /// # type Event = ();
782 /// # type Error = DummyError;
783 /// # fn aggregate_type() -> &'static str { "User" }
784 /// #
785 /// fn version(&self) -> i64 {
786 /// self.version
787 /// }
788 /// #
789 /// # async fn handle(&self, _: Self::Command) -> Result<Vec<Event>, Self::Error> { Ok(vec![]) }
790 /// # fn apply(&mut self, _: &Event) {}
791 /// # }
792 /// #
793 /// let aggregate = UserAggregate::default();
794 /// assert_eq!(aggregate.version(), 0); // New aggregate
795 /// ```
796 fn version(&self) -> i64;
797
798 /// Handle a command and produce events.
799 ///
800 /// This is where your business logic lives. The method should:
801 ///
802 /// 1. Inspect current state (`self`) to make decisions
803 /// 2. Validate the command against business rules
804 /// 3. Return error if validation fails
805 /// 4. Produce one or more events if validation succeeds
806 /// 5. Return empty vec if command has no effect
807 ///
808 /// **Important**: This method should have no side effects:
809 /// - Don't write to databases
810 /// - Don't call external APIs
811 /// - Don't mutate state
812 ///
813 /// Side effects happen in projections and event handlers.
814 ///
815 /// # Arguments
816 ///
817 /// - `command`: The command to handle
818 ///
819 /// # Returns
820 ///
821 /// - `Ok(Vec<Event>)`: One or more events to persist and publish
822 /// - `Err(Self::Error)`: Validation or business rule failure
823 ///
824 /// # Example
825 ///
826 /// ```rust,ignore
827 /// async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
828 /// match command {
829 /// UserCommand::CreateUser { id, name, email } => {
830 /// // Check invariant
831 /// if self.created {
832 /// return Err(UserError::AlreadyExists);
833 /// }
834 ///
835 /// // Validate input
836 /// if !email.contains('@') {
837 /// return Err(UserError::InvalidEmail);
838 /// }
839 ///
840 /// // Produce event
841 /// Ok(vec![Event::new(NewEvent {
842 /// aggregate_type: "User",
843 /// aggregate_id: &id,
844 /// sequence: self.version + 1,
845 /// event_type: "UserCreated",
846 /// payload: ...,
847 /// })])
848 /// }
849 /// }
850 /// }
851 /// ```
852 async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error>;
853
854 /// Apply an event to update aggregate state.
855 ///
856 /// This method must be **deterministic** and **side-effect free**:
857 /// - Same events always produce same state
858 /// - No I/O operations
859 /// - No randomness
860 /// - No external dependencies
861 ///
862 /// The event is already persisted when this is called. Your job is to
863 /// update the aggregate's internal state to reflect the event.
864 ///
865 /// # Arguments
866 ///
867 /// - `event`: The event to apply (already persisted)
868 ///
869 /// # Example
870 ///
871 /// ```rust,ignore
872 /// fn apply(&mut self, event: &Event) {
873 /// self.version = event.sequence;
874 ///
875 /// match event.event_type.as_str() {
876 /// "UserCreated" => {
877 /// self.id = Some(event.payload["id"].as_str().unwrap().to_string());
878 /// self.name = Some(event.payload["name"].as_str().unwrap().to_string());
879 /// self.created = true;
880 /// }
881 /// "ProfileUpdated" => {
882 /// self.name = Some(event.payload["name"].as_str().unwrap().to_string());
883 /// }
884 /// _ => {} // Unknown event types are ignored
885 /// }
886 /// }
887 /// ```
888 fn apply(&mut self, event: &Event);
889
890 /// Reconstruct aggregate from its event stream.
891 ///
892 /// This method has a default implementation that:
893 /// 1. Creates a new aggregate instance using `Default::default()`
894 /// 2. Applies each event in order using `apply()`
895 /// 3. Returns the reconstructed aggregate
896 ///
897 /// You typically don't need to override this unless you have special requirements.
898 ///
899 /// # Arguments
900 ///
901 /// - `events`: The complete event stream for this aggregate
902 ///
903 /// # Returns
904 ///
905 /// A fully reconstructed aggregate with all events applied
906 ///
907 /// # Example
908 ///
909 /// ```rust
910 /// # use arc_core::aggregate::{Aggregate, Command};
911 /// # use arc_core::event::{Event, NewEvent};
912 /// # use thiserror::Error;
913 /// #
914 /// # #[derive(Debug)]
915 /// # struct DummyCommand;
916 /// # impl Command for DummyCommand {
917 /// # fn aggregate_id(&self) -> &str { "dummy" }
918 /// # }
919 /// #
920 /// # #[derive(Debug, Error)]
921 /// # #[error("dummy error")]
922 /// # struct DummyError;
923 /// #
924 /// # #[derive(Default)]
925 /// # struct UserAggregate {
926 /// # id: Option<String>,
927 /// # version: i64,
928 /// # created: bool,
929 /// # }
930 /// #
931 /// # #[async_trait::async_trait]
932 /// # impl Aggregate for UserAggregate {
933 /// # type Command = DummyCommand;
934 /// # type Event = ();
935 /// # type Error = DummyError;
936 /// # fn aggregate_type() -> &'static str { "User" }
937 /// # fn version(&self) -> i64 { self.version }
938 /// # async fn handle(&self, _: Self::Command) -> Result<Vec<Event>, Self::Error> { Ok(vec![]) }
939 /// #
940 /// # fn apply(&mut self, event: &Event) {
941 /// # self.version = event.sequence;
942 /// # if event.event_type == "UserCreated" {
943 /// # self.id = Some(event.aggregate_id.clone());
944 /// # self.created = true;
945 /// # }
946 /// # }
947 /// # }
948 /// #
949 /// // Load events from event store
950 /// let events = vec![
951 /// Event::new(NewEvent {
952 /// aggregate_type: "User",
953 /// aggregate_id: "user-123",
954 /// sequence: 1,
955 /// event_type: "UserCreated",
956 /// payload: serde_json::json!({"id": "user-123"}),
957 /// }),
958 /// Event::new(NewEvent {
959 /// aggregate_type: "User",
960 /// aggregate_id: "user-123",
961 /// sequence: 2,
962 /// event_type: "ProfileUpdated",
963 /// payload: serde_json::json!({"name": "Alice"}),
964 /// }),
965 /// ];
966 ///
967 /// // Reconstruct aggregate
968 /// let aggregate = UserAggregate::from_events(events);
969 ///
970 /// assert_eq!(aggregate.version(), 2);
971 /// assert!(aggregate.created);
972 /// ```
973 fn from_events(events: Vec<Event>) -> Self {
974 let mut aggregate = Self::default();
975 for event in events {
976 aggregate.apply(&event);
977 }
978 aggregate
979 }
980
981 /// Serialize current state for snapshotting.
982 ///
983 /// Default returns `None`: an aggregate opts out and is always reconstructed
984 /// by replaying its stream. Override to return `Some(state)` — typically via
985 /// `serde_json::to_value(self)` — once the aggregate is `Serialize`.
986 fn to_snapshot(&self) -> Option<serde_json::Value> {
987 None
988 }
989
990 /// Reconstruct an aggregate from a previously snapshotted state.
991 ///
992 /// Default returns `None` so a loader falls back to stream replay. Override
993 /// to deserialize the value produced by `to_snapshot`. `Option` (rather than
994 /// `Result`) keeps the contract simple: a `None` — whether opted out or a
995 /// failed decode — is always handled by replaying from sequence 0.
996 fn from_snapshot(state: serde_json::Value) -> Option<Self>
997 where
998 Self: Sized,
999 {
1000 let _ = state;
1001 None
1002 }
1003}
1004
1005#[cfg(test)]
1006mod tests {
1007 use super::*;
1008 use serde::{Deserialize, Serialize};
1009 use thiserror::Error;
1010
1011 // Test domain: Simple counter aggregate
1012 #[derive(Debug, Clone, Serialize, Deserialize)]
1013 enum CounterCommand {
1014 Create { id: String },
1015 Increment { id: String, amount: i32 },
1016 Decrement { id: String, amount: i32 },
1017 }
1018
1019 impl Command for CounterCommand {
1020 fn aggregate_id(&self) -> &str {
1021 match self {
1022 CounterCommand::Create { id } => id,
1023 CounterCommand::Increment { id, .. } => id,
1024 CounterCommand::Decrement { id, .. } => id,
1025 }
1026 }
1027 }
1028
1029 #[derive(Debug, Clone, Serialize, Deserialize)]
1030 enum CounterEvent {
1031 Created { id: String },
1032 Incremented { amount: i32 },
1033 Decremented { amount: i32 },
1034 }
1035
1036 #[derive(Debug, Error)]
1037 enum CounterError {
1038 #[error("Counter already exists")]
1039 AlreadyExists,
1040 #[error("Counter not found")]
1041 NotFound,
1042 #[error("Amount must be positive")]
1043 InvalidAmount,
1044 #[error("Counter would go negative")]
1045 WouldGoNegative,
1046 }
1047
1048 #[derive(Default)]
1049 struct CounterAggregate {
1050 id: Option<String>,
1051 value: i32,
1052 version: i64,
1053 created: bool,
1054 }
1055
1056 #[async_trait]
1057 impl Aggregate for CounterAggregate {
1058 type Command = CounterCommand;
1059 type Event = CounterEvent;
1060 type Error = CounterError;
1061
1062 fn aggregate_type() -> &'static str {
1063 "Counter"
1064 }
1065
1066 fn version(&self) -> i64 {
1067 self.version
1068 }
1069
1070 async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
1071 match command {
1072 CounterCommand::Create { id } => {
1073 if self.created {
1074 return Err(CounterError::AlreadyExists);
1075 }
1076
1077 Ok(vec![Event::new(NewEvent {
1078 aggregate_type: "Counter",
1079 aggregate_id: &id,
1080 sequence: self.version + 1,
1081 event_type: "Created",
1082 payload: serde_json::json!({ "id": id }),
1083 })])
1084 }
1085 CounterCommand::Increment { id, amount } => {
1086 if !self.created {
1087 return Err(CounterError::NotFound);
1088 }
1089
1090 if amount <= 0 {
1091 return Err(CounterError::InvalidAmount);
1092 }
1093
1094 Ok(vec![Event::new(NewEvent {
1095 aggregate_type: "Counter",
1096 aggregate_id: &id,
1097 sequence: self.version + 1,
1098 event_type: "Incremented",
1099 payload: serde_json::json!({ "amount": amount }),
1100 })])
1101 }
1102 CounterCommand::Decrement { id, amount } => {
1103 if !self.created {
1104 return Err(CounterError::NotFound);
1105 }
1106
1107 if amount <= 0 {
1108 return Err(CounterError::InvalidAmount);
1109 }
1110
1111 if self.value - amount < 0 {
1112 return Err(CounterError::WouldGoNegative);
1113 }
1114
1115 Ok(vec![Event::new(NewEvent {
1116 aggregate_type: "Counter",
1117 aggregate_id: &id,
1118 sequence: self.version + 1,
1119 event_type: "Decremented",
1120 payload: serde_json::json!({ "amount": amount }),
1121 })])
1122 }
1123 }
1124 }
1125
1126 fn apply(&mut self, event: &Event) {
1127 self.version = event.sequence;
1128
1129 match event.event_type.as_str() {
1130 "Created" => {
1131 self.id = Some(event.aggregate_id.clone());
1132 self.value = 0;
1133 self.created = true;
1134 }
1135 "Incremented" => {
1136 let amount = event.payload["amount"].as_i64().unwrap() as i32;
1137 self.value += amount;
1138 }
1139 "Decremented" => {
1140 let amount = event.payload["amount"].as_i64().unwrap() as i32;
1141 self.value -= amount;
1142 }
1143 _ => {}
1144 }
1145 }
1146 }
1147
1148 #[tokio::test]
1149 async fn test_command_aggregate_id() {
1150 let command = CounterCommand::Create {
1151 id: "counter-1".to_string(),
1152 };
1153
1154 assert_eq!(command.aggregate_id(), "counter-1");
1155 }
1156
1157 #[tokio::test]
1158 async fn test_aggregate_type() {
1159 assert_eq!(CounterAggregate::aggregate_type(), "Counter");
1160 }
1161
1162 #[tokio::test]
1163 async fn test_create_counter() {
1164 let aggregate = CounterAggregate::default();
1165
1166 let command = CounterCommand::Create {
1167 id: "counter-1".to_string(),
1168 };
1169
1170 let events = aggregate.handle(command).await.unwrap();
1171
1172 assert_eq!(events.len(), 1);
1173 assert_eq!(events[0].event_type, "Created");
1174 assert_eq!(events[0].aggregate_id, "counter-1");
1175 assert_eq!(events[0].sequence, 1);
1176 }
1177
1178 #[tokio::test]
1179 async fn test_cannot_create_twice() {
1180 let mut aggregate = CounterAggregate::default();
1181 let event = Event::new(NewEvent {
1182 aggregate_type: "Counter",
1183 aggregate_id: "counter-1",
1184 sequence: 1,
1185 event_type: "Created",
1186 payload: serde_json::json!({ "id": "counter-1" }),
1187 });
1188 aggregate.apply(&event);
1189
1190 let command = CounterCommand::Create {
1191 id: "counter-1".to_string(),
1192 };
1193
1194 let result = aggregate.handle(command).await;
1195 assert!(matches!(result, Err(CounterError::AlreadyExists)));
1196 }
1197
1198 #[tokio::test]
1199 async fn test_increment_counter() {
1200 let mut aggregate = CounterAggregate::default();
1201 let event = Event::new(NewEvent {
1202 aggregate_type: "Counter",
1203 aggregate_id: "counter-1",
1204 sequence: 1,
1205 event_type: "Created",
1206 payload: serde_json::json!({ "id": "counter-1" }),
1207 });
1208 aggregate.apply(&event);
1209
1210 let command = CounterCommand::Increment {
1211 id: "counter-1".to_string(),
1212 amount: 5,
1213 };
1214
1215 let events = aggregate.handle(command).await.unwrap();
1216 assert_eq!(events.len(), 1);
1217 assert_eq!(events[0].event_type, "Incremented");
1218 assert_eq!(events[0].sequence, 2);
1219 }
1220
1221 #[tokio::test]
1222 async fn test_increment_not_found() {
1223 let aggregate = CounterAggregate::default();
1224
1225 let command = CounterCommand::Increment {
1226 id: "counter-1".to_string(),
1227 amount: 5,
1228 };
1229
1230 let result = aggregate.handle(command).await;
1231 assert!(matches!(result, Err(CounterError::NotFound)));
1232 }
1233
1234 #[tokio::test]
1235 async fn test_decrement_counter() {
1236 let mut aggregate = CounterAggregate::default();
1237
1238 // Create and increment to 10
1239 let events = vec![
1240 Event::new(NewEvent {
1241 aggregate_type: "Counter",
1242 aggregate_id: "counter-1",
1243 sequence: 1,
1244 event_type: "Created",
1245 payload: serde_json::json!({ "id": "counter-1" }),
1246 }),
1247 Event::new(NewEvent {
1248 aggregate_type: "Counter",
1249 aggregate_id: "counter-1",
1250 sequence: 2,
1251 event_type: "Incremented",
1252 payload: serde_json::json!({ "amount": 10 }),
1253 }),
1254 ];
1255
1256 for event in events {
1257 aggregate.apply(&event);
1258 }
1259
1260 assert_eq!(aggregate.value, 10);
1261
1262 // Decrement by 3
1263 let command = CounterCommand::Decrement {
1264 id: "counter-1".to_string(),
1265 amount: 3,
1266 };
1267
1268 let events = aggregate.handle(command).await.unwrap();
1269 assert_eq!(events.len(), 1);
1270 assert_eq!(events[0].event_type, "Decremented");
1271 }
1272
1273 #[tokio::test]
1274 async fn test_decrement_would_go_negative() {
1275 let mut aggregate = CounterAggregate::default();
1276
1277 let events = vec![
1278 Event::new(NewEvent {
1279 aggregate_type: "Counter",
1280 aggregate_id: "counter-1",
1281 sequence: 1,
1282 event_type: "Created",
1283 payload: serde_json::json!({ "id": "counter-1" }),
1284 }),
1285 Event::new(NewEvent {
1286 aggregate_type: "Counter",
1287 aggregate_id: "counter-1",
1288 sequence: 2,
1289 event_type: "Incremented",
1290 payload: serde_json::json!({ "amount": 5 }),
1291 }),
1292 ];
1293
1294 for event in events {
1295 aggregate.apply(&event);
1296 }
1297
1298 assert_eq!(aggregate.value, 5);
1299
1300 // Try to decrement by 10 (would go negative)
1301 let command = CounterCommand::Decrement {
1302 id: "counter-1".to_string(),
1303 amount: 10,
1304 };
1305
1306 let result = aggregate.handle(command).await;
1307 assert!(matches!(result, Err(CounterError::WouldGoNegative)));
1308 }
1309
1310 #[tokio::test]
1311 async fn test_from_events() {
1312 let events = vec![
1313 Event::new(NewEvent {
1314 aggregate_type: "Counter",
1315 aggregate_id: "counter-1",
1316 sequence: 1,
1317 event_type: "Created",
1318 payload: serde_json::json!({ "id": "counter-1" }),
1319 }),
1320 Event::new(NewEvent {
1321 aggregate_type: "Counter",
1322 aggregate_id: "counter-1",
1323 sequence: 2,
1324 event_type: "Incremented",
1325 payload: serde_json::json!({ "amount": 5 }),
1326 }),
1327 Event::new(NewEvent {
1328 aggregate_type: "Counter",
1329 aggregate_id: "counter-1",
1330 sequence: 3,
1331 event_type: "Incremented",
1332 payload: serde_json::json!({ "amount": 3 }),
1333 }),
1334 Event::new(NewEvent {
1335 aggregate_type: "Counter",
1336 aggregate_id: "counter-1",
1337 sequence: 4,
1338 event_type: "Decremented",
1339 payload: serde_json::json!({ "amount": 2 }),
1340 }),
1341 ];
1342
1343 let aggregate = CounterAggregate::from_events(events);
1344
1345 assert_eq!(aggregate.version, 4);
1346 assert_eq!(aggregate.value, 6); // 0 + 5 + 3 - 2
1347 assert!(aggregate.created);
1348 }
1349
1350 #[tokio::test]
1351 async fn test_version_increments() {
1352 let aggregate = CounterAggregate::default();
1353 assert_eq!(aggregate.version(), 0);
1354
1355 let mut aggregate = CounterAggregate::default();
1356 let event1 = Event::new(NewEvent {
1357 aggregate_type: "Counter",
1358 aggregate_id: "counter-1",
1359 sequence: 1,
1360 event_type: "Created",
1361 payload: serde_json::json!({ "id": "counter-1" }),
1362 });
1363 aggregate.apply(&event1);
1364 assert_eq!(aggregate.version(), 1);
1365
1366 let event2 = Event::new(NewEvent {
1367 aggregate_type: "Counter",
1368 aggregate_id: "counter-1",
1369 sequence: 2,
1370 event_type: "Incremented",
1371 payload: serde_json::json!({ "amount": 5 }),
1372 });
1373 aggregate.apply(&event2);
1374 assert_eq!(aggregate.version(), 2);
1375 }
1376
1377 // An aggregate that opts into snapshots by serializing its own state.
1378 #[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
1379 struct SnapshotCounter {
1380 value: i32,
1381 version: i64,
1382 }
1383
1384 #[async_trait]
1385 impl Aggregate for SnapshotCounter {
1386 type Command = CounterCommand;
1387 type Event = CounterEvent;
1388 type Error = CounterError;
1389
1390 fn aggregate_type() -> &'static str {
1391 "SnapshotCounter"
1392 }
1393
1394 fn version(&self) -> i64 {
1395 self.version
1396 }
1397
1398 async fn handle(&self, _command: Self::Command) -> Result<Vec<Event>, Self::Error> {
1399 Ok(vec![])
1400 }
1401
1402 fn apply(&mut self, event: &Event) {
1403 self.version = event.sequence;
1404 }
1405
1406 fn to_snapshot(&self) -> Option<serde_json::Value> {
1407 serde_json::to_value(self).ok()
1408 }
1409
1410 fn from_snapshot(state: serde_json::Value) -> Option<Self> {
1411 serde_json::from_value(state).ok()
1412 }
1413 }
1414
1415 #[test]
1416 fn test_snapshot_round_trip_reconstructs_state() {
1417 let original = SnapshotCounter {
1418 value: 42,
1419 version: 7,
1420 };
1421 let state = original
1422 .to_snapshot()
1423 .expect("opted-in aggregate snapshots");
1424 let restored = SnapshotCounter::from_snapshot(state).expect("snapshot decodes");
1425 assert_eq!(original, restored);
1426 }
1427
1428 #[test]
1429 fn test_default_snapshot_methods_opt_out() {
1430 // CounterAggregate does not override the snapshot hooks.
1431 assert!(CounterAggregate::default().to_snapshot().is_none());
1432 assert!(CounterAggregate::from_snapshot(serde_json::json!({})).is_none());
1433 }
1434
1435 #[tokio::test]
1436 async fn test_apply_unknown_event() {
1437 let mut aggregate = CounterAggregate::default();
1438
1439 // Unknown event type should be silently ignored
1440 let event = Event::new(NewEvent {
1441 aggregate_type: "Counter",
1442 aggregate_id: "counter-1",
1443 sequence: 1,
1444 event_type: "UnknownEvent",
1445 payload: serde_json::json!({}),
1446 });
1447
1448 aggregate.apply(&event);
1449
1450 // Version still updates (this is important for correctness)
1451 assert_eq!(aggregate.version, 1);
1452 // But state is unchanged
1453 assert!(!aggregate.created);
1454 assert_eq!(aggregate.value, 0);
1455 }
1456}