Design problems
Platform & infrastructureHardSDE-3 40 min

Design a multi-tenant SaaS platform

Asked at
AtlassianFreshworksMicrosoftSalesforceSlackZoho

The interviewer asks

“Design the platform for a B2B SaaS product (think a helpdesk or CRM like Freshdesk or Zoho Desk) with an AI assistant that answers from each customer's own documents. It serves 50,000 tenant companies, from 3-person startups to enterprises with 20,000 agents, and some enterprise customers demand data residency and dedicated infrastructure.”

What a strong answer looks like

A strong answer is built around one question: how is tenant isolation enforced at every layer? Start with the tenancy model and choose deliberately: pooled (shared tables with tenant_id, Postgres Row-Level Security as a safety net), bridge (schema per tenant) or silo (dedicated database or cell) for enterprises, with a tenant catalog that routes each request to the right place. Put tenant context in the token, resolve it once at the edge, and carry it everywhere: DB sessions (SET LOCAL app.tenant_id), cache keys, queue messages, object-storage prefixes, search indexes, vector stores and LLM prompts. Then handle noisy neighbours (per-tenant rate limits, quotas, fair queues, shard rebalancing for big tenants), a cell-based architecture to cap blast radius, data residency (India, EU cells), tenant-aware migrations and onboarding, per-tenant encryption keys, and cost attribution. The AI angle: retrieval and semantic caches must be tenant-scoped, or one customer's documents leak into another's answer.

Concepts to mention

pool vs bridge vs silo tenancy modelstenant catalog and routingtenant context from the verified token, carried everywherePostgres RLS with SET LOCAL and non-owner roletenant-scoped cache keys, queues, S3 prefixes, vector searchnoisy neighbour controls: per-tenant limits and fair queuescell-based architecture for blast radiusdata residency and per-tenant encryption keysonline tenant migration between cellsper-tenant usage and cost attribution

1 Requirements

Functional

  • Tenant sign-up and provisioning in under a minute for self-serve plans
  • Per-tenant users, roles, SSO configuration, custom fields and branding (subdomain acme.helpdesk.com)
  • Core product: tickets, contacts, knowledge base, automations
  • AI assistant that answers agents' and customers' questions using only that tenant's knowledge base and tickets
  • Plan-based limits: seats, API calls, storage, AI tokens per month
  • Enterprise options: dedicated database or cell, data residency (India / EU / US), customer-managed encryption keys
  • Tenant data export and full deletion on contract end

Non-functional

  • Zero cross-tenant data access: isolation enforced by infrastructure, tested continuously
  • Availability 99.95% per cell; an incident in one cell must not affect others
  • API p95 under 300 ms; AI answer time-to-first-token under 1.5 s
  • Scale: 50,000 tenants, ~2M agent users, ~15k API req/s peak, largest tenant ~5% of total load
  • No tenant can use more than its fair share: one tenant's spike must not raise others' p99 by more than ~10%
  • Data residency: Indian tenants' data (including backups and logs) stays in Indian regions when contracted

Out of scope (say it out loud)

  • Billing and invoicing engine (consume usage events only)
  • Detailed helpdesk features (SLA timers, channels like email/WhatsApp ingestion)
  • Marketplace for third-party apps

2 Estimates

Tenant size distribution~90% small, ~0.5% enterpriseTypical power law: 45k tenants under 25 agents, ~250 tenants with 1,000+ agents generate ~40% of load
API traffic~5k req/s average, 15k peak2M agents, ~30% active in business hours, ~1 request per 40 s each ≈ 15k/s peak (IST morning overlaps EU)
Primary data~40 TB~2B tickets × ~10 KB with conversations ≈ 20 TB, plus contacts, KB, indexes ×2
Vector data for AI~150M chunks, ~600 GB of vectorsKB articles + resolved tickets chunked at ~500 tokens; 150M × 1,024 dims × 4 B ≈ 600 GB (less with int8 / halfvec quantisation)
Cells~10-20 cells of ~3,000-5,000 tenantsEach cell sized to a known-good maximum (e.g. one Postgres cluster at ~2-4 TB and 5k req/s) with headroom
AI tokens~5B tokens/month~200k AI answers/day × ~6k tokens (retrieved context + answer) × 30 ≈ 36B worst case; with caching and smaller models for routing, ~5-10B realistic

