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>
542 lines
18 KiB
Markdown
542 lines
18 KiB
Markdown
# 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:**
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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.
|