System designby Learnastra

System-design interview · Extended interviews

Design a multitenant SaaS platform

By Anup Rai

Design how customers share an application without reading one another's data or monopolizing its resources, and move one customer's data without allowing two locations to accept conflicting writes.

You will learn to

  • Carry trusted tenant identity through APIs, queries, caches, jobs, and files.
  • Compare pooled, isolated, and grouped deployment models with explicit costs.
  • Move or restore one tenant without exposing or corrupting another tenant’s state.

Practice in this chapter

8 interview questions with model answers and follow-ups.

Go to interview practice

Useful foundations: Databases, data models, and ACID transactions · Data partitioning and sharding · Design an API rate limiter · Production readiness: SLI, SLO, observability, and recovery

Workload and timing examples are interview assumptions.

01Problem and scope

A multitenant software-as-a-service (SaaS) platform lets customer organizations, called tenants, share infrastructure while keeping their data and permissions separate. For each request, verify who the user is and which tenant they may act for. Carry that verified tenant identity into every database query, cache lookup, background job and file-access check. Invoice 17 can exist in both T7 and T8, so its local number alone is not an access key. Begin with a pooled application/database; introduce cells or dedicated placement when measured scale, isolation or policy requires it.

Smallest working design

Start with one application and one database. Every owned row includes tenant ID; every request establishes the authenticated actor and authorized active tenant. The first design goal is demonstrable isolation. Adding Kubernetes, schemas, or separate databases cannot repair an API that trusts an attacker-supplied tenant identifier.

Clarify the isolation contract

Candidate: “Can a user belong to multiple companies?” Interviewer: “Yes, but every request selects one authorized active tenant.” Candidate: “Does every tenant require its own database?” Interviewer: “No; pool ordinary tenants and isolate unusually large or specifically constrained ones.” This defines an economical default without confusing physical separation with application authorization.

Protocol cases to prove

The read/export protocols must preserve T7/invoice17 scope, and migration from cell C2 to C5 must transfer exclusive write authority. A cell is an independently operated slice of application/data capacity containing a bounded tenant set. The key interview question is where tenant identity is established and enforced at every boundary, including caches, jobs, files and operator tools.

02Functional requirements

  1. Onboard organizations and members. Support users in multiple organizations; each request selects a tenant where the user has current membership.
  2. Manage tenant documents. Administrators invite users, assign roles and create/update invoices through tenant-scoped APIs.
  3. Export in the background. Support bounded export jobs and pagination rather than an unrestricted synchronous dump of a pooled database.
  4. Enforce quotas and show usage. Expose tenant usage and resource limits.
  5. Place and move tenants. Pool ordinary tenants by default; support dedicated placement for unusually large or specially constrained tenants.
  6. Recover and delete. Operators can provision cells, restore one tenant and delete its data under a documented retention process.
  7. Make sharing explicit. Cross-tenant sharing/reporting requires privileged or consented policy and separate query paths. Ordinary invoice APIs never accept a wildcard tenant.

Authorization of long-running work

Export workers revalidate the initiating actor's current permission before reading and before exposing a download. A removed user does not retain a perpetual export grant from a historical request. Operator actions are authenticated and audited with narrowly scoped temporary access.

Specify the isolation boundary

Isolation What it prevents Design question
Logical Reading or modifying another tenant's data Where is tenant scope enforced?
Resource Consuming everyone's CPU, memory, connections or queue capacity Which per-tenant limits and reservations apply?
Placement Violating residency, key, recovery or administrator-access constraints Which components must be dedicated?

Dedicated tenants may share a control plane while receiving isolated data/compute. Compliance labels alone are insufficient: collect the actual placement, key, recovery and access constraints. Microsoft's guidance treats these as choices with operational tradeoffs. Multitenant overview.

03Non-functional requirements

  1. Workload assumption. 10,000 tenants; about 8,333 ordinary requests/s under the worked active-user model; test a fivefold peak.
  2. Latency. Invoice reads p95 below 200 ms and ordinary writes p95 below 400 ms.
  3. Availability. 99.95% per cell. A healthy global average does not hide T7's outage while 9,999 other tenants remain healthy.
  4. Write durability. Successful writes survive the promised single-node failure through configured database replication.
  5. Resource isolation. Set per-tenant concurrent-export limits, database statement timeouts, output-byte quotas and connection/CPU budgets. Purchased tiers may differ but remain bounded; reserve interactive capacity during exports.
  6. Migration availability. Allow an illustrative short write pause at cutover. Do not promise zero-downtime dual writes without reconciliation.
  7. Recovery and placement. Define how much recent tenant data may be lost, the recovery point objective (RPO), and how long restoration may take, the recovery time objective (RTO). Test those targets by restoring backups in a separate recovery environment; collect residency and encryption-key constraints during onboarding. Pooled backups alone do not prove safe single-tenant restoration.

Safety invariants

Boundary Required guarantee
Records, queries, caches, files and jobs Carry trusted tenant scope
Composite relationships Cannot cross tenants unintentionally
Write placement At most one active write placement per tenant
Authorization Never trust only a caller-supplied tenant header
Authorization and placement freshness Use explicit consistency/freshness policies, not arbitrary cache TTLs

04Capacity estimates

Assume 10K tenants, 100 users each, 10% simultaneously active, and five requests per active user/minute.

