All event-sourcing patterns
Production pattern445 words · verified 10 September 2026

Multi-Tenant Event Store Stream Design

A multi-tenant event store must enforce tenant scope before every read, write, replay, subscription, snapshot, and projection operation. Tenant identity is an authorization boundary carried independently from aggregate identity; matching entity IDs in two tenants must never share history or derived state.

Problem

Why this pattern exists

Adding tenant_id field without making it mandatory in query and storage paths creates cross-tenant failure modes. Background rebuilds, admin endpoints, snapshots, cache keys, metrics, and WebSocket filters can bypass controller checks even when ordinary HTTP requests appear isolated.

Embedding tenant only inside entity string is brittle. It invites parsing inconsistencies and makes authorization depend on naming convention. Treat tenant scope as typed request context, validate it at boundary, include it in storage/index keys, and reject missing scope unless endpoint is explicitly system-level with audited privilege.

Design decisions

Make boundaries explicit

  1. 01

    Scope before lookup

    Resolve authenticated tenant and authorization before accessing event IDs, streams, snapshots, or cursors. Filtering after global lookup can leak existence, counts, timing, or payloads.

  2. 02

    Keep aggregate identity independent

    Use tenant scope plus stable entity ID as compound boundary. Two tenants may both own order-42 without collision; moving data between tenants becomes explicit migration, not string rename.

  3. 03

    Partition derived state

    Tenant-facing projection compute and caches must key by tenant before entity. Administrative global views require separate code path, role, telemetry, and audit trail.

AllSource implementation

Apply pattern to durable Core history

AllSource hosted architecture authenticates tenant at control and Query Service layers, while Core remains source of truth for events and operational metadata. Query Service owns per-tenant user-facing projections; Core stores enabled set as opaque tenant metadata and serves tenant-scoped event history. This keeps hot ingest engine from becoming tenant-specific compute layer.

Propagate tenant context to event ingestion, queries, durable-consumer registration, WebSocket delivery, snapshot lookup, and replay. Use fail-closed defaults: missing or invalid tenant context returns error, not global results. Run adversarial tests with same entity and consumer IDs across tenants, including historical reconstruction and reconnect paths.

Compound stream boundary
tenant acme + entity order-42 -> independent stream
tenant orbit + entity order-42 -> independent stream

query scope: tenant resolved from authenticated context
projection key: (tenant_id, entity_id)
missing tenant: reject
system-wide operation: explicit admin path + audit event

Failure modes

Detect weak implementations early

Entity snapshot cache keys only by entity ID.

Fix: Include tenant in key or disable unsafe snapshot fast path for tenant-scoped reads.

WebSocket reconnect receives events outside tenant filter.

Fix: Bind tenant authorization to durable consumer and server-side subscription filter.

Missing tenant parameter returns global data.

Fix: Fail closed; reserve global access for explicit audited administration route.

Production checklist

Ready when each statement is true

  • Tenant scope is typed authenticated context, not optional query text.
  • All indexes, caches, snapshots, consumers, and projections include tenant boundary.
  • Missing scope fails closed.
  • Administrative cross-tenant paths are separate and audited.
  • Isolation tests reuse same IDs across tenants and every access path.

Related patterns

Store history once

Rebuild every useful view from durable events.