# Domain Model Summary: Event Sourcing ## Core Finding The **Event Sourcing** bounded context in Aether is **correctly modeled as tactical DDD**. The library implements: 1. **One core aggregate** (ActorEventStream) protecting one critical invariant: monotonic versioning 2. **Clear commands** (SaveEvent, GetLatestVersion, GetEvents) 3. **Immutable events** (published after SaveEvent succeeds) 4. **Policies** (version validation, append-only persistence, idempotent publishing) 5. **Read models** (event stream, current version, snapshots, namespace-scoped events) 6. **Value objects** (Event, ActorSnapshot, Version) 7. **Namespace isolation** (logical boundaries at storage and bus level) ## Strategic Alignment Aether's vision states: **"Primitives over frameworks"** The Event Sourcing context perfectly embodies this: - **Provides primitives**: Event, EventStore, Version, Snapshot - **Not a framework**: No command handlers, no projections, no sagas - **Caller controls logic**: Version passed by caller, retry on conflict is caller's decision - **Composable**: InMemoryEventStore (testing), JetStreamEventStore (production), both implement same interface - **Optional features**: Snapshots are separate interface, tracing metadata is optional ## The One Invariant **Monotonic Versioning**: Each actor's event stream must have strictly increasing version numbers. ``` Version: 1 → 2 → 3 → 4 → ... (strictly increasing) ↓ ↓ ↓ ↓ EventA EventB EventC EventD ``` Why this invariant exists: - **Optimistic concurrency control**: Detect when another writer moved the actor forward - **Event ordering**: Guarantee causal ordering within single actor stream - **Idempotence detection**: Caller can tell if their write succeeded by checking version ## The Two Critical Design Decisions ### Decision 1: Version Passed by Caller **NOT auto-incremented by library.** Caller does: ```go currentVersion, _ := store.GetLatestVersion(actorID) event.Version = currentVersion + 1 err := store.SaveEvent(event) ``` **Why?** Because caller knows: - Whether event is idempotent (same command, safe to skip if already saved) - What the expected previous version should be - How to detect if another writer won the race Auto-increment would hide this logic and break idempotence safety. ### Decision 2: Fail on Conflict, Don't Retry **SaveEvent returns error if version conflict.** Caller decides next action: ```go err := store.SaveEvent(event) if errors.Is(err, aether.ErrVersionConflict) { // Caller decides: // - Retry with new version? (for legitimate concurrent write) // - Skip? (for duplicate retry of same command) // - Alert? (for unexpected behavior) // - Fail-fast? (for critical paths) } ``` **Why?** Because library can't decide. Auto-retry + auto-increment could turn conflict into invisible duplicate write. ## What's NOT Modeled Here The Event Sourcing context does NOT model: - **Business logic** (e.g., "order can't exceed inventory"): That's Inventory or Sales context - **Saga coordination** (e.g., "when Order placed, reserve inventory"): That's Policy context - **Event schema** (e.g., "OrderPlaced must have productId"): That's domain layer above this - **Read model projections** (e.g., "all orders for customer X"): That's Query/Reporting context - **Multi-aggregate transactions** (e.g., "update order and inventory atomically"): That's domain layer The library provides the **primitives**. Downstream contexts provide the **business logic**. ## Code Alignment: "Brownfield" Assessment Current implementation aligns perfectly with intended model. No refactoring needed: ✓ **Invariant enforced**: SaveEvent validates version > current (both in-memory and JetStream) ✓ **Append-only**: No delete/update in interface ✓ **Commands explicit**: SaveEvent, GetLatestVersion, GetEvents are clear intents ✓ **Events immutable**: Event struct has no setters, metadata helpers don't modify fields ✓ **Policies enforced**: Version validation, idempotent publishing (via message ID) ✓ **Read models clear**: GetEvents, GetLatestVersion, GetLatestSnapshot, Subscribe ✓ **Value objects**: Event, ActorSnapshot, Version are attribute-defined ✓ **Namespace isolation**: Stream-level prefix (JetStream), pattern matching (EventBus) ## The Elegant Part: Versioning The monotonic version approach solves three problems with one invariant: 1. **Detect contention**: If SaveEvent fails with VersionConflict, another writer won 2. **Prevent duplicates**: Caller uses event ID + version as idempotence key 3. **Enable causal ordering**: Version numbers guarantee order within single actor stream This is **not a workaround**—it's the fundamental pattern of optimistic concurrency control. And it's **caller-controlled**, not library magic. Perfect example of "primitives over frameworks." ## Gaps (Minor, Optional) Four improvements identified (all marked as future/optional): 1. **Snapshot invalidation policy**: Snapshots not auto-invalidated when too many events added 2. **Bulk operations**: No SaveMultipleEvents for atomic saves 3. **Event schema evolution**: Caller responsible for versioning (Data is map[string]interface{}) 4. **Deduplication on save**: Could reject duplicate event IDs, currently library doesn't None are critical. All are optimizations or edge cases. Core model is sound. ## Why This Matters This domain model demonstrates how to apply DDD to a library (not an app): - **Aggregates are tiny** (one entity, one invariant) because library scope is small - **Commands are few** (SaveEvent, GetEvents, GetLatestVersion) because caller brings domain - **Events are facts** (EventStored) not commands (not "SaveEvent") - **Policies are infrastructure** (version validation, append-only) not business rules - **Read models are queries** (with no invariants) for deriving state This is DDD applied correctly to **infrastructure code**, where the business domain lives upstream. ## Use This Model For: 1. **Onboarding** new developers: "Here's how Event Sourcing works in Aether" 2. **Design review**: "Are we breaking monotonic versioning invariant?" 3. **Extension design**: "Should this be a policy, policy, or new command?" 4. **Backward compatibility**: "If we change SaveEvent signature, what breaks?" 5. **Testing strategy**: "What are the critical invariants to test?"