Quantity Calculation Consequence
Active users 10K × 100 × 0.1 = 100K Session/API capacity
Request rate 100K × 5 / 60 ≈ 8,333/s Base load before exports/retries
Per-tenant mean 8,333 / 10K ≈ 0.83/s Misleading for large customers
One tenant at 20% of traffic 8,333 × 0.2 ≈ 1,667/s May justify dedicated capacity
Stored customer data 10K × 1 GB average = 10 TB Before replicas/backups

Cell sizing

Group tenants into independently scalable deployment cells, sometimes called stamps. One hundred cells with roughly 100 tenants each limits how many customers a failure in one cell affects, but routing and balancing must follow measured workload rather than fixed tenant count alone.

Peak and skew

At a fivefold peak the fleet handles about 41,667 requests/s. Across 100 cells that averages 417/s, but a tenant producing 20% of the base workload already contributes 1,667/s alone. Placement must consider peak CPU, query cost, storage and batch work, not simply 100 tenant names per cell.

Compare exports with interactive work

If an invoice read uses two milliseconds of database CPU and an export scans one million rows at 100 microseconds each, that export costs roughly 100 CPU-seconds—equivalent to 50,000 such reads. A request-count-only limiter gives both operations one token and misses the resource disparity. Set export concurrency and scanned/output-byte limits, and measure real query plans.

Storage and operational cost

Ten TB of logical customer data with three copies becomes at least 30 TB before indexes, transaction logs and backups. A full copy during migration of a 1 TB large tenant temporarily needs another 1 TB plus replay/log headroom. If its write stream is 20 MB/s and copy takes an hour, up to 72 GB of changes may need catch-up. The destination must apply changes faster than they arrive before cutover can become short.

05APIs and contracts

GET /tenants/T7/invoices/17
Authorization: authenticated U7 session
→ {tenantId:T7,invoiceId:17,version:5,...}

POST /tenants/T7/invoices  Idempotency-Key:k44
{customerId:4,amountMinor:2500,currency:"USD"}
→ {invoiceId:18,version:1}

POST /tenants/T7/exports {type:"invoices",filters:{...}}
→ {jobId:J8,state:"queued"}

Trusted scope and retry identity

The path tenant is a requested scope, not proof of membership. Gateway/auth middleware verifies user U7 can act in T7 and creates an internal trusted context. Downstream services validate that context and their required action; they do not trust a user-forged forwarded header. Request IDs and idempotency keys are scoped by tenant/actor/operation so T8 cannot collide with T7's replay records.

Updates, pagination and exports

Invoice updates use expected version to prevent accidental lost edits. Pagination cursors bind tenant, filters and sort; switching the tenant invalidates the cursor. File/download APIs validate owner metadata before issuing short-lived grants. Export status and cancellation also require current tenant permission.

Internal placement and safe errors

Placement is internal. A stale route can return a retriable placement-changed response carrying a validated current route version; clients should not choose arbitrary database cells. The gateway retries a safe operation under its original idempotency identity after refreshing routing, rather than creating a new invoice during migration.

06Data model and access patterns

Interface/data Example
Authorized request GET /tenants/T7/invoices/17 with user U7’s verified membership
Primary key Invoice(tenantId=T7,invoiceId=17,customerId=4,...)
Scoped query WHERE tenant_id=:trustedTenant AND invoice_id=:invoiceId
Foreign key (tenant_id,customer_id) → Customer(tenant_id,id)
Cache identity T7:invoice:17:version5
Job context {job:J8,tenant:T7,actor:U7,permissionSnapshot:...,type:export}

Composite identity and constraints

Uniqueness constraints should reflect the intended scope: invoice numbers may repeat across tenants. Composite foreign keys prevent a T7 invoice referencing T8’s customer accidentally. Files need owner metadata and authorized access even if their object keys contain a tenant prefix. A prefix is naming, not enforcement.

Placement and tenant controls

The tenant directory maps T7 to C2, route epoch 8, region and service tier. Each cell stores TenantControl with the current write epoch and state. An epoch is the version of the tenant's write placement. Every write transaction checks it so a stale router cannot make the old cell accept writes after migration. Membership records associate actor, tenant, roles and policy version. Invoice/customer primary and foreign keys include tenant identity.

Query and RLS context

Cache and job identity

07Basic working design

Authenticate and scope every query

The baseline uses one application, one pooled relational database, a tenant-aware cache and private object storage. Authenticate user U7, verify T7 membership, authorize invoice-read and execute WHERE tenant_id=T7 AND invoice_id=17. The composite primary key identifies T7's row; user U8's T8 row is a separate record even though its local invoice number matches.

Transactional invoice write

For a new invoice, begin a transaction, establish transaction-local tenant context, check the scoped idempotency record, verify customer (T7,4), insert invoice (T7,18) and replay result, then commit. A composite foreign key prevents accidentally linking it to customer (T8,4). The response is returned only after the database's commit policy.

Scoped cache and export

Cache lookup uses a T7-scoped key; on a miss, the same tenant-scoped database query loads the row and fills the cache. Export J8 is persisted with T7 and user U7's identity before enqueueing. A worker revalidates permission, executes bounded tenant queries and writes a T7-owned object. Download authorization is checked separately when the result is retrieved.

Logical versus physical isolation

This baseline demonstrates logical isolation without claiming physical isolation. Test the wrong-tenant path, cache collision and reused connection before scaling. Separate databases would still need these application checks to prevent routing or file-access mistakes.

architecture · baselineBaseline: trusted tenant scope through one pool

T7 and T8 may share infrastructure while invoice17 remains a distinct scoped object everywhere.

