Enterprise Architecture Patterns Tutorial 0/65 lessons ~6 min read Lesson 57

    Shared Database

    Multi-Tenant Shared Database stores all tenants' data in one physical database cluster, discriminating rows by a tenant_id column (or equivalent).

    Course progress0%
    Focus
    18 guided sections
    Practice signal
    Examples included
    Career prep
    Interview Q&A included

    Introduction

    Multi-Tenant Shared Database stores all tenants' data in one physical database cluster, discriminating rows by a tenant_id column (or equivalent). Google Workspace, early Salesforce, and many SaaS platforms start here for operational simplicity — one backup, one migration, one capacity plan — but isolation becomes entirely application-enforced.

    Real production story

    A Google Workspace team ran all customer documents in a single Cloud Spanner database with org_id on every row. Operational wins were real: one schema migration, unified monitoring, and 40% lower infra cost vs per-org databases at 50K orgs scale. Then an internal admin tool shipped without WHERE org_id = ? on a support query — two enterprise customers saw each other's folder names in a debug panel for 11 minutes before rollback.

    The post-incident program didn't abandon shared database — it hardened it: mandatory row-level security policies, ORM middleware that injects tenant predicate on every query, integration tests that fail if any SQL lacks tenant filter, and audit logs on cross-tenant access attempts. Shared database remained the model through 5M orgs with zero repeat incidents in 3 years.

    Business problem

    Business pressure: Google Workspace must onboard thousands of new organizations daily without provisioning a new database per signup. Shared database minimizes ops overhead but demands rigorous tenant isolation in application and database layers.

    • Revenue at risk: Cross-tenant data leak terminates enterprise contracts and triggers regulatory investigation — one bug is existential.
    • Engineering velocity: Single schema migration updates all tenants simultaneously — fast feature rollout but high blast radius on migration bugs.
    • Compliance / trust: SOC2, HIPAA BAA, and FedRAMP require demonstrable isolation controls — "we filter in app code" needs RLS and audit evidence.

    Architecture overview

    Shared Database multi-tenancy in production: one database instance, shared schema, every table carries tenant_id, every query filtered by tenant context from authenticated session — never from client-supplied parameter alone.

    • Definition: All tenants share one physical database; logical isolation via tenant discriminator column and access controls.
    • When to adopt: High tenant count, similar schema needs, strong platform team for isolation guardrails, cost-sensitive scale.
    • When to defer: Enterprise deals requiring physical isolation, regulatory mandates for separate storage, or noisy-neighbor SLAs per tenant.
    • Operability: Per-tenant query metrics, RLS policy audit, synthetic cross-tenant penetration tests in CI.

    Architecture motivation

    Why architects care: Shared database optimizes cost and operational simplicity at the expense of isolation risk. The naive "we'll always remember tenant_id" fails at scale — Google treats isolation as defense in depth: app middleware + RLS + audit.

    • Force: Tenant count exceeds per-DB provisioning economics (10K+ tenants).
    • Constraint: Cannot afford N databases × backup × migration × on-call for N tenants.
    • Outcome: Single fleet with tenant_id on all tables, RLS as safety net, automated tenant filter lint in CI.

    Internal architecture

    Google Workspace shared database topology:

    text
    Tenant A ──┐
    Tenant B ──┼──▶ API Gateway (JWT → org_id claim)
    Tenant C ──┘ ↓
    TenantContext (request-scoped)
    Repository layer (auto WHERE org_id = ?)
    Cloud Spanner (shared instance)
    ┌─────────────────────────────┐
    │ documents(org_id, doc_id,…) │
    │ RLS: org_id = session_org() │
    └─────────────────────────────┘

    Data flow

    Read path: JWT validated → org_id extracted → set in TenantContext → repository generates SELECT ... WHERE org_id = @ctx → Spanner RLS double-checks → response scoped to tenant.

    • Write path: org_id set from context on INSERT — never from request body field.
    • Read path: connection pool shared; no per-tenant connections needed.
    • Async path: background jobs carry tenant_id in message envelope; worker sets context before DB access.
    typescript
    class TenantContext {
    private static als = new AsyncLocalStorage<{ orgId: string }>();
    static run<T>(orgId: string, fn: () => T): T {
    return this.als.run({ orgId }, fn);
    }
    static get orgId(): string {
    const ctx = this.als.getStore();
    if (!ctx?.orgId) throw new Error("Missing tenant context — query blocked");
    return ctx.orgId;
    }
    }
    class DocumentRepository {
    async list(): Promise<Document[]> {
    const orgId = TenantContext.orgId;
    return db.query(
    "SELECT * FROM documents WHERE org_id = @orgId",
    { orgId },
    );
    }
    }

    System design diagram

    Two diagrams show the Shared Database topology and the primary request/event path used in production at scale.

    Shared Database — system view
    All tenants
    Edge
    App middleware
    Core
    Shared Spanner
    Data
    RLS policies
    Async
    High-level topology for Shared Database.
    Shared Database — request / event flow
    Auth → tenant ctx
    Ingress
    Inject filter
    Store
    Query + RLS
    Store
    Audit log
    Emit
    Follow this path when reviewing production designs.

    Production code example

    Tenant-safe repository base class with CI hook for SQL audit:

    typescript
    abstract class TenantScopedRepository<T> {
    protected abstract table: string;
    protected baseQuery(): QueryBuilder {
    return db.from(this.table).where("org_id", TenantContext.orgId);
    }
    async findById(id: string): Promise<T | null> {
    return this.baseQuery().andWhere("id", id).first();
    }
    }
    // CI: sql-lint fails on raw queries missing org_id on tenant tables
    // scripts/lint-tenant-queries.ts scans ORM + raw SQL in PR diff

    Enterprise case study

    Google Workspace — shared Spanner at 5M orgs: TenantContext middleware, RLS policies, CI tenant filter lint, and quarterly red-team cross-tenant tests.

    • Before: 11-minute cross-tenant leak in admin tool; enterprise escalations.
    • Decision: Defense in depth — app + RLS + CI lint + audit; keep shared DB for economics.
    • After: Zero cross-tenant incidents in 3 years; 40% lower infra cost vs per-org DB at current scale.

    Trade-offs

    • Cost vs isolation risk: Lowest $/tenant — highest blast radius on missing filter bug.
    • Migration simplicity vs noisy neighbor: One bad query can starve all tenants — need query quotas and per-tenant rate limits.
    • Uniform schema vs customization: All tenants share schema — hard to offer tenant-specific columns without EAV or extension tables.

    Security considerations

    Security is architectural: Shared database means one SQL injection or ORM bug can cross tenant boundaries — treat tenant context as security boundary.

    • Identity: org_id from signed JWT only — never trust query param or header from client without signature.
    • Data: RLS as defense in depth; encrypt sensitive columns; per-tenant KMS keys optional for enterprise tier.
    • Supply chain: ORM and query builder versions pinned; SQL linter in CI for all services touching shared DB.

    Scalability analysis

    Scale dimensions: Google plans for 5M orgs, petabyte storage, and hot orgs 1000× larger than median — shared DB needs sharding strategy before single instance limits.

    • Horizontal scale: Spanner sharding by org_id range; rebalancing hot orgs to dedicated split.
    • Hot spots: Mega-enterprise org dominates read QPS — read replica routing or cache partition per hot org_id.
    • Cost: Shared fleet amortizes storage; monitor $/org/month and alert when hot tenant exceeds fair share.

    Failure scenarios

    What breaks: Missing tenant filter in new endpoint; RLS disabled during migration; background job without tenant context writes cross-tenant.

    • Missing filter: CI SQL linter fails build if query touches tenant table without org_id predicate.
    • RLS bypass: Admin migrations use break-glass role with full audit — never disable RLS in prod without ticket.
    • Job without context: Message schema requires tenant_id; worker throws if absent.

    Staff engineer insights

    • If your isolation strategy is "developers remember tenant_id," you will leak — automate enforcement.
    • RLS is the seatbelt, not the steering wheel — app layer must set context correctly; RLS catches mistakes.
    • Plan sharding exit ramp before single Spanner instance hits limits — org_id range split is easier early.

    Interview questions

    Interview Prep

    Practice concise answers, then expand each card for the explanation.

    3 questions
    1AdvancedQuestionWhy would Google choose shared database over separate database per org at 5M orgs?+

    Answer

    Economics and ops: one migration fleet, unified backup/DR, simpler capacity planning, 40%+ cost savings at high tenant count. Trade isolation risk for cost — mitigated with RLS, TenantContext, CI lint. Separate DB per org breaks down at provisioning and migration scale.

    Follow-up

    At what tenant count or deal size would you offer separate database tier?
    2AdvancedQuestionDesign defense in depth for tenant isolation in a shared Spanner database.+

    Answer

    Layer 1: JWT → TenantContext (AsyncLocalStorage). Layer 2: repository base class auto-injects org_id. Layer 3: Spanner RLS policies. Layer 4: CI SQL linter on PR. Layer 5: audit log on break-glass admin access. Layer 6: quarterly automated cross-tenant penetration tests.

    Follow-up

    How do you handle admin support tools that legitimately need cross-tenant read?
    3AdvancedQuestionA hot enterprise tenant is causing noisy neighbor latency for smaller tenants. Options?+

    Answer

    Short term: per-org query rate limits, read replica dedicated to hot org_id range, aggressive caching for hot org. Medium: Spanner split moving hot org to dedicated shard. Long: offer enterprise tier with separate schema or database — revenue funds isolation premium.

    Follow-up

    How do you detect noisy neighbor before customer complaints?

    Architecture review questions

    • Are quality attributes (latency, availability, consistency) explicit with SLOs for Multi-Tenant Shared Database?
    • Is the failure/degraded mode documented — including what happens when dependencies are down?
    • Are boundaries and ownership clear on an architecture diagram a new engineer understands in 10 minutes?
    • Is there an ADR capturing alternatives considered and why they were rejected?
    • Can this design scale 10× on traffic and 3× on engineering headcount without a rewrite?
    • Security: authn/authz, encryption, and blast radius reviewed at every external interface?

    Summary

    Multi-Tenant Shared Database at Google scale means one fleet, tenant_id everywhere, and isolation enforced by multiple layers — not developer discipline alone. Master the economics, wire automated guardrails, and plan the sharding exit ramp.

    Ready to mark this lesson complete?Track your journey across the entire course.