# Aether Domain Models Index This directory contains tactical Domain-Driven Design models for Aether's bounded contexts. Each model documents the invariants, aggregates, commands, events, policies, and read models for one bounded context. ## Bounded Contexts in Aether Aether's system consists of three primary bounded contexts: ### 1. Event Sourcing (Core) **File:** `DOMAIN_MODEL_EVENT_SOURCING.md` **Responsibility:** Persist events as immutable source of truth; enable state reconstruction through replay **Core Invariant:** Monotonic versioning per actor (version > previous version) **Aggregate:** ActorEventStream (tracks current version, enforces monotonic writes) **Key Commands:** - SaveEvent: Persist event; fail if version conflict - GetLatestVersion: Read current version - GetEvents: Replay event stream - GetEventsWithErrors: Replay with error visibility **Key Events:** - EventStored (implicitly published after SaveEvent) **Key Policies:** - Version Validation: SaveEvent enforces version > current - Append-Only: No delete/update operations - Idempotent Publishing: JetStream dedup by event ID **Key Read Models:** - EventStream (all events for actor) - CurrentVersion (latest version) - StateSnapshot (point-in-time state) **Design Principle:** "Primitives over frameworks" - Caller controls versioning (not auto-incremented) - Caller decides retry strategy (library fails on conflict) - Caller builds domain logic (library provides persistence) --- ### 2. Optimistic Concurrency Control (Pattern) **File:** `DOMAIN_MODEL_OCC.md` **Responsibility:** Detect concurrent write conflicts without locks; signal conflict with full context **Core Invariant:** Monotonic version sequence per actor (strictly increasing) **Aggregate:** ActorEventStream (same as Event Sourcing) **Key Design:** - No locks, no blocking - First writer wins (version conflict) - Caller sees conflict and decides: retry, skip, backoff, or fail - Works by: caller gets current version → sets next version → SaveEvent validates **Why This Pattern?** - Efficient under low contention (no lock overhead) - Slow under high contention (must retry) - Gives caller full control (auto-retry is not library's job) - Enables idempotence (caller can detect duplicate retries) --- ### 3. Namespace Isolation (Cross-Cutting) **File:** `DOMAIN_MODEL_NAMESPACE_ISOLATION.md` **Responsibility:** Provide logical boundaries for event visibility and storage; prevent cross-contamination **Core Invariants:** 1. Events in namespace X invisible to queries from namespace Y (except wildcard) 2. Namespace names safe for NATS subjects (no wildcards, spaces, or dots) 3. Wildcard subscriptions deliberately bypass isolation (for logging, monitoring, auditing) 4. Pattern matching consistent across layers **Key Mechanism:** - Storage: JetStreamEventStore prefixes stream name with namespace (e.g., "tenant-a_events") - Pub/Sub: EventBus maintains exact vs wildcard subscriber lists separately - Patterns: NATS-style token matching ("*" single token, ">" multiple tokens) **Not an Aggregate:** - Namespace has no invariants of its own - It's a primitive value object used by other contexts - Isolation is enforced as a policy, not an aggregate rule --- ## How These Relate ``` Event Sourcing Context ├── Uses: OCC pattern (monotonic versioning) ├── Uses: Namespace Isolation (multi-scope deployments) └── Provides: EventStore interface (InMemory, JetStream) └── JetStream supports namespaces (complete storage isolation) EventBus (pub/sub) ├── Uses: Namespace Isolation (exact + wildcard subscriptions) └── Distributes: Events published by SaveEvent Downstream Contexts (Clustering, Actors, etc.) ├── Depend on: EventStore (for persistence) ├── Depend on: EventBus (for coordination) ├── Depend on: OCC pattern (for handling version conflicts) └── May use: Namespace Isolation (for multi-tenancy or logical domains) ``` --- ## Key Insights ### 1. Only One True Aggregate ActorEventStream is the only aggregate in Event Sourcing because: - It's the only entity that enforces an invariant (monotonic versioning) - Events are immutable value objects, not child entities - Snapshots are optional, stored separately This is intentional minimalism. Aether provides primitives. ### 2. Version Passed by Caller Unlike typical frameworks, Aether does NOT auto-increment versions because: - Caller knows whether event is idempotent (can detect duplicate retries) - Caller knows expected previous version (optimistic concurrency control) - Caller decides retry strategy (immediate, backoff, circuit-break, skip, fail) This requires more code from user, but gives more control. ### 3. Fail Fast on Conflict SaveEvent returns error immediately (no auto-retry) because: - Auto-retry could turn conflict into invisible duplicate write - Caller might be sending same command twice (duplicate), not a new command - Library can't distinguish between these cases Caller decides: "Is this a new command (retry) or duplicate (skip)?" ### 4. Namespace is Not an Aggregate Namespaces have no invariants, so they're not aggregates. Instead: - Namespace is a primitive value object (string with restrictions) - Isolation is a policy (enforced at storage and pub/sub layer) - Application defines what namespaces mean (tenants, domains, environments) Aether doesn't impose multi-tenancy opinions. ### 5. No Schema Validation in Library Event.Data is `map[string]interface{}` because: - Schema is domain concern, not infrastructure concern - Different domains need different schemas - Caller can add schema validation layer Caller is responsible for: event type versioning, data validation, migration logic. --- ## Using These Models ### For Code Review "Is this change respecting the monotonic version invariant?" → See Event Sourcing model, Invariants section "Why does SaveEvent fail on conflict instead of retrying?" → See OCC model, "Why This Pattern?" and "Design Decisions" sections "Should namespace names allow dots?" → See Namespace Isolation model, "Invariant: Namespace Name Safety" ### For Onboarding "How does event sourcing work in Aether?" → Start with Event Sourcing model, Summary + Aggregates + Commands "What's the difference between InMemoryEventStore and JetStreamEventStore?" → See Event Sourcing model, Code Analysis section "What does 'version conflict' mean?" → See OCC model, "Invariant: Monotonic Version Sequence" ### For Design Decisions "Should we implement snapshot invalidation?" → See Event Sourcing model, Gaps & Improvements section "Can we share events across namespaces?" → See Namespace Isolation model, "Invariant: Namespace Boundary Isolation" "How do we handle event schema evolution?" → See Event Sourcing model, Gap 3 (Event Schema Evolution) --- ## Document Structure Each domain model follows this structure: 1. **Summary**: What problem this context solves, what invariants it protects 2. **Problem Space**: User journeys, decision points, risks 3. **Invariants**: Business rules that must never break 4. **Aggregates**: Entity clusters enforcing invariants 5. **Commands**: Intents that may succeed or fail 6. **Events**: Facts that happened (immutable history) 7. **Policies**: Automated reactions 8. **Read Models**: Queries with no invariants 9. **Value Objects**: Immutable, attribute-defined concepts 10. **Code Analysis**: Current implementation vs intended model 11. **Design Decisions**: Why we chose X instead of Y 12. **Gaps & Improvements**: Optional enhancements (not critical) 13. **References**: Key files and related contexts --- ## Alignment with Aether Vision All models embody two core principles: ### Principle 1: "Primitives Over Frameworks" Aether provides building blocks (Event, EventStore, Version, Namespace), not opinions: - No event schema enforcement (caller builds that) - No command handlers (caller builds that) - No sagas (caller builds that) - No projections (caller builds that) ### Principle 2: "NATS-Native" JetStream is first-class, not bolted-on: - JetStreamEventStore leverages JetStream deduplication, retention, replication - Namespace isolation uses stream naming, not generic filtering - EventBus can extend to NATSEventBus for distributed pub/sub --- ## Testing Strategy Based on these models, test the following: ### Unit Tests (Event Sourcing) - SaveEvent rejects version <= current - SaveEvent accepts version > current - GetLatestVersion returns max of all events - Metadata helpers work correctly ### Integration Tests (OCC) - Concurrent writes with version conflict → first wins, second gets error - Caller can retry with new version and succeed - Idempotent event ID prevents duplicate writes (if implemented) ### Integration Tests (Namespace Isolation) - Events published to namespace A invisible to namespace B - Wildcard subscribers see events from all matching namespaces - Pattern matching (NATS-style) works correctly ### Brownfield Migration Start with InMemoryEventStore (testing) → JetStreamEventStore (integration) → Production deployment --- ## Glossary | Term | Definition | |------|-----------| | **Aggregate** | Cluster of entities enforcing an invariant; has a root entity; transactional boundary | | **Command** | Intent to change state; may succeed or fail | | **Event** | Fact that happened; immutable; published after command succeeds | | **Invariant** | Business rule that must never be broken; enforced by aggregate | | **Policy** | Automated reaction to event; e.g., "when OrderPlaced, reserve inventory" | | **Read Model** | Query view with no invariants; derived from events; may be eventually consistent | | **Value Object** | Immutable, attribute-defined concept; no identity; can be shared | | **ActorEventStream** | Aggregate protecting monotonic version invariant for one actor | | **OCC** | Optimistic Concurrency Control; detect conflicts, don't prevent with locks | | **Namespace** | Logical boundary for events (tenant, domain, environment) | | **Event Sourcing** | Use events as source of truth; derive state by replaying | | **Version Conflict** | Attempt to write event with version <= current (concurrency detected) | --- ## References ### Key Files - Event Sourcing: - `/Users/hugo.nijhuis/src/github/flowmade-one/aether/event.go`: Event, EventStore, VersionConflictError - `/Users/hugo.nijhuis/src/github/flowmade-one/aether/store/memory.go`: InMemoryEventStore - `/Users/hugo.nijhuis/src/github/flowmade-one/aether/store/jetstream.go`: JetStreamEventStore - Pub/Sub: - `/Users/hugo.nijhuis/src/github/flowmade-one/aether/eventbus.go`: EventBus, SubscriptionFilter - `/Users/hugo.nijhuis/src/github/flowmade-one/aether/pattern.go`: Namespace pattern matching ### Related Documents - `CLAUDE.md`: Project context and architecture overview - `vision.md`: Product vision and principles - `/git.flowmade.one/flowmade-one/architecture/manifesto.md`: Organization values and beliefs