Baseline: trusted tenant scope through one poolT7 and T8 may share infrastructure while invoice17 remains a distinct scoped object everywhere. user to api: 1. Request T7 / invoice17; api to cache: 2. Key T7:invoice:17:version; api to db: 3. Scoped query / transaction; api to job: 4. Durable T7 / actor job; job to db: Revalidate and read only T7; job to files: Write T7-owned export; api to files: Authorize scoped download1. Request T7 / invoice172. Key T7:invoice:17:version3. Scoped query / transaction4. Durable T7 / actor jobRevalidate and read only T7Write T7-owned exportAuthorize scoped downloadACTORTenant userSERVICEAuthenticatedscoped APICACHETenant-aware objectcacheSTOREPooledcomposite-keydatabaseWORKERScoped exportworkerSTOREPrivate tenant-ownedfilessyncasync
Read each connection in order
  1. sync1. Request T7 / invoice17Tenant user → Authenticated scoped API
  2. sync2. Key T7:invoice:17:versionAuthenticated scoped API → Tenant-aware object cache
  3. sync3. Scoped query / transactionAuthenticated scoped API → Pooled composite-key database
  4. async4. Durable T7 / actor jobAuthenticated scoped API → Scoped export worker
  5. syncRevalidate and read only T7Scoped export worker → Pooled composite-key database
  6. syncWrite T7-owned exportScoped export worker → Private tenant-owned files
  7. syncAuthorize scoped downloadAuthenticated scoped API → Private tenant-owned files

08Find the baseline flaws

Failure test What breaks and what must follow
Missing tenant scope The simplest leak is a query WHERE invoice_id=17 without tenant scope. A second leak survives correct SQL: cache key invoice:17 is filled by T8, then returned to T7 without a database query. A third occurs after enqueueing: an export worker receives only J8 and assumes the tenant from a thread-local value left by a previous job. Check tenant ownership at every step, including paths that never query the main table.
Pooled connection retains context Connection pooling creates another concrete risk. A session-level tenant variable is set to T7 and the connection is returned without reset. User U8's T8 request reuses it. Transaction-local scope plus explicit initialization/default-deny and tests make this failure visible; relying on developers to remember cleanup in every exception path is fragile.
Batch work starves interactive reads For performance, T9 submits 10,000 expensive exports into one FIFO. Even if ordinary requests remain modest, the batch pool and database scans can delay T7 for hours. Adding application replicas can increase concurrent database pressure and make latency worse. Request-count limits do not capture a million-row export's cost.
Two live write placements Finally, moving T7 by copying its data and changing a router cache while C2 still accepts writes creates two diverging versions. Changing the directory does not stop requests already traveling to the old cell. The later architecture must enforce a single active write placement inside each transactional write path.

09Improve the design, step by step

Model Benefit Cost
Shared tables Efficient pooling and common migrations Every access path must enforce tenant scope
Separate schemas Manage each tenant’s database namespace separately Maintain and migrate many schemas
Separate databases Stronger data administration boundaries Connection, backup, and cost overhead
Dedicated compute/data Reduced noisy-neighbor exposure Lower utilization and larger operational fleet
Hybrid cells Pool most tenants, isolate selected ones Track placement, routing and tenant moves

Choose from actual isolation and economic constraints; these models can coexist. Microsoft storage approaches. Row-level security can provide defense in depth, but privileged/owner roles may bypass it. Verify application role, policies, and pooled-connection context resets rather than assuming “RLS enabled” proves isolation. PostgreSQL RLS.

1. Tenant-aware limits and fair batch scheduling

  • Trigger: T9's exports starve T7.
  • Mechanism: Separate interactive and batch budgets, enforce per-tenant active jobs and schedule fairly across tenant queues. This improves latency isolation without duplicating every server.
  • Benefit, cost and alternative: Costs include tracking each tenant's queued and running jobs, and possibly leaving reserved capacity unused; weighted tiers must not create accidental starvation. A simple global queue is acceptable only while workload skew is demonstrably small.

2. Independently operated cells

  • Trigger: One database reaches capacity, or its failure would affect too many customers.
  • Mechanism: Route bounded tenant groups to replicated cell data/compute. Failures and rollouts affect a subset of customers and aggregate throughput scales.
  • Benefit, cost and alternative: Costs include placement directory, migrations and a larger operational fleet; a broken global auth/control plane can still affect all cells. A larger pooled database is preferable until cell isolation buys measurable reliability or capacity.

3. Dedicated placement for exceptional tenants

  • Trigger: One tenant persistently disrupts others, or needs a particular storage region, encryption key or recovery policy.
  • Mechanism: Move selected tenants to dedicated data/compute using the same application contracts.
  • Benefit, cost and alternative: This improves resource/administrative boundaries at lower utilization and higher per-tenant operations cost. Separate schemas alone may help namespace management but do not reserve CPU or repair untrusted tenant context.

4. Audited migration/restore orchestration

  • Trigger: tenant growth and recovery require movement.
  • Mechanism: Copy a consistent snapshot, apply subsequent source changes, stop old-cell writes with an enforced database guard, and advance the route epoch only after validating the destination.
  • Benefit, cost and alternative: This enables safe lifecycle operations but introduces temporary duplicate data and a cutover pause. Ad hoc dual writes are rejected because partial failure creates divergence without a defined source of truth.

10Detailed architecture

Identity edge and cell routing

The global edge verifies identity and requested tenant membership, then consults the tenant directory for placement and tier. A cell API validates trusted context and action authorization again at the appropriate boundary. Its cache, database, job scheduling and object metadata are all tenant-scoped. The database has replicated durable state and a TenantControl row enforcing whether this cell may write T7 at epoch 8.

