Design a multi-tenant SaaS platform
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
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
3 API
| Endpoint | What it does |
|---|---|
POST/v1/tenants | Self-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-cell | Operator 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 cachetenant_id (PK)slug/subdomain (unique)regioncell_idtier (pooled|dedicated)planlimits jsonbkms_key_arnstatusThe 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_idRLS 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 cellkeys prefixed t:{tenant_id}:...Prefix is part of a shared helper, never hand-built; semantic cache entries scoped by tenant and model.
filesS3 per regions3://bucket-{region}/{tenant_id}/...IAM conditions or presigned URLs restricted to the tenant prefix; per-tenant KMS keys for enterprise.
usage_eventsKafka → ClickHousetenant_idmetric (api_call, ai_tokens, storage_gb)qtytscellFeeds quotas, billing and cost per tenant.
5 Architecture
Hover a box to highlight its connections.
6 Request flows
Agent asks the AI assistant a question
- 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.
- 2The cell router looks up t42 in the cached catalog and forwards to cell in4.
- 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.
- 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'.
- 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.
- 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
- 1Sales creates tenant BigBank with region=India, tier=dedicated, customer-managed KMS key.
- 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.
- 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.
- 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
Lessons this design uses
Practice questions
- You run a B2B SaaS API used by 5,000 companies. How do you design authentication, tenant isolation and fairness at the API layer?
- You are building a B2B SaaS helpdesk for 20,000 companies, from 5-person startups to banks. How do you isolate tenant data in the database?
- You run a B2B SaaS helpdesk with 50,000 tenants. Most are tiny, but 20 enterprise tenants hold 40% of the data. How do you partition the database?
- A bad config push took down your whole payments platform for all merchants at once. Leadership wants to limit blast radius. Would you move to a cell-based architecture? Explain how it works.
- In your multi-tenant SaaS, every tenant shares one Postgres cluster. One large customer started a big data import and now all other customers see slow pages. What do you do now, and how do you prevent it?
- A bug in one API endpoint returned another customer's invoices because a query missed the tenant filter. How would you use Postgres Row-Level Security to prevent this class of bug, and what are the pitfalls?
- In your multi-tenant SaaS, one large customer runs a bulk import and an expensive report at the same time, and every other customer sees timeouts. How do you design the platform so one tenant cannot hurt the others?
- Your SaaS has customers in India, the EU and the US. A large Indian bank requires its data to stay in India, and EU customers want data kept in the EU. How would you change the architecture?
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.