Architecture: consistency and evolution · 3 MIN READ
Tenant isolation belongs in every access path
Carry a verified tenant identity into storage predicates, cache keys and background jobs.
A request can be authenticated and still read another tenant’s object. Authentication proves a principal; authorization decides whether that principal may perform this operation on this resource. Tenant isolation fails when one endpoint is careful but a cache key, bulk export or queued job forgets the same boundary. Treat tenant identity as part of the operation contract.
system-design · A MAP OF THE IDEA
- Verify principal
- Authorize tenant + resource
- Scope data and cache
- Test crossing attempts
Useful when
- Carry a verified tenant identity into storage predicates, cache keys and background jobs.
- Makes the owner and the failure boundary explicit.
Watch out
- A helper named GetByID can bypass isolation even if the list endpoint filters correctly.
- Run native service and failure tests in your own development environment.
Implementation & operation cost: Additional state and tests must be justified by the demonstrated failure; this lesson is not a requirement for every Go program.
The contract and the failure
A request can be authenticated and still read another tenant’s object. Authentication proves a principal; authorization decides whether that principal may perform this operation on this resource. Tenant isolation fails when one endpoint is careful but a cache key, bulk export or queued job forgets the same boundary. Treat tenant identity as part of the operation contract.
Work through the example
Use tenant and resource together in query predicates and uniqueness constraints. A background job records a verified tenant scope and rechecks the relevant permission where policy requires it. Include tenant scope in cache and idempotency keys, pagination cursors and object-storage paths. Consider database row-level security as defense in depth, with explicit connection/session reset rules.
package main
import "fmt"
type Key struct{ Tenant, Order string }
func main() {
cache := map[Key]string{}
cache[Key{"tenant-a", "42"}] = "Ada's order"
cache[Key{"tenant-b", "42"}] = "Grace's order"
fmt.Println(cache[Key{"tenant-a", "42"}])
fmt.Println(cache[Key{"tenant-b", "42"}])
}The composite key demonstrates why order ID alone is insufficient when IDs are only unique within a tenant. It is not an authorization system. Production code must derive the tenant from a verified principal and membership check, not accept an arbitrary X-Tenant header as authority.
Production decisions
A helper named GetByID can bypass isolation even if the list endpoint filters correctly. Shared cache entries and metrics labels can leak data or create unbounded cardinality. Test two tenants with the same resource ID, guessed IDs, stale permissions, bulk operations and queued work. A context value transports identity; it does not validate the identity or replace an explicit authorization decision.
Explain it in an interview
Two tenants use order ID 42. Is a cache key containing only 42 safe? No. The verified tenant scope must participate in lookup and authorization, including the cache key, storage predicate and job context. Describe the state before the operation, its owner, the failure between steps and the evidence you would test. Distinguish what this example demonstrates from the infrastructure a production service still needs.
TAKE THIS WITH YOU
Carry a verified tenant identity into storage predicates, cache keys and background jobs. No. The verified tenant scope must participate in lookup and authorization, including the cache key, storage predicate and job context.
Check your understanding
Two tenants use order ID 42. Is a cache key containing only 42 safe?
Go to the source
Progress stays in this browser. No account needed.