Shared control plane

The control plane manages onboarding, role/placement policy and audited moves. It does not sit inside every data query if validated routing/context caches can meet the contract, but revocation and route changes have explicit freshness/fencing rules. Each cell has bounded failure and rollout scope; a dedicated cell uses the same protocol with a smaller tenant set.

Fair jobs and file access

Background jobs travel through durable tenant-context records into fair workers. Workers revalidate the job's authorization policy, set scoped database context and write private tenant-owned results. The download service checks ownership and permission before issuing a grant. A user switching organizations does not automatically change an already-running job's stored tenant.

Migration boundaries

The final diagram includes a migration coordinator and source/destination cells because single-writer placement is a hard correctness boundary. Copy/replay traffic is distinct from user writes. Old cells reject fenced epochs even if a gateway's directory cache remains stale. This prevents control-plane propagation delay from becoming split ownership.

architecture · finalFinal: shared identity, isolated cells and fenced moves

Tenant routing selects placement; each cell independently enforces authorization and current write epoch.

Final: shared identity, isolated cells and fenced movesTenant routing selects placement; each cell independently enforces authorization and current write epoch. user to edge: 1. Authenticate; select authorized T7; edge to directory: 2. Resolve C2 / epoch 8 / tier; edge to cell: 3. Trusted tenant context + epoch; cell to cache: 4. Scoped representation lookup; cell to db: 5. Scope + write fence + transaction; db to rep: Replicate commits and fence state; cell to queue: 6. Persist T7 / actor job; queue to worker: 7. Tenant-budgeted execution; worker to db: Revalidate; scoped bounded query; worker to files: 8. Write owner-tagged export; cell to files: 9. Authorize result download; admin to move: Audited move / restore intent; move to db: 10. Snapshot; replay; exclusive fence; move to dest: 11. Copy and apply through watermark; move to directory: 12. Publish C5 / epoch 9 after ready; edge to destapi: After cutover: trusted T7 / epoch 9; destapi to dest: Scope + current write fence1. Authenticate; selectauthorized T72. Resolve C2 / epoch 8 / tier3. Trusted tenant context +epoch4. Scoped representationlookup5. Scope + write fence +transactionReplicate commits and fencestate6. Persist T7 / actor job7. Tenant-budgeted executionRevalidate; scoped boundedquery8. Write owner-tagged export9. Authorize result downloadAudited move / restore intent10. Snapshot; replay; exclusivefence11. Copy and apply throughwatermark12. Publish C5 / epoch 9 afterreadyAfter cutover: trusted T7 /epoch 9Scope + current write fenceACTOROrganization usersG1SERVICEIdentity / tenantauthorization edgeG1STORETenant placement /tier directoryG1SERVICECell API / tenantbudgetsG2CACHEScoped cell cacheG2STORECell C2 pooled DBTenantControl fenceG2STOREC2 durable replicasG2QUEUEPer-tenant durablejob schedulingG2WORKERFair export workersG2STOREOwner-checkedobject serviceG2WORKERAudited migrationcoordinatorG1STORECell C5 staging /destination DBG3SERVICEScoped operatorcontrol APIG1SERVICECell C5 scoped APIG3syncreplicationasynccontrolG1 Identity and placement controlG2 Cell C2 data planeG3 Cell C5 migration boundary
Read each connection in order
  1. sync1. Authenticate; select authorized T7Organization users → Identity / tenant authorization edge
  2. sync2. Resolve C2 / epoch 8 / tierIdentity / tenant authorization edge → Tenant placement / tier directory
  3. sync3. Trusted tenant context + epochIdentity / tenant authorization edge → Cell API / tenant budgets
  4. sync4. Scoped representation lookupCell API / tenant budgets → Scoped cell cache
  5. sync5. Scope + write fence + transactionCell API / tenant budgets → Cell C2 pooled DB TenantControl fence
  6. replicationReplicate commits and fence stateCell C2 pooled DB TenantControl fence → C2 durable replicas
  7. async6. Persist T7 / actor jobCell API / tenant budgets → Per-tenant durable job scheduling
  8. async7. Tenant-budgeted executionPer-tenant durable job scheduling → Fair export workers
  9. syncRevalidate; scoped bounded queryFair export workers → Cell C2 pooled DB TenantControl fence
  10. sync8. Write owner-tagged exportFair export workers → Owner-checked object service
  11. sync9. Authorize result downloadCell API / tenant budgets → Owner-checked object service
  12. controlAudited move / restore intentScoped operator control API → Audited migration coordinator
  13. sync10. Snapshot; replay; exclusive fenceAudited migration coordinator → Cell C2 pooled DB TenantControl fence
  14. replication11. Copy and apply through watermarkAudited migration coordinator → Cell C5 staging / destination DB
  15. control12. Publish C5 / epoch 9 after readyAudited migration coordinator → Tenant placement / tier directory
  16. syncAfter cutover: trusted T7 / epoch 9Identity / tenant authorization edge → Cell C5 scoped API
  17. syncScope + current write fenceCell C5 scoped API → Cell C5 staging / destination DB

11Write path and acknowledgement

Every mutation requires trusted tenant context and current cell ownership. Background jobs must pass the same tenant and current-placement checks as interactive writes; migration replay has separate authority to populate the read-only destination.

Numbered mutation flow

