Organize all product strategy and domain modeling documentation into a dedicated .product-strategy directory for better separation from code. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
11 KiB
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:
- Events in namespace X invisible to queries from namespace Y (except wildcard)
- Namespace names safe for NATS subjects (no wildcards, spaces, or dots)
- Wildcard subscriptions deliberately bypass isolation (for logging, monitoring, auditing)
- 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:
- Summary: What problem this context solves, what invariants it protects
- Problem Space: User journeys, decision points, risks
- Invariants: Business rules that must never break
- Aggregates: Entity clusters enforcing invariants
- Commands: Intents that may succeed or fail
- Events: Facts that happened (immutable history)
- Policies: Automated reactions
- Read Models: Queries with no invariants
- Value Objects: Immutable, attribute-defined concepts
- Code Analysis: Current implementation vs intended model
- Design Decisions: Why we chose X instead of Y
- Gaps & Improvements: Optional enhancements (not critical)
- 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 overviewvision.md: Product vision and principles/git.flowmade.one/flowmade-one/architecture/manifesto.md: Organization values and beliefs