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>
305 lines
11 KiB
Markdown
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
|
|
|