3 API

EndpointWhat it does
POST/v1/tenantsSelf-serve sign-up: creates tenant in the catalog, assigns a cell and region, provisions defaults asynchronously.
GET/v1/tickets?status=open (Host: acme.helpdesk.com, Bearer JWT with tenant_id)Normal product API; tenant comes from the token, never from a query parameter.
POST/v1/assistant/answer (SSE response)AI answer grounded in the tenant's KB; streams tokens, returns cited sources.
GET/internal/v1/catalog/tenants/{id}Routing lookup: cell, region, tier, plan limits, status (active, migrating, suspended); cached at the edge.
POST/admin/v1/tenants/{id}:move-cellOperator action to migrate a tenant to another cell or a dedicated silo.
POST/v1/tenants/{id}/exports and DELETE /v1/tenants/{id}Data export and contract-end deletion (crypto-shred keys, purge backups by policy).

4 Data model

tenant_catalogGlobal Postgres (small, replicated) + edge cache
tenant_id (PK)slug/subdomain (unique)regioncell_idtier (pooled|dedicated)planlimits jsonbkms_key_arnstatus

The only global data. Every request resolves tenant → cell here (cached for minutes). Contains no customer content.

tickets, contacts, kb_articles (per cell)Postgres per cell (pooled tables)
tenant_id + id composite PK...all indexes lead with tenant_id

RLS policy USING (tenant_id = current_setting('app.tenant_id')::uuid) with FORCE ROW LEVEL SECURITY; app connects as a non-owner role. Large tenants can be placed in a dedicated database with the same schema.

kb_chunkspgvector in the cell DB (or a vector DB with namespaces)
tenant_iddoc_idchunk_idcontenttsvembedding vector(1024)

Same RLS. Hybrid search (BM25-style + vector, merged with RRF). Partition by tenant hash; big tenants get partial HNSW indexes so filtering does not starve their results.

cacheRedis per cell
keys prefixed t:{tenant_id}:...

Prefix is part of a shared helper, never hand-built; semantic cache entries scoped by tenant and model.

filesS3 per region
s3://bucket-{region}/{tenant_id}/...

IAM conditions or presigned URLs restricted to the tenant prefix; per-tenant KMS keys for enterprise.

usage_eventsKafka → ClickHouse
tenant_idmetric (api_call, ai_tokens, storage_gb)qtytscell

Feeds quotas, billing and cost per tenant.

5 Architecture

CLIENTSEDGESERVICESASYNCDATAEXTERNALacme.helpdesk.comlookup (cached)to tenant's cellSET LOCAL tenantt:{tenant} keysask assistanthybrid search + RLSprompt + budgetjobs with tenant_idtenant prefixprovision, movemigrationsAgents, customers,APIsCell router(tenant→cell)Edge: CDN + WAFControl planeCell app servicesAI assistant (RAG)LLM gatewayPer-tenant fair queuesCell workersTenant catalog(global)Cell Postgres + RLSCell RedisRegional S3 + KMSLLM providers
ClientEdge / gatewayService / workerQueue / streamCacheDatabase / storageAI / modelThird partyrequestasync (event / queue)

Hover a box to highlight its connections.

6 Request flows