A write fence is a database-enforced guard that rejects writes at an old placement. Here ordinary writers hold a shared lock while they check TenantControl and commit. Migration takes the exclusive form of that lock, waits for existing writers, and freezes the source before activating the destination.

  1. Authenticate tenant and action. User U7 requests T7, and authentication verifies current membership and invoice-write permission. The internal context binds actor, tenant and relevant policy identity; forged client headers are discarded.
  2. Acquire the current placement fence. The router resolves C2/epoch 8. The cell begins a transaction and acquires the tenant write-fence lock in shared mode, checking TenantControl is active at epoch 8. All tenant-mutating paths, including workers and admin imports, use this guard.
  3. Check scoped retry identity. It sets transaction-local tenant context, checks (T7,U7,create-invoice,k44) and validates customer (T7,4). An identical replay returns the existing invoice; conflicting payload reuse fails.
  4. Commit under the fence. It inserts (T7,18), the replay result and any outbox event, then commits under replication policy. The transaction holds the lock through commit. Migration must wait, so its final copy cannot miss a write that was still finishing.
  5. Persist and run a scoped export. User U7 starts export J8. Persist its trusted tenant/actor/filter/operation data, then enqueue its ID. A fair worker revalidates access, claims a lease, scans bounded T7 pages and writes an immutable result object with T7 owner metadata.
  6. Authorize the completed download. Completion records the object reference and job state; a lost response is recovered through J8. Before download, recheck current permission and issue a scoped short-lived grant.

Reject untrusted tenant context

The application never copies a client-supplied tenant field into trusted job state without validation. Tenant placement may change while J8 waits, so its worker resolves current routing and epoch at execution time.

12Read and delivery path

Lookups combine tenant and local object identity, then check permission. Cache hits and exported files cannot bypass those checks.

Numbered read and export trace

  1. Verify membership. The gateway verifies user U7’s session and membership in T7; a requested active tenant is checked against that membership.
  2. Resolve placement. The routing directory maps T7 to cell C2/version 8.
  3. Authorize and query the scoped key. The API authorizes invoice-read and looks up T7:invoice:17:version5; a cache miss executes the scoped database query.
  4. Return only the scoped row. It returns only T7’s row. User U8’s T8/invoice 17 uses a different query and cache identity.
  5. Persist export authority. User U7 requests export J8. The durable job carries T7 and a defined authorization policy, not a thread-local variable that disappears after enqueueing.
  6. Execute within tenant budget. A tenant-budgeted worker revalidates required access, writes the export as T7-owned content, and returns a scoped download grant.

Every step logs a correlation ID and tenant context without dumping invoice contents or credentials.

Validate cached representations

On a cache hit, validate that the cached representation's tenant/object/version and audience assumptions match the authorized request; a fast hit cannot skip the access decision. A cache entry may hold a public-to-tenant summary while a finance-admin view contains extra fields, requiring separate representation keys or post-cache field authorization.

Scoped query and pagination

For a database read, use the scoped composite key and a role/context policy tested under the real connection pool. Avoid broad error messages that reveal whether another tenant's invoice exists. Pagination cursors bind T7 and the chosen snapshot/order. An object result lookup checks its owner metadata before generating a URL; guessing a T7 prefix is not sufficient.

Stale-route behavior

During a move, a stale C2 route receives a placement-changed response after the fence. The router refreshes and retries a safe read on C5 once ready, with bounded attempts. If the request requires read-your-write after an invoice mutation, use the current authority or a replica proven caught up to the returned commit/version. A random replica can otherwise make a just-created invoice appear missing.

Bind field permissions to returned bytes

For invoice representations with mutable field permissions, authorize a specific row/content version and policy revision, then load that immutable representation; repeat authorization if loading the bytes returns a different revision. For an export, publish the exact immutable object version the worker verified, using create-only object writes or a provider VersionId. An ordinary presigned download URL is a bearer capability, meaning anyone who possesses the URL can use the access it grants: owner checks occur when issuing it, and revoking membership need not revoke an already issued URL. Here grants expire within 60 seconds; a download admitted before expiry may finish. If the contract requires fresh permission on every request or forbids bearer sharing, serve through an authenticated download gateway that compares the requester with the grant and checks current policy. Merely signing a user ID into a transferable URL does not enforce identity.

13Correctness deep dive

Move one tenant without two writers

To move tenant T7 from C2 to C5, create a destination snapshot, replay T7’s changes from a known log position, then establish a controlled write cutover. The directory advances route version 8→9 only when C5 is ready. Fence old C2 writers or briefly pause writes so both cells cannot independently accept conflicting updates. Retain an audited rollback plan and invalidate placement caches.

A request already routed with version 8 must be redirected/retried under an idempotency key or rejected after cutover. Exports and object references need the same tenant ownership even if placement changes. A single-tenant restore should stage a backup separately, validate scope, and import only intended records; restoring a pooled database in place would overwrite unrelated tenants.

Enforce the source fence

The database must reject old writers; a directory flag alone cannot stop them. Every T7 write transaction acquires a shared lock on C2's TenantControl(T7), verifies state=ACTIVE,epoch=8, and holds that lock through commit. Cutover acquires an exclusive lock on the same row, waiting for earlier writers to finish, then commits state=FROZEN,epoch=8 and records a final source change-log watermark W: the log position through which the destination must apply all committed source changes before accepting writes.

Migration state table

Phase Source C2 Destination C5 Router
Copy/replay Active epoch 8 Read-only staging C2/8
Fence Frozen; old writers drained Catch up through W Old routes get retry/pause
Validate No new T7 writes Counts/checksums/versions verified through W Still paused
Activate Remains frozen Active epoch 9 Publish C5/9

Writer versus freeze ordering

