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>
18 KiB
18 KiB
Namespace Isolation Architecture Diagram
System Architecture
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ (Defines namespace meaning, validates format, controls access) │
└──────────────┬─────────────────────────────────────────────────┘
│
┌──────┴──────────────────────────────────────────┐
│ │
v v
┌─────────────────────┐ ┌──────────────────────┐
│ EventBus │ │ JetStreamEventStore │
│ (Local Pub/Sub) │ │ (Persistent Store) │
│ │ │ │
│ publish(ns, event) │ │ saveEvent(ns, event) │
│ subscribe(pattern) │ │ getEvents(ns, actor) │
│ │ │ │
│ exactSubscribers │ │ Stream: │
│ [ns1] → [sub1] │ │ {namespace}_events │
│ [ns2] → [sub2] │ │ │
│ wildcardSubscribers │ │ Subject: │
│ [*] → [sub3] │ │ events.{type}.{id} │
│ [prod.>] → [sub4] │ │ │
└──────────┬──────────┘ └──────────┬───────────┘
│ │
│ (local node) │ (local node)
│ │
v v
┌──────────────────────────────────────────────────────┐
│ NATSEventBus (Cross-Node) │
│ │
│ publish(ns) → aether.events.{namespace} │
│ subscribe(pattern) → aether.events.{pattern} │
│ │
│ NATS Subjects: │
│ ├─ aether.events.tenant-abc │
│ ├─ aether.events.tenant-def │
│ ├─ aether.events.prod.* (wildcard) │
│ └─ aether.events.> (all) │
└──────────────────────────────────────────────────────┘
│
│ (NATS cluster)
v
┌──────────────────────────┐
│ Node 1 | Node 2 | ... │
│ (NATS cluster members) │
└──────────────────────────┘
Invariant Enforcement Layers
Layer 1: EventBus (Memory Isolation)
Exact Namespace Isolation
────────────────────────
publish("tenant-a", event)
└─> exactSubscribers["tenant-a"] → [sub1, sub2] ✓ Receives
└─> exactSubscribers["tenant-b"] → [sub3] ✗ Does NOT receive
└─> wildcardSubscribers → [sub4 ("*"), sub5 (">")] ✓ Receive (intentional)
Implementation:
├─ exactSubscribers: map[namespace] → [subscriber] (isolation boundary)
├─ wildcardSubscribers: [subscriber] (cross-boundary, trusted)
└─ publish() delivers to both groups (filter matching applied)
Layer 2: JetStreamEventStore (Storage Isolation)
Storage Namespace Isolation
──────────────────────────
Application 1: NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-a")
└─> Stream: "tenant_a_events"
├─ Subject: events.order.order-123
└─ Subject: events.user.user-456
Application 2: NewJetStreamEventStoreWithNamespace(natsConn, "events", "tenant-b")
└─> Stream: "tenant_b_events"
├─ Subject: events.order.order-789
└─ Subject: events.user.user-012
Isolation Guarantee:
└─ GetEvents(store1, actor) → only from "tenant_a_events" stream
GetEvents(store2, actor) → only from "tenant_b_events" stream
(Different NATS streams = complete isolation)
Layer 3: NATS Subject Routing (Cross-Node)
NATS Subject Hierarchy
─────────────────────
aether.events.tenant-a ← Exact subscription
aether.events.tenant-b ← Exact subscription
aether.events.prod.orders ← Hierarchical
aether.events.prod.users ← Hierarchical
aether.events.staging.orders ← Hierarchical
Pattern Matching:
├─ "tenant-a" → aether.events.tenant-a only
├─ "*" → aether.events.X (one token, no dots)
├─ "prod.*" → aether.events.prod.orders, prod.users
├─ "prod.>" → aether.events.prod.orders, prod.users, prod.orders.legacy
└─ ">" → aether.events.ANYTHING (global, auditing only)
Event Flow: Publish and Subscribe
Scenario 1: Exact Namespace Isolation
Application Code:
─────────────────
bus := aether.NewEventBus()
ch1 := bus.Subscribe("tenant-a") // Exact match: isolation enforced
ch2 := bus.Subscribe("tenant-b") // Exact match: isolation enforced
event := Event{ActorID: "order-123", EventType: "OrderPlaced"}
bus.Publish("tenant-a", event) // Publish to tenant-a
Result:
├─ ch1 ← event ✓ (matched exactly)
├─ ch2 ← (nothing) ✗ (different namespace)
└─ Isolation enforced
Implementation:
func (eb *EventBus) Publish(namespaceID string, event *Event) {
// 1. Deliver to exact subscribers
subscribers := eb.exactSubscribers[namespaceID] // Only this namespace
for _, sub := range subscribers {
eb.deliverToSubscriber(sub, event, namespaceID)
}
// 2. Deliver to matching wildcard subscribers
for _, sub := range eb.wildcardSubscribers {
if MatchNamespacePattern(sub.pattern, namespaceID) { // Pattern matching
eb.deliverToSubscriber(sub, event, namespaceID)
}
}
}
Scenario 2: Wildcard Subscription (Cross-Boundary)
Application Code (Auditing System):
────────────────────────────────────
bus := aether.NewEventBus()
chAudit := bus.Subscribe(">") // Wildcard: receives all namespaces
bus.Publish("tenant-a", event1) // Publish to tenant-a
bus.Publish("tenant-b", event2) // Publish to tenant-b
Result:
├─ chAudit ← event1 ✓ (matches ">")
└─ chAudit ← event2 ✓ (matches ">")
Note: Wildcard intentionally bypasses isolation for observability
(only granted to trusted auditing/logging/monitoring code)
Scenario 3: Cross-Node Publishing
Node 1: Node 2:
──────── ────────
neb1 := NewNATSEventBus(conn1) neb2 := NewNATSEventBus(conn2)
ch1 := neb1.Subscribe("tenant-a") ch2 := neb2.Subscribe("tenant-a")
neb1.Publish("tenant-a", event)
│
├─> EventBus.Publish() [local]
│ └─> ch1 ← event (local delivery)
│
└─> NATS Publish(aether.events.tenant-a) [cross-node]
│
└─> [NATS Broker]
│
└─> Node 2: Subscription aether.events.tenant-a
└─> ch2 ← event (remote delivery)
Result: Both nodes receive, isolation enforced at namespace level
Pattern Matching Rules
Token-Based Matching (NATS-Native)
Namespace: "prod.orders.acme" (3 tokens: ["prod", "orders", "acme"])
Patterns and Results:
├─ "prod.orders.acme" → MATCH (exact)
├─ "prod.orders.*" → MATCH ("*" matches "acme")
├─ "prod.*.acme" → MATCH ("*" matches "orders")
├─ "prod.>" → MATCH (">" matches "orders.acme")
├─ "prod.*" → NO MATCH ("*" doesn't match multiple tokens)
├─ "*" → NO MATCH ("*" doesn't match multiple tokens)
├─ ">" → MATCH (">" matches everything)
└─ "prod.users.acme" → NO MATCH (different tokens)
Rules:
├─ "." separates tokens
├─ "*" matches exactly one token (anything except ".")
├─ ">" matches one or more tokens (only at end)
├─ Exact strings match exactly
└─ Empty pattern matches nothing
Subject Sanitization
Why Sanitization Is Needed
NATS subject tokens have restrictions:
- Cannot contain spaces: "tenant abc" ✗
- Cannot contain dots: "tenant.abc" (becomes two tokens)
- Cannot contain wildcards: "tenant*abc" ✗
- Cannot contain ">": "tenant>abc" ✗
Sanitization Process
Input Namespace: "tenant.abc"
Sanitization: Replace . with _
Output Stream Name: "tenant_abc_events"
NATS Subject: "tenant_abc_events.events.order.order-123"
Security Guarantee:
└─ No way to inject NATS wildcards or special subjects via namespace
Sanitization Rules
func sanitizeSubject(s string) string {
s = strings.ReplaceAll(s, " ", "_") // space → underscore
s = strings.ReplaceAll(s, ".", "_") // dot → underscore
s = strings.ReplaceAll(s, "*", "_") // star → underscore
s = strings.ReplaceAll(s, ">", "_") // greater → underscore
return s
}
Examples:
"tenant abc" → "tenant_abc"
"tenant.acme" → "tenant_acme"
"tenant*acme" → "tenant_acme"
"tenant>acme" → "tenant_acme"
"prod.orders.v2" → "prod_orders_v2"
Value Objects
Namespace
type Namespace string
Characteristics:
├─ Identity: Defined by string value alone
├─ Equality: Namespace("a") == Namespace("a")
├─ Immutability: Cannot change (value type)
├─ Meaning: Application-defined (tenant, domain, environment, etc.)
└─ Format: Alphanumeric + hyphens + dots (for hierarchies)
Examples:
├─ "tenant-a" (simple)
├─ "tenant-123" (alphanumeric)
├─ "prod.orders" (hierarchical: domain.environment)
├─ "prod.us.east" (hierarchical: domain.env.region)
└─ "acme.prod.orders" (hierarchical: tenant.env.domain)
SubjectPattern
type SubjectPattern string
Characteristics:
├─ NATS-native patterns (*, >)
├─ Dot-separated tokens
├─ Wildcards bypass isolation (documented)
└─ Applied consistently via MatchNamespacePattern()
Examples:
├─ "tenant-a" (exact: isolation enforced)
├─ "*" (single-token wildcard)
├─ "prod.*" (match prod.X)
├─ "prod.>" (match prod.X.Y.Z)
└─ ">" (match all: auditing only)
Commands and Their Effects
DefineNamespace
Input: namespace string
└─ Validates format (application responsibility)
└─ Creates logical boundary identifier
Result: Namespace can be used in:
├─ NewJetStreamEventStoreWithNamespace(conn, "events", namespace)
├─ EventBus.Subscribe(namespace)
└─ EventBus.Publish(namespace, event)
PublishToNamespace
Input: namespace string, event *Event
└─ Validates event structure
Processing:
├─ EventBus.Publish()
│ ├─ Deliver to exactSubscribers[namespace]
│ └─ Deliver to matching wildcard subscribers
└─ NATSEventBus.Publish()
└─ Publish to NATS subject "aether.events.{namespace}"
Result: Event delivered to all subscribers matching the namespace
SubscribeToNamespace
Input: pattern string, filter *SubscriptionFilter
└─ Validates pattern format
Processing:
├─ EventBus.SubscribeWithFilter()
│ ├─ If pattern has wildcards: add to wildcardSubscribers
│ └─ Else: add to exactSubscribers[pattern]
└─ NATSEventBus.SubscribeWithFilter()
└─ Create NATS subscription to "aether.events.{pattern}"
Result: Channel created that receives matching events
(filtered by EventTypes and ActorPattern if provided)
Policies
Policy 1: Namespace Event Routing
Trigger: publish(namespace, event)
────────────────────────────────
Action:
1. Deliver to exactSubscribers[namespace]
2. Deliver to all wildcardSubscribers where pattern matches namespace
Filter Application:
├─ EventTypes: Event must have one of the specified types
├─ ActorPattern: Event.ActorID must match pattern
└─ Logic: AND (both must match if both specified)
Invariant Enforced:
└─ Namespace Boundary Isolation
(events in namespace X don't leak to namespace Y,
except through intentional wildcard subscriptions)
Policy 2: NATS Subject Namespacing
Trigger: NATSEventBus.Publish(namespace, event)
──────────────────────────────────────────────
Action: Format subject with namespace prefix
└─ subject := fmt.Sprintf("aether.events.%s", namespace)
Example:
├─ namespace = "tenant-a"
└─ subject = "aether.events.tenant-a"
Purpose:
└─ Ensures cross-node events respect namespace boundaries
(NATS natively routes aether.events.tenant-a separately
from aether.events.tenant-b)
Policy 3: Storage Stream Isolation
Trigger: NewJetStreamEventStoreWithNamespace(conn, streamName, namespace)
──────────────────────────────────────────────────────────────────────
Action: Create separate JetStream stream per namespace
└─ effectiveStreamName := fmt.Sprintf("%s_%s", sanitizeSubject(namespace), streamName)
→ "tenant-a_events", "tenant-b_events", etc.
Purpose:
├─ Complete storage-layer isolation (separate NATS streams)
├─ GetEvents(store1) cannot see events from GetEvents(store2)
└─ Prevents accidental data leakage even if EventBus logic fails
Invariant Enforced:
└─ Namespace Boundary Isolation (storage layer)
Failure Scenarios
Scenario 1: Attempting Cross-Namespace Read
Application Code:
─────────────────
store1 := NewJetStreamEventStoreWithNamespace(conn, "events", "tenant-a")
store2 := NewJetStreamEventStoreWithNamespace(conn, "events", "tenant-b")
store1.SaveEvent(Event{ActorID: "order-123", ...})
store2.GetEvents("order-123", 0)
Expected Result:
├─ store2 queries its own stream "tenant_b_events"
└─ Returns: empty (order-123 was not saved in tenant_b_events)
Isolation Enforced: ✓
Scenario 2: Wildcard Subscription Receives Cross-Namespace
Application Code:
─────────────────
bus := NewEventBus()
chWildcard := bus.Subscribe(">") // CAUTION: bypass isolation!
chExact := bus.Subscribe("tenant-a") // isolation enforced
bus.Publish("tenant-a", event1)
bus.Publish("tenant-b", event2)
Result:
├─ chExact ← event1 ✓ (only tenant-a)
├─ chWildcard ← event1 ✓ (matches ">")
├─ chWildcard ← event2 ✓ (matches ">")
└─ Isolation bypassed intentionally for auditing
Risk: chWildcard should only be used by trusted auditing code
Scenario 3: Invalid Namespace (Current Behavior)
Current Implementation:
─────────────────────
namespace := "prod.orders" // Contains dot (invalid NATS token)
store := NewJetStreamEventStoreWithNamespace(conn, "events", namespace)
Behavior:
└─ sanitizeSubject("prod.orders") → "prod_orders"
→ Stream: "prod_orders_events" (dot replaced with underscore)
Problem: Silent transformation
└─ Developer thinks they're using "prod.orders"
└─ Storage actually uses "prod_orders"
└─ No validation error, behavior change is implicit
Future (Proposed):
──────────────────
Behavior:
└─ ValidateNamespace("prod.orders") → Error: "namespace contains invalid characters"
→ Application must fix: use "prod_orders" or define hierarchical format
Benefit: Explicit error > silent transformation
Testing Strategy
Unit Tests (Existing)
✓ pattern_test.go
- MatchNamespacePattern() with various patterns
- IsWildcardPattern() detection
✓ namespace_test.go
- sanitizeSubject() character replacement
- Stream naming with/without namespace
- Actor type extraction
Integration Tests (Needed)
✗ store_isolation_test.go
- SaveEvent(store1, ns1) → GetEvents(store2, ns2) returns empty
- Different streams have completely isolated data
✗ eventbus_isolation_test.go
- Publish(ns1) → Subscribe(ns2) receives nothing
- Publish(ns1) → Subscribe(wildcard) receives it
✗ cross_node_isolation_test.go
- Node1 Publish("tenant-a") → Node2 Subscribe("tenant-b") blocked
- Node1 Publish("prod.orders") → Node2 Subscribe("prod.*") receives it
✗ pattern_matching_test.go
- Complex hierarchical patterns verified
- Edge cases: dots, prefixes, suffixes
Summary: Invariants and Their Enforcement
| Invariant | Enforced At | Mechanism |
|---|---|---|
| Namespace Boundary Isolation | EventBus, JetStream, NATS | exactSubscribers per namespace; separate streams; subject routing |
| Namespace Name Safety | JetStream SaveEvent | sanitizeSubject() prevents NATS injection |
| Wildcard Subscriptions Bypass Isolation | EventBus logic | Separate wildcardSubscribers list; documented in comments |
| Subject Pattern Matching Consistency | MatchNamespacePattern() | Token-based matching with * and > |
Result: Three-layer isolation (memory, storage, network) with intentional, documented exceptions for trusted components.