Agent asks the AI assistant a question

  1. 1An agent at Acme (tenant t42, cell in4 in Mumbai) asks 'How do refunds work for annual plans?'. The JWT carries tenant_id t42; the edge rejects any request whose host subdomain does not match the token's tenant.
  2. 2The cell router looks up t42 in the cached catalog and forwards to cell in4.
  3. 3The assistant service opens a DB transaction, runs SET LOCAL app.tenant_id = 't42', and executes hybrid search: top 50 by vector distance and top 50 by full-text rank, fused with Reciprocal Rank Fusion. RLS guarantees only t42 rows are visible even if a query forgets a filter.
  4. 4It builds the prompt (static system instructions first for provider prompt caching, then the top 8 chunks, then the question) and calls the LLM gateway with tenant t42 and feature 'assistant'.
  5. 5The gateway checks t42's monthly AI token budget and per-minute rate, checks a semantic cache scoped to (t42, model), redacts PII, and streams the answer from the provider.
  6. 6Tokens stream back to the agent over SSE with source citations; a usage event (t42, 6,200 tokens, cost) goes to Kafka for quota and billing.

Onboard an enterprise tenant with residency

  1. 1Sales creates tenant BigBank with region=India, tier=dedicated, customer-managed KMS key.
  2. 2The control plane provisions a dedicated database (from the same schema and migration version as cells) in ap-south-1 via Terraform modules, and registers it in the catalog as its own cell.
  3. 3Encryption uses BigBank's KMS key for the DB, S3 prefix and backups; logs and traces for this cell are shipped to an in-region observability stack.
  4. 4The catalog entry turns active; the router now sends bigbank.helpdesk.com traffic to the dedicated cell, while the code deployed is identical to every other cell.

7 Deep dives

Choosing the tenancy model: pool, bridge, silo

Pooled means all tenants share tables, separated by a tenant_id column. It is the cheapest and simplest to operate (one schema migration, high density), and it fits 50,000 small tenants, but isolation depends on every query being correct and one large tenant affects others. Bridge means a schema per tenant in a shared database: better logical separation and easy per-tenant export, but Postgres struggles with tens of thousands of schemas (catalog bloat, migration time grows with tenant count, connection pooling gets harder). Silo means a dedicated database or full stack per tenant: strongest isolation, per-tenant encryption, residency and maintenance windows, but costly and only manageable with heavy automation. Most real SaaS platforms use a hybrid: pooled for the long tail, silo for the top enterprise customers who pay for it, and the same code and schema everywhere. The tenant catalog makes this possible: the application asks 'where does t42 live?' and gets a cell or dedicated database. Design so a tenant can move between models (pool → silo) with an online migration, because today's startup tenant may be next year's enterprise.

Enforcing isolation in every layer

Cross-tenant leaks almost never come from the database model; they come from one forgotten filter, a cache key without a tenant, or a background job that loses context. Treat tenant context as a security principal. Resolve it once at the edge from the verified token (never from a URL parameter or header the client controls), and carry it in the request context. In Postgres, use Row-Level Security as a second line of defence: policies on every tenant table, FORCE ROW LEVEL SECURITY, the application connecting as a non-owner role, and SET LOCAL inside each transaction so a pooled connection cannot leak the previous request's tenant (plain SET with PgBouncer transaction pooling is a classic bug). Every index leads with tenant_id. Cache keys, queue messages, S3 prefixes, search and vector indexes, and log lines all carry the tenant id through shared libraries, not ad-hoc code. For AI features this matters even more: retrieval must be tenant-filtered (RLS on chunk tables, or a separate namespace per tenant in a vector DB), and the semantic cache must be scoped by tenant, or one company's confidential answer is served to another. Finally, test it: an automated suite that logs in as tenant A and asserts zero rows or objects from tenant B for every endpoint, run in CI forever.

Noisy neighbours and cells

In a pooled system one tenant's bulk import, report query or runaway automation can raise everyone's latency. Defend at each shared resource. At the edge, per-tenant rate limits by plan (token bucket in Redis). For async work, avoid one FIFO queue where a tenant's 1M-job import blocks everyone: use per-tenant fair scheduling (weighted round-robin across tenant sub-queues, or a per-tenant concurrency cap on workers). In the database, statement timeouts, per-tenant connection limits in the pooler, and moving heavy analytics to a replica or ClickHouse. For AI, per-tenant token budgets in the LLM gateway, because one tenant's agent loop can exhaust the provider rate limit for everyone. Beyond that, limit the blast radius with cells: independent copies of the full stack (app, DB, cache, queues), each serving a few thousand tenants, with a thin global router and catalog in front. A bad deploy is rolled out cell by cell; a database incident affects 5% of tenants, not 100%. Cells also give data residency (India cells, EU cells) and a unit of scaling: when cells fill, add a new cell rather than growing one database forever. Hot tenants are moved to quieter cells or their own silo with an online migration (logical replication filtered by tenant_id, then a short write freeze and catalog switch).

