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

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:

  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
  • 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