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>
This commit is contained in:
541
.product-strategy/NAMESPACE_ISOLATION_ARCHITECTURE.md
Normal file
541
.product-strategy/NAMESPACE_ISOLATION_ARCHITECTURE.md
Normal file
@@ -0,0 +1,541 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user