Writer wins first: user U7's transaction holds the shared fence lock and commits invoice18. The cutover waits, then freezes; W includes that commit, so C5 receives it before activation. Cutover wins first: user U7's stale C2 request obtains the lock afterward, sees FROZEN and aborts without mutation. Retrying k44 at C5/9 either creates the invoice once or reads its copied replay result.

Guarded write protocol

tenantWrite(T7, expectedEpoch, operation):
  begin; lock TenantControl(T7) SHARED until commit
  require state == ACTIVE and epoch == expectedEpoch
  execute tenant-scoped operation plus replay result
  commit

Close every write path

The fence must cover every write path; a privileged batch job bypassing it breaks the proof. Destination activation happens only after durable replay through W. A directory propagation delay may cause temporary rejections, but cannot enable two writers because C2 remains frozen. After C5 accepts new writes, returning to C2 requires copying C5's new changes back and preventing C5 from writing before C2 is reactivated. Alternatively, keep C5 authoritative and repair it there. Simply pointing back to stale C2 loses committed data.

Coordinator decision fence

Replay and activation authority

The destination's replayed invoice data, idempotency results and control state must be durable before choosing COMMIT_TO_C5. After that choice, recovery completes destination activation and route publication; it does not fall back by reopening the source. ABORT_TO_C2 is allowed only before the commit decision and fences the destination's staging migration from later activation. If the placement authority is unavailable, keep writes paused. Keeping writes paused costs availability, but avoids letting both cells accept changes while the outcome is uncertain.

Freeze watermark and read admission

The final watermark is the durable log position after the freeze transaction commits, not an approximate timestamp collected before draining writers. Copy/replay includes transactional outbox rows and deduplication outcomes. Job completion is a mutation and must resolve the current cell and pass its fence, even if the worker began in C2. Reads also validate the served placement epoch before admitting a current read; a read already admitted before cutover may finish under the documented snapshot contract.

sequence · fenced-moveA stale C2 write cannot race C5 activation

The source fence is enforced by every write transaction and remains closed after destination activation.

A stale C2 write cannot race C5 activationThe source fence is enforced by every write transaction and remains closed after destination activation. write to c2: Shared tenant fence; begin invoice18; move to c2: Request exclusive cutover fence; wait; write to c2: Commit invoice18 + replay k44; c2 to move: Writers drained; freeze at watermark W; move to c5: Replay through W; validate durable staging; move to route: Commit irreversible COMMIT_TO_C5 decision; move to c5: Validate decision; activate epoch 9; move to route: Publish C5 / epoch 9; write to c2: Late request using epoch 8; c2 to write: Reject FROZEN; refresh placement; write to c5: Retry k44 at epoch 9; c5 to write: Return copied invoice18 resultPARTICIPANTUser U7 writePARTICIPANTC2 tenantauthorityPARTICIPANTMigrationcoordinatorPARTICIPANTC5 destinationPARTICIPANTPlacementauthority /directory1. Shared tenant fence; begininvoice182. Request exclusive cutoverfence; wait3. Commit invoice18 + replayk444. Writers drained; freeze atwatermark W5. Replay through W; validatedurable staging6. Commit irreversible COMMIT_TO_C5 decision7. Validate decision; activateepoch 98. Publish C5 / epoch 99. Late request using epoch 810. Reject FROZEN; refreshplacement11. Retry k44 at epoch 912. Return copied invoice18 resultsyncreturn
Read each connection in order
  1. syncShared tenant fence; begin invoice18User U7 write → C2 tenant authority
  2. syncRequest exclusive cutover fence; waitMigration coordinator → C2 tenant authority
  3. syncCommit invoice18 + replay k44User U7 write → C2 tenant authority
  4. returnWriters drained; freeze at watermark WC2 tenant authority → Migration coordinator
  5. syncReplay through W; validate durable stagingMigration coordinator → C5 destination
  6. syncCommit irreversible COMMIT_TO_C5 decisionMigration coordinator → Placement authority / directory
  7. syncValidate decision; activate epoch 9Migration coordinator → C5 destination
  8. syncPublish C5 / epoch 9Migration coordinator → Placement authority / directory
  9. syncLate request using epoch 8User U7 write → C2 tenant authority
  10. returnReject FROZEN; refresh placementC2 tenant authority → User U7 write
  11. syncRetry k44 at epoch 9User U7 write → C5 destination
  12. returnReturn copied invoice18 resultC5 destination → User U7 write

14Failure and recovery

Failure or condition Surviving state, response and recovery
Export flood Tenant T9 submits ten thousand exports. A single global FIFO allows those jobs to delay T7 for hours. Use per-tenant queues or fair scheduling with bounded concurrent work and weighted service tiers. Limit database query time, output bytes, memory, and connection use as well as request count; one expensive export can cost more than thousands of reads.
Autoscaling and shared-resource limits Autoscaling expands total resources but does not guarantee fairness. Shared caches need tenant-aware admission/quotas to reduce eviction attacks. Place very large tenants separately when sustained use warrants the cost. Microsoft discusses pooled compute, dedicated resources, and noisy-neighbor tradeoffs as options rather than universal provider behavior. Compute approaches.
Cell outage If cell C2 fails, its assigned tenants lose service while other cells continue; the shared identity and routing services still need their own availability design. Database failover must preserve committed tenant fences and invoice results, not only table contents. A lost cache reconstructs from scoped authority; never warm it with unscoped global rows for convenience.
Migration coordinator crash If migration crashes after freezing C2 but before activating C5, T7 writes remain paused. The coordinator reads the durable migration decision and watermark: resume COMMIT_TO_C5, or unfreeze C2 only after winning the mutually exclusive ABORT_TO_C2 decision. If it crashes after C5 activation but before the directory response, recovery reads the persisted epoch/state and publishes the existing destination; it does not activate another copy.
Export permission revoked If an export's initiating user is removed mid-job, the selected policy revalidates before further reads/result exposure and cancels or withholds output. If a worker dies after uploading a result but before recording completion, retry identifies the same job/output generation or collects the orphan; it must not return another tenant's similarly named object. Expired download grants require fresh authorization.

