Files
aether/.product-strategy/DOMAIN_MODELS_INDEX.md
Hugo Nijhuis 271f5db444
Some checks failed
CI / build (push) Successful in 21s
CI / integration (push) Failing after 2m1s
Move product strategy documentation to .product-strategy directory
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>
2026-01-12 23:57:20 +01:00

305 lines
11 KiB
Markdown

# 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