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;
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(
191//! "User",
192//! &id,
193//! self.version + 1,
194//! "UserCreated",
195//! 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(
208//! "User",
209//! &id,
210//! self.version + 1,
211//! "ProfileUpdated",
212//! 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(
225//! "User",
226//! &id,
227//! self.version + 1,
228//! "EmailChanged",
229//! 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;
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;
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("User", &id, self.version + 1, "UserCreated",
403//! # serde_json::json!({ "id": id, "name": name, "email": email }))])
404//! # }
405//! # }
406//! # }
407//! # fn apply(&mut self, event: &Event) {
408//! # self.version = event.sequence;
409//! # if event.event_type == "UserCreated" {
410//! # self.id = Some(event.aggregate_id.clone());
411//! # self.created = true;
412//! # }
413//! # }
414//! # }
415//! #
416//! #[tokio::test]
417//! async fn test_user_creation() {
418//! // Given: A new user aggregate
419//! let aggregate = UserAggregate::default();
420//!
421//! // When: Creating a user
422//! let command = UserCommand::CreateUser {
423//! id: "user-123".to_string(),
424//! name: "Alice".to_string(),
425//! email: "alice@example.com".to_string(),
426//! };
427//!
428//! let events = aggregate.handle(command).await.unwrap();
429//!
430//! // Then: UserCreated event is produced
431//! assert_eq!(events.len(), 1);
432//! assert_eq!(events[0].event_type, "UserCreated");
433//! assert_eq!(events[0].aggregate_id, "user-123");
434//! }
435//!
436//! #[tokio::test]
437//! async fn test_invalid_email() {
438//! // Given: A new user aggregate
439//! let aggregate = UserAggregate::default();
440//!
441//! // When: Creating a user with invalid email
442//! let command = UserCommand::CreateUser {
443//! id: "user-456".to_string(),
444//! name: "Bob".to_string(),
445//! email: "not-an-email".to_string(), // Invalid!
446//! };
447//!
448//! let result = aggregate.handle(command).await;
449//!
450//! // Then: Error is returned
451//! assert!(result.is_err());
452//! }
453//!
454//! #[tokio::test]
455//! async fn test_cannot_create_twice() {
456//! // Given: An existing user (reconstructed from events)
457//! let mut aggregate = UserAggregate::default();
458//! let event = Event::new(
459//! "User",
460//! "user-789",
461//! 1,
462//! "UserCreated",
463//! serde_json::json!({
464//! "id": "user-789",
465//! "name": "Charlie",
466//! "email": "charlie@example.com"
467//! }),
468//! );
469//! aggregate.apply(&event);
470//!
471//! // When: Trying to create the user again
472//! let command = UserCommand::CreateUser {
473//! id: "user-789".to_string(),
474//! name: "Charlie".to_string(),
475//! email: "charlie@example.com".to_string(),
476//! };
477//!
478//! let result = aggregate.handle(command).await;
479//!
480//! // Then: Error is returned
481//! assert!(result.is_err());
482//! }
483//! ```
484//!
485//! ## Advanced Patterns
486//!
487//! ### Multiple Events from One Command
488//!
489//! Sometimes a command should produce multiple events atomically:
490//!
491//! ```rust,ignore
492//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
493//! match command {
494//! OrderCommand::PlaceOrder { order_id, items } => {
495//! // Validate stock
496//! // ...
497//!
498//! // Produce multiple events
499//! Ok(vec![
500//! Event::new("Order", &order_id, self.version + 1, "OrderPlaced", ...),
501//! Event::new("Order", &order_id, self.version + 2, "InventoryReserved", ...),
502//! Event::new("Order", &order_id, self.version + 3, "PaymentRequested", ...),
503//! ])
504//! }
505//! }
506//! }
507//! ```
508//!
509//! ### Conditional Events
510//!
511//! Commands may produce zero events if preconditions aren't met:
512//!
513//! ```rust,ignore
514//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
515//! match command {
516//! UserCommand::MarkAsActive { id } => {
517//! // If already active, no event needed
518//! if self.is_active {
519//! return Ok(vec![]);
520//! }
521//!
522//! Ok(vec![Event::new("User", &id, self.version + 1, "UserActivated", ...)])
523//! }
524//! }
525//! }
526//! ```
527//!
528//! ### Complex Validation
529//!
530//! Use the aggregate state to enforce complex business rules:
531//!
532//! ```rust,ignore
533//! async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
534//! match command {
535//! AccountCommand::Withdraw { amount } => {
536//! // Check balance
537//! if self.balance < amount {
538//! return Err(AccountError::InsufficientFunds);
539//! }
540//!
541//! // Check withdrawal limit
542//! if self.daily_withdrawals + amount > self.daily_limit {
543//! return Err(AccountError::DailyLimitExceeded);
544//! }
545//!
546//! Ok(vec![Event::new("Account", &self.id, self.version + 1, "Withdrawn", ...)])
547//! }
548//! }
549//! }
550//! ```
551
552use crate::event::Event;
553use async_trait::async_trait;
554use std::error::Error;
555
556/// Trait for commands that can be dispatched to aggregates.
557///
558/// Commands represent intent to change aggregate state. They are imperative
559/// (e.g., `CreateUser`, `UpdateProfile`) and can be rejected if business rules
560/// aren't satisfied.
561///
562/// # Design Principles
563///
564/// - **Imperative naming**: Use verbs (Create, Update, Delete, Activate)
565/// - **Validation happens in aggregates**: Commands may be rejected
566/// - **Identify target**: Commands must know which aggregate instance to operate on
567/// - **Serializable**: Commands should be serializable for command sourcing
568///
569/// # Example
570///
571/// ```rust
572/// use arc_core::aggregate::Command;
573/// use serde::{Deserialize, Serialize};
574///
575/// #[derive(Debug, Clone, Serialize, Deserialize)]
576/// pub enum UserCommand {
577/// CreateUser { id: String, name: String, email: String },
578/// UpdateProfile { id: String, name: String },
579/// DeleteUser { id: String },
580/// }
581///
582/// impl Command for UserCommand {
583/// fn aggregate_id(&self) -> &str {
584/// match self {
585/// UserCommand::CreateUser { id, .. } => id,
586/// UserCommand::UpdateProfile { id, .. } => id,
587/// UserCommand::DeleteUser { id } => id,
588/// }
589/// }
590/// }
591/// ```
592pub trait Command: Send + Sync {
593 /// Get the aggregate instance ID this command targets.
594 ///
595 /// The command bus uses this to load the correct aggregate instance
596 /// from the event store.
597 ///
598 /// # Example
599 ///
600 /// ```rust
601 /// # use arc_core::aggregate::Command;
602 /// # use serde::{Deserialize, Serialize};
603 /// #
604 /// # #[derive(Debug, Clone, Serialize, Deserialize)]
605 /// # pub enum UserCommand {
606 /// # CreateUser { id: String, name: String },
607 /// # }
608 /// #
609 /// # impl Command for UserCommand {
610 /// # fn aggregate_id(&self) -> &str {
611 /// # match self {
612 /// # UserCommand::CreateUser { id, .. } => id,
613 /// # }
614 /// # }
615 /// # }
616 /// #
617 /// let command = UserCommand::CreateUser {
618 /// id: "user-123".to_string(),
619 /// name: "Alice".to_string(),
620 /// };
621 ///
622 /// assert_eq!(command.aggregate_id(), "user-123");
623 /// ```
624 fn aggregate_id(&self) -> &str;
625}
626
627/// Trait for domain aggregates in event sourcing.
628///
629/// Aggregates are consistency boundaries that encapsulate business logic,
630/// enforce invariants, and produce events in response to commands.
631///
632/// # Lifecycle
633///
634/// 1. **Load**: Reconstruct aggregate from event stream using `from_events()`
635/// 2. **Command**: Handle command with `handle()` to produce new events
636/// 3. **Store**: Persist events to event store (done by command bus)
637/// 4. **Apply**: Apply new events to update aggregate state using `apply()`
638/// 5. **Publish**: Publish events to event bus (done by command bus)
639///
640/// # Design Principles
641///
642/// - **State is private**: Aggregate state should not be exposed outside
643/// - **Commands produce events**: `handle()` validates and produces events
644/// - **Events update state**: `apply()` updates internal state deterministically
645/// - **Pure functions**: `handle()` has no side effects (no I/O, no mutations)
646/// - **Deterministic apply**: Same events always produce same state
647/// - **Default implementation**: Aggregate must implement `Default` for initial state
648///
649/// # Type Parameters
650///
651/// - `Command`: The command type this aggregate handles (must implement `Command`)
652/// - `Event`: The domain event type (typically an enum, must be serializable)
653/// - `Error`: The error type for validation failures (must implement `std::error::Error`)
654///
655/// # Example
656///
657/// See the module-level documentation for a complete example.
658#[async_trait]
659pub trait Aggregate: Send + Sync + Default {
660 /// The command type this aggregate handles
661 type Command: Command;
662
663 /// The domain event type (your custom enum)
664 type Event;
665
666 /// The error type for validation failures
667 type Error: Error + Send + Sync + 'static;
668
669 /// Get the aggregate type name (e.g., "User", "Order", "Account").
670 ///
671 /// This is used for event metadata and debugging. Should be a static string
672 /// that uniquely identifies this aggregate type in your domain.
673 ///
674 /// # Example
675 ///
676 /// ```rust
677 /// # use arc_core::aggregate::{Aggregate, Command};
678 /// # use arc_core::event::Event;
679 /// # use thiserror::Error;
680 /// #
681 /// # #[derive(Debug)]
682 /// # struct DummyCommand;
683 /// # impl Command for DummyCommand {
684 /// # fn aggregate_id(&self) -> &str { "dummy" }
685 /// # }
686 /// #
687 /// # #[derive(Debug, Error)]
688 /// # #[error("dummy error")]
689 /// # struct DummyError;
690 /// #
691 /// # #[derive(Default)]
692 /// # struct UserAggregate;
693 /// #
694 /// # #[async_trait::async_trait]
695 /// # impl Aggregate for UserAggregate {
696 /// # type Command = DummyCommand;
697 /// # type Event = ();
698 /// # type Error = DummyError;
699 /// #
700 /// fn aggregate_type() -> &'static str {
701 /// "User"
702 /// }
703 /// #
704 /// # fn version(&self) -> i64 { 0 }
705 /// # async fn handle(&self, _: Self::Command) -> Result<Vec<Event>, Self::Error> { Ok(vec![]) }
706 /// # fn apply(&mut self, _: &Event) {}
707 /// # }
708 /// #
709 /// assert_eq!(UserAggregate::aggregate_type(), "User");
710 /// ```
711 fn aggregate_type() -> &'static str;
712
713 /// Get the current version (sequence number) of this aggregate.
714 ///
715 /// The version represents how many events have been applied to this aggregate.
716 /// It starts at 0 for a new aggregate and increments with each event.
717 ///
718 /// This is used for optimistic concurrency control - the event store checks
719 /// that the version hasn't changed since the aggregate was loaded.
720 ///
721 /// # Example
722 ///
723 /// ```rust
724 /// # use arc_core::aggregate::{Aggregate, Command};
725 /// # use arc_core::event::Event;
726 /// # use thiserror::Error;
727 /// #
728 /// # #[derive(Debug)]
729 /// # struct DummyCommand;
730 /// # impl Command for DummyCommand {
731 /// # fn aggregate_id(&self) -> &str { "dummy" }
732 /// # }
733 /// #
734 /// # #[derive(Debug, Error)]
735 /// # #[error("dummy error")]
736 /// # struct DummyError;
737 /// #
738 /// # #[derive(Default)]
739 /// # struct UserAggregate { version: i64 }
740 /// #
741 /// # #[async_trait::async_trait]
742 /// # impl Aggregate for UserAggregate {
743 /// # type Command = DummyCommand;
744 /// # type Event = ();
745 /// # type Error = DummyError;
746 /// # fn aggregate_type() -> &'static str { "User" }
747 /// #
748 /// fn version(&self) -> i64 {
749 /// self.version
750 /// }
751 /// #
752 /// # async fn handle(&self, _: Self::Command) -> Result<Vec<Event>, Self::Error> { Ok(vec![]) }
753 /// # fn apply(&mut self, _: &Event) {}
754 /// # }
755 /// #
756 /// let aggregate = UserAggregate::default();
757 /// assert_eq!(aggregate.version(), 0); // New aggregate
758 /// ```
759 fn version(&self) -> i64;
760
761 /// Handle a command and produce events.
762 ///
763 /// This is where your business logic lives. The method should:
764 ///
765 /// 1. Inspect current state (`self`) to make decisions
766 /// 2. Validate the command against business rules
767 /// 3. Return error if validation fails
768 /// 4. Produce one or more events if validation succeeds
769 /// 5. Return empty vec if command has no effect
770 ///
771 /// **Important**: This method should have no side effects:
772 /// - Don't write to databases
773 /// - Don't call external APIs
774 /// - Don't mutate state
775 ///
776 /// Side effects happen in projections and event handlers.
777 ///
778 /// # Arguments
779 ///
780 /// - `command`: The command to handle
781 ///
782 /// # Returns
783 ///
784 /// - `Ok(Vec<Event>)`: One or more events to persist and publish
785 /// - `Err(Self::Error)`: Validation or business rule failure
786 ///
787 /// # Example
788 ///
789 /// ```rust,ignore
790 /// async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
791 /// match command {
792 /// UserCommand::CreateUser { id, name, email } => {
793 /// // Check invariant
794 /// if self.created {
795 /// return Err(UserError::AlreadyExists);
796 /// }
797 ///
798 /// // Validate input
799 /// if !email.contains('@') {
800 /// return Err(UserError::InvalidEmail);
801 /// }
802 ///
803 /// // Produce event
804 /// Ok(vec![Event::new("User", &id, self.version + 1, "UserCreated", ...)])
805 /// }
806 /// }
807 /// }
808 /// ```
809 async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error>;
810
811 /// Apply an event to update aggregate state.
812 ///
813 /// This method must be **deterministic** and **side-effect free**:
814 /// - Same events always produce same state
815 /// - No I/O operations
816 /// - No randomness
817 /// - No external dependencies
818 ///
819 /// The event is already persisted when this is called. Your job is to
820 /// update the aggregate's internal state to reflect the event.
821 ///
822 /// # Arguments
823 ///
824 /// - `event`: The event to apply (already persisted)
825 ///
826 /// # Example
827 ///
828 /// ```rust,ignore
829 /// fn apply(&mut self, event: &Event) {
830 /// self.version = event.sequence;
831 ///
832 /// match event.event_type.as_str() {
833 /// "UserCreated" => {
834 /// self.id = Some(event.payload["id"].as_str().unwrap().to_string());
835 /// self.name = Some(event.payload["name"].as_str().unwrap().to_string());
836 /// self.created = true;
837 /// }
838 /// "ProfileUpdated" => {
839 /// self.name = Some(event.payload["name"].as_str().unwrap().to_string());
840 /// }
841 /// _ => {} // Unknown event types are ignored
842 /// }
843 /// }
844 /// ```
845 fn apply(&mut self, event: &Event);
846
847 /// Reconstruct aggregate from its event stream.
848 ///
849 /// This method has a default implementation that:
850 /// 1. Creates a new aggregate instance using `Default::default()`
851 /// 2. Applies each event in order using `apply()`
852 /// 3. Returns the reconstructed aggregate
853 ///
854 /// You typically don't need to override this unless you have special requirements.
855 ///
856 /// # Arguments
857 ///
858 /// - `events`: The complete event stream for this aggregate
859 ///
860 /// # Returns
861 ///
862 /// A fully reconstructed aggregate with all events applied
863 ///
864 /// # Example
865 ///
866 /// ```rust
867 /// # use arc_core::aggregate::{Aggregate, Command};
868 /// # use arc_core::event::Event;
869 /// # use thiserror::Error;
870 /// #
871 /// # #[derive(Debug)]
872 /// # struct DummyCommand;
873 /// # impl Command for DummyCommand {
874 /// # fn aggregate_id(&self) -> &str { "dummy" }
875 /// # }
876 /// #
877 /// # #[derive(Debug, Error)]
878 /// # #[error("dummy error")]
879 /// # struct DummyError;
880 /// #
881 /// # #[derive(Default)]
882 /// # struct UserAggregate {
883 /// # id: Option<String>,
884 /// # version: i64,
885 /// # created: bool,
886 /// # }
887 /// #
888 /// # #[async_trait::async_trait]
889 /// # impl Aggregate for UserAggregate {
890 /// # type Command = DummyCommand;
891 /// # type Event = ();
892 /// # type Error = DummyError;
893 /// # fn aggregate_type() -> &'static str { "User" }
894 /// # fn version(&self) -> i64 { self.version }
895 /// # async fn handle(&self, _: Self::Command) -> Result<Vec<Event>, Self::Error> { Ok(vec![]) }
896 /// #
897 /// # fn apply(&mut self, event: &Event) {
898 /// # self.version = event.sequence;
899 /// # if event.event_type == "UserCreated" {
900 /// # self.id = Some(event.aggregate_id.clone());
901 /// # self.created = true;
902 /// # }
903 /// # }
904 /// # }
905 /// #
906 /// // Load events from event store
907 /// let events = vec![
908 /// Event::new("User", "user-123", 1, "UserCreated", serde_json::json!({"id": "user-123"})),
909 /// Event::new("User", "user-123", 2, "ProfileUpdated", serde_json::json!({"name": "Alice"})),
910 /// ];
911 ///
912 /// // Reconstruct aggregate
913 /// let aggregate = UserAggregate::from_events(events);
914 ///
915 /// assert_eq!(aggregate.version(), 2);
916 /// assert!(aggregate.created);
917 /// ```
918 fn from_events(events: Vec<Event>) -> Self {
919 let mut aggregate = Self::default();
920 for event in events {
921 aggregate.apply(&event);
922 }
923 aggregate
924 }
925
926 /// Serialize current state for snapshotting.
927 ///
928 /// Default returns `None`: an aggregate opts out and is always reconstructed
929 /// by replaying its stream. Override to return `Some(state)` — typically via
930 /// `serde_json::to_value(self)` — once the aggregate is `Serialize`.
931 fn to_snapshot(&self) -> Option<serde_json::Value> {
932 None
933 }
934
935 /// Reconstruct an aggregate from a previously snapshotted state.
936 ///
937 /// Default returns `None` so a loader falls back to stream replay. Override
938 /// to deserialize the value produced by `to_snapshot`. `Option` (rather than
939 /// `Result`) keeps the contract simple: a `None` — whether opted out or a
940 /// failed decode — is always handled by replaying from sequence 0.
941 fn from_snapshot(state: serde_json::Value) -> Option<Self>
942 where
943 Self: Sized,
944 {
945 let _ = state;
946 None
947 }
948}
949
950#[cfg(test)]
951mod tests {
952 use super::*;
953 use serde::{Deserialize, Serialize};
954 use thiserror::Error;
955
956 // Test domain: Simple counter aggregate
957 #[derive(Debug, Clone, Serialize, Deserialize)]
958 enum CounterCommand {
959 Create { id: String },
960 Increment { id: String, amount: i32 },
961 Decrement { id: String, amount: i32 },
962 }
963
964 impl Command for CounterCommand {
965 fn aggregate_id(&self) -> &str {
966 match self {
967 CounterCommand::Create { id } => id,
968 CounterCommand::Increment { id, .. } => id,
969 CounterCommand::Decrement { id, .. } => id,
970 }
971 }
972 }
973
974 #[derive(Debug, Clone, Serialize, Deserialize)]
975 enum CounterEvent {
976 Created { id: String },
977 Incremented { amount: i32 },
978 Decremented { amount: i32 },
979 }
980
981 #[derive(Debug, Error)]
982 enum CounterError {
983 #[error("Counter already exists")]
984 AlreadyExists,
985 #[error("Counter not found")]
986 NotFound,
987 #[error("Amount must be positive")]
988 InvalidAmount,
989 #[error("Counter would go negative")]
990 WouldGoNegative,
991 }
992
993 #[derive(Default)]
994 struct CounterAggregate {
995 id: Option<String>,
996 value: i32,
997 version: i64,
998 created: bool,
999 }
1000
1001 #[async_trait]
1002 impl Aggregate for CounterAggregate {
1003 type Command = CounterCommand;
1004 type Event = CounterEvent;
1005 type Error = CounterError;
1006
1007 fn aggregate_type() -> &'static str {
1008 "Counter"
1009 }
1010
1011 fn version(&self) -> i64 {
1012 self.version
1013 }
1014
1015 async fn handle(&self, command: Self::Command) -> Result<Vec<Event>, Self::Error> {
1016 match command {
1017 CounterCommand::Create { id } => {
1018 if self.created {
1019 return Err(CounterError::AlreadyExists);
1020 }
1021
1022 Ok(vec![Event::new(
1023 "Counter",
1024 &id,
1025 self.version + 1,
1026 "Created",
1027 serde_json::json!({ "id": id }),
1028 )])
1029 }
1030 CounterCommand::Increment { id, amount } => {
1031 if !self.created {
1032 return Err(CounterError::NotFound);
1033 }
1034
1035 if amount <= 0 {
1036 return Err(CounterError::InvalidAmount);
1037 }
1038
1039 Ok(vec![Event::new(
1040 "Counter",
1041 &id,
1042 self.version + 1,
1043 "Incremented",
1044 serde_json::json!({ "amount": amount }),
1045 )])
1046 }
1047 CounterCommand::Decrement { id, amount } => {
1048 if !self.created {
1049 return Err(CounterError::NotFound);
1050 }
1051
1052 if amount <= 0 {
1053 return Err(CounterError::InvalidAmount);
1054 }
1055
1056 if self.value - amount < 0 {
1057 return Err(CounterError::WouldGoNegative);
1058 }
1059
1060 Ok(vec![Event::new(
1061 "Counter",
1062 &id,
1063 self.version + 1,
1064 "Decremented",
1065 serde_json::json!({ "amount": amount }),
1066 )])
1067 }
1068 }
1069 }
1070
1071 fn apply(&mut self, event: &Event) {
1072 self.version = event.sequence;
1073
1074 match event.event_type.as_str() {
1075 "Created" => {
1076 self.id = Some(event.aggregate_id.clone());
1077 self.value = 0;
1078 self.created = true;
1079 }
1080 "Incremented" => {
1081 let amount = event.payload["amount"].as_i64().unwrap() as i32;
1082 self.value += amount;
1083 }
1084 "Decremented" => {
1085 let amount = event.payload["amount"].as_i64().unwrap() as i32;
1086 self.value -= amount;
1087 }
1088 _ => {}
1089 }
1090 }
1091 }
1092
1093 #[tokio::test]
1094 async fn test_command_aggregate_id() {
1095 let command = CounterCommand::Create {
1096 id: "counter-1".to_string(),
1097 };
1098
1099 assert_eq!(command.aggregate_id(), "counter-1");
1100 }
1101
1102 #[tokio::test]
1103 async fn test_aggregate_type() {
1104 assert_eq!(CounterAggregate::aggregate_type(), "Counter");
1105 }
1106
1107 #[tokio::test]
1108 async fn test_create_counter() {
1109 let aggregate = CounterAggregate::default();
1110
1111 let command = CounterCommand::Create {
1112 id: "counter-1".to_string(),
1113 };
1114
1115 let events = aggregate.handle(command).await.unwrap();
1116
1117 assert_eq!(events.len(), 1);
1118 assert_eq!(events[0].event_type, "Created");
1119 assert_eq!(events[0].aggregate_id, "counter-1");
1120 assert_eq!(events[0].sequence, 1);
1121 }
1122
1123 #[tokio::test]
1124 async fn test_cannot_create_twice() {
1125 let mut aggregate = CounterAggregate::default();
1126 let event = Event::new(
1127 "Counter",
1128 "counter-1",
1129 1,
1130 "Created",
1131 serde_json::json!({ "id": "counter-1" }),
1132 );
1133 aggregate.apply(&event);
1134
1135 let command = CounterCommand::Create {
1136 id: "counter-1".to_string(),
1137 };
1138
1139 let result = aggregate.handle(command).await;
1140 assert!(matches!(result, Err(CounterError::AlreadyExists)));
1141 }
1142
1143 #[tokio::test]
1144 async fn test_increment_counter() {
1145 let mut aggregate = CounterAggregate::default();
1146 let event = Event::new(
1147 "Counter",
1148 "counter-1",
1149 1,
1150 "Created",
1151 serde_json::json!({ "id": "counter-1" }),
1152 );
1153 aggregate.apply(&event);
1154
1155 let command = CounterCommand::Increment {
1156 id: "counter-1".to_string(),
1157 amount: 5,
1158 };
1159
1160 let events = aggregate.handle(command).await.unwrap();
1161 assert_eq!(events.len(), 1);
1162 assert_eq!(events[0].event_type, "Incremented");
1163 assert_eq!(events[0].sequence, 2);
1164 }
1165
1166 #[tokio::test]
1167 async fn test_increment_not_found() {
1168 let aggregate = CounterAggregate::default();
1169
1170 let command = CounterCommand::Increment {
1171 id: "counter-1".to_string(),
1172 amount: 5,
1173 };
1174
1175 let result = aggregate.handle(command).await;
1176 assert!(matches!(result, Err(CounterError::NotFound)));
1177 }
1178
1179 #[tokio::test]
1180 async fn test_decrement_counter() {
1181 let mut aggregate = CounterAggregate::default();
1182
1183 // Create and increment to 10
1184 let events = vec![
1185 Event::new(
1186 "Counter",
1187 "counter-1",
1188 1,
1189 "Created",
1190 serde_json::json!({ "id": "counter-1" }),
1191 ),
1192 Event::new(
1193 "Counter",
1194 "counter-1",
1195 2,
1196 "Incremented",
1197 serde_json::json!({ "amount": 10 }),
1198 ),
1199 ];
1200
1201 for event in events {
1202 aggregate.apply(&event);
1203 }
1204
1205 assert_eq!(aggregate.value, 10);
1206
1207 // Decrement by 3
1208 let command = CounterCommand::Decrement {
1209 id: "counter-1".to_string(),
1210 amount: 3,
1211 };
1212
1213 let events = aggregate.handle(command).await.unwrap();
1214 assert_eq!(events.len(), 1);
1215 assert_eq!(events[0].event_type, "Decremented");
1216 }
1217
1218 #[tokio::test]
1219 async fn test_decrement_would_go_negative() {
1220 let mut aggregate = CounterAggregate::default();
1221
1222 let events = vec![
1223 Event::new(
1224 "Counter",
1225 "counter-1",
1226 1,
1227 "Created",
1228 serde_json::json!({ "id": "counter-1" }),
1229 ),
1230 Event::new(
1231 "Counter",
1232 "counter-1",
1233 2,
1234 "Incremented",
1235 serde_json::json!({ "amount": 5 }),
1236 ),
1237 ];
1238
1239 for event in events {
1240 aggregate.apply(&event);
1241 }
1242
1243 assert_eq!(aggregate.value, 5);
1244
1245 // Try to decrement by 10 (would go negative)
1246 let command = CounterCommand::Decrement {
1247 id: "counter-1".to_string(),
1248 amount: 10,
1249 };
1250
1251 let result = aggregate.handle(command).await;
1252 assert!(matches!(result, Err(CounterError::WouldGoNegative)));
1253 }
1254
1255 #[tokio::test]
1256 async fn test_from_events() {
1257 let events = vec![
1258 Event::new(
1259 "Counter",
1260 "counter-1",
1261 1,
1262 "Created",
1263 serde_json::json!({ "id": "counter-1" }),
1264 ),
1265 Event::new(
1266 "Counter",
1267 "counter-1",
1268 2,
1269 "Incremented",
1270 serde_json::json!({ "amount": 5 }),
1271 ),
1272 Event::new(
1273 "Counter",
1274 "counter-1",
1275 3,
1276 "Incremented",
1277 serde_json::json!({ "amount": 3 }),
1278 ),
1279 Event::new(
1280 "Counter",
1281 "counter-1",
1282 4,
1283 "Decremented",
1284 serde_json::json!({ "amount": 2 }),
1285 ),
1286 ];
1287
1288 let aggregate = CounterAggregate::from_events(events);
1289
1290 assert_eq!(aggregate.version, 4);
1291 assert_eq!(aggregate.value, 6); // 0 + 5 + 3 - 2
1292 assert!(aggregate.created);
1293 }
1294
1295 #[tokio::test]
1296 async fn test_version_increments() {
1297 let aggregate = CounterAggregate::default();
1298 assert_eq!(aggregate.version(), 0);
1299
1300 let mut aggregate = CounterAggregate::default();
1301 let event1 = Event::new(
1302 "Counter",
1303 "counter-1",
1304 1,
1305 "Created",
1306 serde_json::json!({ "id": "counter-1" }),
1307 );
1308 aggregate.apply(&event1);
1309 assert_eq!(aggregate.version(), 1);
1310
1311 let event2 = Event::new(
1312 "Counter",
1313 "counter-1",
1314 2,
1315 "Incremented",
1316 serde_json::json!({ "amount": 5 }),
1317 );
1318 aggregate.apply(&event2);
1319 assert_eq!(aggregate.version(), 2);
1320 }
1321
1322 // An aggregate that opts into snapshots by serializing its own state.
1323 #[derive(Default, Serialize, Deserialize, PartialEq, Debug)]
1324 struct SnapshotCounter {
1325 value: i32,
1326 version: i64,
1327 }
1328
1329 #[async_trait]
1330 impl Aggregate for SnapshotCounter {
1331 type Command = CounterCommand;
1332 type Event = CounterEvent;
1333 type Error = CounterError;
1334
1335 fn aggregate_type() -> &'static str {
1336 "SnapshotCounter"
1337 }
1338
1339 fn version(&self) -> i64 {
1340 self.version
1341 }
1342
1343 async fn handle(&self, _command: Self::Command) -> Result<Vec<Event>, Self::Error> {
1344 Ok(vec![])
1345 }
1346
1347 fn apply(&mut self, event: &Event) {
1348 self.version = event.sequence;
1349 }
1350
1351 fn to_snapshot(&self) -> Option<serde_json::Value> {
1352 serde_json::to_value(self).ok()
1353 }
1354
1355 fn from_snapshot(state: serde_json::Value) -> Option<Self> {
1356 serde_json::from_value(state).ok()
1357 }
1358 }
1359
1360 #[test]
1361 fn test_snapshot_round_trip_reconstructs_state() {
1362 let original = SnapshotCounter {
1363 value: 42,
1364 version: 7,
1365 };
1366 let state = original
1367 .to_snapshot()
1368 .expect("opted-in aggregate snapshots");
1369 let restored = SnapshotCounter::from_snapshot(state).expect("snapshot decodes");
1370 assert_eq!(original, restored);
1371 }
1372
1373 #[test]
1374 fn test_default_snapshot_methods_opt_out() {
1375 // CounterAggregate does not override the snapshot hooks.
1376 assert!(CounterAggregate::default().to_snapshot().is_none());
1377 assert!(CounterAggregate::from_snapshot(serde_json::json!({})).is_none());
1378 }
1379
1380 #[tokio::test]
1381 async fn test_apply_unknown_event() {
1382 let mut aggregate = CounterAggregate::default();
1383
1384 // Unknown event type should be silently ignored
1385 let event = Event::new(
1386 "Counter",
1387 "counter-1",
1388 1,
1389 "UnknownEvent",
1390 serde_json::json!({}),
1391 );
1392
1393 aggregate.apply(&event);
1394
1395 // Version still updates (this is important for correctness)
1396 assert_eq!(aggregate.version, 1);
1397 // But state is unchanged
1398 assert!(!aggregate.created);
1399 assert_eq!(aggregate.value, 0);
1400 }
1401}