15Operations, security, and cost

Isolation and recovery drills

Exercise a cache key missing T7, a forged tenant header, an unscoped background task, a reused database connection, and an administrator export. Each must fail closed or remain tenant-scoped. Operator tooling needs explicit authorization, least privilege, audit, and short-lived access just like customer APIs. Retention/deletion workflows must include derived indexes, exports, and documented backup treatment.

Tenant-level metrics and operator access

Measure latency/error/queue age by tenant or controlled cohorts, resource consumption, denied cross-tenant access, migration lag, and routing-version mismatches. Limit the number of distinct tenant and metric-label combinations stored in the main monitoring system, while retaining authorized logs or queries for investigating one tenant. Global averages should not conceal one customer’s outage or another’s disproportionate load. Isolation remains a property of the whole request lifecycle.

Adversarial tenant fixture

Add a deterministic test fixture with T7 and T8 both owning invoice17 and customer4. Exercise both through the real cache, database role, pooled connections, async workers and file endpoint. A unit test only on the query helper misses cache/job boundaries. Test tenant-control fencing by pausing a write transaction while starting migration, then reverse the ordering and compare destination data.

Resource cost attribution

Measure per-tenant CPU/DB time, scanned/output bytes, active jobs and queue age, not just request count. With 100 cells of 100 tenants each, allocating the same fixed capacity to every cell can leave lightly loaded cells at only 10% utilization while others are busy. Size and rebalance cells by measured demand; pooling improves utilization but still needs fairness controls. Dedicated placement is justified when sustained resource use or administrative constraints exceed the operational cost of isolation—not by a generic “enterprise” label.

Cell rollout and restoration

Roll out schema changes cell by cell with backward-compatible readers/writers, record migration status and stop on failures. Tenant deletion includes source rows, search indexes, exports, caches and documented backup handling. A single-tenant restore stages a backup elsewhere, validates scope and imports through controlled ownership paths; restoring a pooled database in place would overwrite unrelated customers.

16Decision ledger and limitations

Placement and isolation choices

Decision Benefit Cost/limit Change trigger
Pooled composite-key tables Efficient shared capacity Scope required at every access path Tenant constraints or sustained skew justify separation
RLS defense in depth Database rejects many accidental cross-scope queries Privileged roles/context errors can bypass assumptions Adjust role/policy architecture, not merely a checkbox
Independent cells Smaller data-plane blast radius Routing and fleet operations Cell capacity or region requirements change
Fair batch budgets Protects interactive/other-tenant latency Some paid capacity may idle Tier/SLO policy supports another allocation
Fenced copy/replay move Provable single writer Temporary storage and write pause A more expensive zero-pause protocol is justified

What each isolation model buys

Schemas separate namespaces; databases separate administration and connection pools; dedicated compute separates resource contention. None alone prevents a gateway from routing an unauthorized actor to the wrong tenant or a file API from issuing a cross-tenant grant. Logical authorization remains necessary at every physical isolation level.

Shared dependencies and recovery limits

The shared control plane is still a common dependency. Cells limit many failures, not all. Global migrations, identity bugs and operator mistakes can cross cell boundaries unless rollout/audit design constrains them. Do not infer a compliance outcome solely from architecture names; translate required residency, key ownership, restoration and administrator access into specific mechanisms and tests.

17Interview closing

Rehearse the architecture and contract

“I start with a pooled SaaS whose tenant identity comes from verified membership, not a header. Composite keys and foreign keys, tenant-aware caches, transaction-local database scope, durable job context and owner-checked file access carry that identity through the whole lifecycle. I add fair resource budgets before autoscaling, then split tenants into cells and dedicate capacity only where measured load or explicit constraints justify it.

Defend the critical boundary

“During tenant movement, source writes hold a tenant fence through commit. Cutover acquires the exclusive fence, drains writers and records the final source watermark. The destination replays through that watermark before becoming active under the new ownership epoch. The old owner rejects writes, so directory lag cannot create two writers. The costs are a short write pause, temporary data copies and more operational machinery. My next tests attempt cross-tenant resource access through caches and jobs, then exercise both write-versus-cutover interleavings.”

Answer the follow-up

If the interviewer demands per-tenant point-in-time restore, describe staged restore plus scoped import and derived-state rebuild rather than overwriting the pooled database. If they demand zero-pause movement, introduce a stronger forwarding/replication protocol with explicit acknowledgment and failure behavior; the simple fenced-cutover guarantee should not be relabeled zero downtime.

Practise the interview questions

Say your answer aloud before opening the model answer. Then answer the follow-up and compare the reasoning.

Foundation · Question 1

User U7 sends a header claiming tenant T8. Should the application trust it?

Reveal a model answer

No. I verify the supplied identity, check membership and role for the requested active tenant, and construct a trusted tenant context. A path/header can select among authorized memberships, but it cannot create a membership. That context then scopes downstream operations.

What the answer must demonstrate: Tenant selection is not tenant authorization.

Applied · Question 2

Why include tenant ID in a foreign key and cache key?

