Skip to main content

Domain Modeling and Pure Domain

1. Conceptualization & Domain Modeling

Let’s model the Counter domain. A counter seems simple, but in an enterprise environment, every state change requires complete auditability, precise validation rules, and high scalability.

Mapping the Domain Requirements

To model this domain, we map the requirements to core DDD and Event Sourcing patterns:
  • Aggregate Root (Counter): The primary consistency boundary. It maintains the current counter value, tracks the stream revision (for optimistic concurrency), and ensures that all state mutations are applied sequentially.
  • Value Object (CounterId): A type-safe newtype wrapper around a String representing the unique ID of our counter stream.
  • Commands (CounterCommand): Intentions to change state. These represent the write operations:
    • Increment { amount: i32 }: Requests to add a positive amount.
    • Decrement { amount: i32 }: Requests to subtract a positive amount.
    • Reset: Requests to reset the counter to zero.
  • Events (CounterEvent): Historical, immutable facts that have occurred. These represent our historical log:
    • Incremented { amount: i32 }
    • Decremented { amount: i32 }
    • ResetPerformed { value: i32 }

Why This Matters

By separating commands (intentions) from events (facts), we separate validation from execution.
[!IMPORTANT] Command Handling is Validative: Commands can be rejected if they violate invariants. Event Application is Infallible: Events represent the past. Once an event is committed, it cannot be rejected or fail to apply; it must mutate the aggregate state without further checks.

2. Implementing the Pure Domain

Let’s examine the full, pure domain implementation located inside examples/counter-app/src/domain.rs. Notice that this file has absolutely no infrastructure dependencies (no databases, no network frameworks). It is pure, highly testable Rust logic that implements our framework’s Aggregate trait.
[!TIP] Notice the usage of checked_add and checked_sub in handle. This ensures the aggregate defends its invariants before accepting changes, while apply uses saturating_add as a secondary safety mechanism when applying historical facts.