8 Trade-offs

Shared tables with RLS vs database per tenant

Shared tables give high density and one migration path; RLS adds a small query overhead and needs discipline with connection pools. Database per tenant gives hard isolation and easy deletion and residency but means thousands of databases to migrate, monitor and pay for. Hybrid by tier is the usual answer.

Cells vs one big global deployment

Cells cap blast radius and allow residency, but add a routing layer, cross-cell operations (global search across tenants, admin reports) become harder, and capacity is fragmented. One global deployment is simpler until a single incident takes down every customer. At 50,000 tenants with enterprise SLAs, cells are worth it.

Shared LLM provider account vs per-tenant keys

A shared account with an internal gateway gives volume pricing and central control. Some enterprises want their own provider keys or a model in their own cloud (bring your own key/model); support it as a gateway routing option per tenant, not a different code path.

9 Bottlenecks

One tenant outgrows a cell

A tenant growing to 20% of a cell's load makes it hot. Monitor per-tenant share; above a threshold, move it to a dedicated cell with logical replication and a catalog switch, ideally before it becomes an incident.

Schema migrations across many databases

With dozens of cells and hundreds of silos, migrations take hours and can half-fail. Use expand-and-contract migrations (backward-compatible), a migration orchestrator that tracks version per database, rolling by cell with automatic pause on errors.

Global catalog as a single point of failure

Every request needs tenant → cell. Cache the catalog at the edge and in routers with long TTLs and serve stale on error; the catalog changes rarely.

10 Failure handling

A cell's database fails

Failover to the synchronous standby in another AZ (~30-60 s). Only that cell's tenants see errors; the status page is per cell. Other cells are untouched.

Cross-tenant leak bug shipped

RLS blocks the query from returning other tenants' rows even if the application forgot the filter; the leak test in CI should catch it first. If a leak happens, the audit log (with tenant and actor on every access) scopes the incident and notification.

LLM provider outage

The gateway fails over to a backup provider or model; the assistant degrades to plain search results with links if all fail. Core helpdesk functions do not depend on the LLM.

11 What impresses

  • Chooses a hybrid tenancy model by tier and explains why schema-per-tenant breaks at 50,000 tenants
  • Mentions the SET vs SET LOCAL connection-pool leak and RLS bypass by table owners
  • Extends isolation to semantic caches and vector retrieval, with the RAG leak scenario
  • Uses cells to cap blast radius and to satisfy residency, with a staged deploy per cell
  • Designs fair queuing so one tenant's bulk job cannot starve others
  • Proposes a permanent cross-tenant leak test in CI

12 Mistakes

  • Taking tenant_id from a request parameter instead of the authenticated token
  • Relying only on WHERE tenant_id = ? in application code with no second line of defence
  • Cache keys or vector searches that are not scoped by tenant
  • One global FIFO queue shared by all tenants
  • Picking database-per-tenant for 50,000 small tenants without considering operational cost
  • Ignoring that backups and logs also count for data residency

13 Follow-ups

Q1How do you move a large tenant from the shared pool to its own database with no downtime?
Q2How do you run an admin report across all tenants without breaking isolation?
Q3A tenant asks for full deletion. What exactly do you delete, including backups?
Q4How do you price and limit AI usage per tenant fairly?
Q5How would you test that no endpoint leaks data across tenants?
Q6How do you deploy a risky change safely across 20 cells?

Saved on this device. Create a free account to keep it on every device.

Practise this with Sam

A real mock interview, out loud, with an honest scorecard. Free to start.

Start free