Reveal a model answer

Tenants T7 and T8 may both own invoice 17/customer4. A composite foreign key prevents crossing tenant relationships, and a tenant-qualified cache key prevents one customer reading another’s cached row. Database correctness does not protect an incorrectly shared application cache.

What the answer must demonstrate: Check tenant ownership in database rows, cached views, jobs and downloadable files.

Applied · Question 3

Does enabling row-level security remove the need for application checks?

Reveal a model answer

No. It is valuable defense in depth, but policy coverage, database role privileges, owner behavior, and tenant-context setup matter. The application still authenticates actors and authorizes actions, while the database limits row visibility under the tested policy.

What the answer must demonstrate: Test the actual connection and role lifecycle.

Follow-up · Question 4

T9’s exports occupy every worker. Why not simply add replicas?

Reveal a model answer

More replicas can help total capacity but do not ensure T7 receives a fair share. I enforce per-tenant concurrency/resource budgets and fair queue scheduling, with separate treatment for expensive exports. Sustained large demand can move to dedicated placement.

What the answer must demonstrate: Resource fairness must reflect work cost.

Follow-up · Question 5

How do you move T7 without two cells becoming writers?

Reveal a model answer

Copy a consistent tenant snapshot and replay its changes. Freeze the source with the tenant lock, capture the durable final watermark, and replay through it. The placement authority then atomically records a COMMIT_TO_DESTINATION decision before destination activation and routing publication; old-cell writes remain fenced. A competing ABORT_TO_SOURCE decision is permitted only before commit. A lost activation reply is therefore recovered by reading the decision, never by guessing that reopening the source is safe.

What the answer must demonstrate: Placement changes must preserve a single write authority.

Foundation · Question 6

When would you choose a dedicated database for one tenant?

Reveal a model answer

When its workload, administration, recovery, or isolation requirements justify the operational cost. Pooled tables are often efficient, while a hybrid fleet can isolate selected customers. I would name the requirement rather than claim separate databases are always safer or always necessary.

What the answer must demonstrate: Isolation has multiple dimensions.

Applied · Question 7

How does the migration fence handle a write already in progress?

Reveal a model answer

Every tenant write holds a shared TenantControl lock through commit. Cutover takes an exclusive lock, so it waits for existing writers, then commits FROZEN and records the final log watermark. Later old-cell writers see FROZEN and abort. Destination replay includes all earlier commits before activation.

What the answer must demonstrate: Name the lock lifetime and final watermark.

Applied · Question 8

The SQL query includes tenantId. Can the service still leak T8’s invoice17 to T7?

Reveal a model answer

Yes, if a shared cache uses invoice17 alone, a file endpoint trusts a guessed prefix, or a background job loses tenant context. Scope must be carried through caches, schemas, job records and object authorization, with representation-sensitive keys where fields vary by role.

What the answer must demonstrate: Do not reduce isolation to one SQL predicate.

Blank-page exercise · 45 minutes

Build the answer yourself

Build an invoicing SaaS where tenants T7 and T8 both own invoice 17. Trace user U7’s read/export, overload it with T9 jobs, then migrate and restore T7 alone.

  • Verify tenant membership before routing.
  • Scope database, cache, foreign keys, files, and jobs.
  • Calculate average load and a large-tenant skew case.
  • Choose pooling/dedicated boundaries and fair resource budgets.
  • Fence a tenant placement cutover and plan tenant-only recovery.
  • Test mixed-tenant connection/cache failures.

Check that each component and design decision follows from your requirements and workload.

Recall the key ideas

Answer from memory before opening each card. Explain why the choice works and what it costs. Revisit missed cards tomorrow.

Design a multitenant SaaS platformWhere does tenant identity come from?Recall first, then reveal

Authenticate the caller, verify membership and authorize the selected tenant. A request header alone proves none of those things.

Authenticate → membership → active tenant.

Return to lesson
Design a multitenant SaaS platformWhat belongs in shared keys?Recall first, then reveal

Tenant identity as well as local resource identity, including database keys, cache keys, jobs, and object authorization.

Tenant scope travels with the record.

Return to lesson
Design a multitenant SaaS platformDoes a separate database solve every isolation problem?Recall first, then reveal

No. Shared compute, queues, caches, logs, credentials, and control-plane tools still need tenant-aware boundaries.

Data isolation is one boundary, not all of them.

Return to lesson

Final revision

Summary and interview notes

Tenant checks must follow requests through database relationships, caches, jobs and files. Shared cells limit cost, while per-tenant work budgets protect neighbors. During a move, database guards stop the old cell’s writers before one durable decision permits the destination to take over.

Remember these points

  • Tenant selection is authorized from authenticated membership; a path, prefix or header alone does not create access.
  • Composite identities and foreign keys must include tenant scope wherever local IDs can repeat.
  • Every mutation holds its current cell's placement guard through commit. Migration copies deduplication records and job/outbox state along with business data.
  • One durable migration decision permits either destination activation or source reopening; recovery cannot choose both after an uncertain response.
  • Resource fairness requires work-cost and concurrency limits in addition to request counts.

Interview tips

  • Use two tenants with identical invoice numbers to test every cache, job and file boundary.
  • Draw the in-flight write versus cutover race, then ask what a new coordinator does after a lost activation reply.

Important qualifications

  • RLS must use constrained application roles and transaction-local context; privileged roles can bypass its assumptions.
  • Anyone holding a presigned download URL may use it before expiry unless the serving endpoint also checks current identity and permission.

Technical references

Practice marks stay in this browser.