Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

AI System Design

Design a Multi-Tenant AI SaaS Platform

Learn AI system design by building an AI product serving many separate customer organizations from shared infrastructure.

The numbers to beatper-tenant quotacontains the blast radiusspiking tenantthrottled at their own limitevery other tenantunaffected

The whole design, in writing

Every step of the build above, written out: the problem each piece solves, the option that was taken and the ones that were not, the numbers, and how it fails in production.

The big idea

Shared infrastructure, zero cross-tenant leakage

Serving many separate customer organizations from one shared platform is efficient — until tenant A's private data ends up retrieved into tenant B's answer, or tenant C's traffic spike makes every other tenant's product feel broken. Multi-tenancy done right is invisible to every tenant; done wrong, it's a data breach or an outage waiting to happen.

Tenant Useruses the product
New in this step: Tenant User.

We'll build isolation as a first-class, multi-layer property — not application-level trust — plus the operational realities of running one platform for many customers: customization without duplication, noisy-neighbor prevention, cost attribution, and compliance-driven data residency.

What the new pieces do

Tenant Userclient
An end user at one customer organization, whose data and queries must never be visible to any other tenant sharing the same platform.

Step 1 · The baseline

One shared config, one shared index

Simplest version: every tenant's documents go into one shared vector index, and everyone uses the same prompt and configuration. What's the danger, specifically for a retrieval-augmented system?

Tenant-Aware APIevery request scopedTenant-Isolated Datashared, unscoped
New in this step: Tenant-Aware API, Tenant-Isolated Data.

All tenants' documents share one unscoped vector index. What's the specific danger?

  1. Storage capacity is a real, separate scaling concern, but it's not the IMMEDIATE danger — the immediate danger is far more serious than running out of disk.

  2. Without tenant scoping enforced on retrieval, similarity search has no concept of "whose data this is" — it just finds the closest-matching vectors regardless of tenant, meaning one tenant's private, confidential documents can genuinely surface in another tenant's query results. For a B2B SaaS product, this is often the single most catastrophic possible failure.

  3. Quality degradation from a larger shared index is a real but comparatively minor concern next to the much more severe risk of one tenant's confidential data literally appearing in another tenant's results.

An unscoped shared index risks one tenant's private documents surfacing directly in another tenant's retrieval results — a severe data breach, not a minor quality issue. Tenant isolation has to be enforced at the data layer, from the very first design decision, not treated as an afterthought.

Why this piece earns its place

The decision hiding under "one shared index" is a physical layout choice, and it is expensive to reverse. One index with a tenant field on every vector keeps memory in one place and one recall profile, but the filter has to be applied inside the search: applied to the neighbors after they come back, it can return nothing at all while that tenant’s own matching documents sit just outside the fetched window. An index per tenant makes the boundary structural instead, at the price of a fixed memory and build cost each, paid in full by your smallest customer. Neither shape is free to walk back later — moving one tenant across is a rebuild of their vectors while their users are still querying them. And a shared index shares its writes: one tenant loading their entire document history reshapes the neighborhood every other tenant searches and lengthens the rebuild everyone waits on. Size it for the largest corpus the plan allows a tenant to bring, not the one the median tenant actually brings.

What the new pieces do

Tenant-Aware APIgateway
Every request carries a tenant id that must be enforced at this layer AND every layer downstream — never trusted from just one place.
Tenant-Isolated Datadata
Documents, embeddings, and history — starts shared and unscoped; becomes independently tenant-checked at every single query.

Step 2 · Enforce isolation at the data layer

Every query, independently scoped

Fix Step 1: every retrieval and every data access must be scoped to exactly one tenant. Should this scoping be trusted to the application layer alone?

Tenant UserTenant-Aware APITenant-Isolated Data
The system as it stands at this step. · swipe to pan the diagram

Tenant scoping needs to be enforced somewhere. Is trusting the application/API layer alone sufficient?

  1. Trusting a SINGLE layer means any bug in that one layer's logic — a missed check, a copy-paste error, an edge case in how a tenant id gets threaded through — becomes a full cross-tenant data breach with no independent backstop.

  2. Independent, redundant enforcement at the data layer means even if the API layer's own tenant check has a bug, the data store's OWN check still blocks the cross-tenant access — a genuine defense-in-depth property that a single point of enforcement can never provide.

  3. Making tenant isolation opt-in treats a fundamental correctness guarantee as an optional add-on — every tenant's data needs to be protected from every other tenant by default, not as a premium feature some tenants might not select.

Enforce tenant scoping independently at the data layer, not just trusted from the API — every query to Tenant-Isolated Data carries and verifies a tenant id, checked by the store itself. This gives genuine defense-in-depth: a bug at any single layer doesn't automatically mean a full cross-tenant breach.

Why this piece earns its place

The check has to fail closed, and most data layers default to the opposite: a query built without a tenant predicate returns every row rather than raising. So what you want is not a WHERE tenant_id = ? added at each call site — that is the single layer again wearing a different hat — but a store that refuses a query carrying no tenant identity at all. Row-level security with the tenant set per session is the usual shape, and it brings its own trap: pooled connections outlive requests, so a session variable left set by the previous checkout is inherited by the next one. Set it inside the transaction, clear it on release, and exercise the pool under concurrency, because that bug only appears under load. The other cost is organizational. Once the store demands an identity, every background job, every analytics query and every support tool needs one too, and those are the paths that quietly earn an exemption. Keep exemptions as a short, named list of service identities rather than a branch that skips the check, because a list can be reviewed and a branch cannot.

Step 3 · Customization without duplicated infrastructure

Different prompts, one shared runtime

Tenants want different system prompts, different knowledge-base scopes, maybe different feature sets. Does each tenant need its own separately-deployed copy of the whole application stack?

Tenant UserTenant AdminTenant-Aware APITenant-Isolated DataPer-Tenant Config
New in this step: Tenant Admin, Per-Tenant Config. · swipe to pan the diagram

Tenants want meaningfully different behavior. Does each need a fully separate deployed stack?

  1. Deploying an entire separate stack per tenant doesn't scale operationally past a small number of customers — every code update, bug fix, or new feature would need to be deployed N times, once per tenant, which becomes unmanageable quickly.

  2. A shared runtime with tenant-scoped CONFIGURATION resolved per request gives each tenant meaningfully different behavior — different prompts, different feature flags, different retrieval scope — without needing to deploy or maintain a separate copy of the actual application for each one. Updates and fixes ship once, to everyone, automatically.

  3. No customization at all fails real B2B SaaS requirements — most enterprise customers reasonably expect some degree of tailoring (their own knowledge base, their own tone/prompt) that a one-size-fits-all product can't provide.

Use one shared runtime with Per-Tenant Config resolved at request time — prompts, retrieval scope, and feature flags differ per tenant, while the underlying application code and infrastructure remain shared. Tenant admins configure their own instance through this layer, without needing platform engineering to deploy anything separately.

Why this piece earns its place

Making behavior data rather than code means a tenant admin now performs something very like a deploy — no review, no staging, no rollback — every time they edit a prompt. Treat the config store as versioned and append-only for that reason, and stamp the resolved config version onto each request record beside the tenant id. Without it, "the product started answering strangely at four" is unanswerable, because the config that produced those answers has already been overwritten by the one you are reading. The hot path needs care too. A lookup per request makes the config store a hard dependency of every answer, so it gets cached, and the cache turns an admin’s correction into something that takes effect after a delay rather than at once — which they will report as a bug unless the interface says so plainly. Per-tenant flags also quietly multiply what you ship: there is no longer one product under test but one per combination of flags, and CI only covers the combinations somebody wrote down. Keep the flag set small, and delete a flag once its rollout is done.

What the new pieces do

Tenant Adminclient
Configures their organization's instance — prompts, knowledge base, quotas — without needing platform engineering involvement.
Per-Tenant Configdata
Each tenant's customization — system prompts, retrieval scope, feature flags — resolved at request time from one shared runtime.

Step 4 · Prevent noisy neighbors

One tenant's spike shouldn't degrade everyone else

All tenants share the same underlying infrastructure. One tenant suddenly sends 50x their normal traffic. Should that be allowed to affect other tenants' latency or availability?

Tenant-Aware APIevery request scopedTenant-Isolated Datascoped at every queryQuota Enforcerper-tenant limits
New in this step: Quota Enforcer.

One tenant spikes traffic dramatically on shared infrastructure. Should other tenants feel the effect?

  1. Accepting cross-tenant impact as "unavoidable" abandons a solvable problem — per-tenant quotas are specifically designed to prevent exactly this, so it isn't actually unavoidable, just unaddressed without them.

  2. Per-tenant quotas contain the blast radius of any single tenant's traffic pattern to that tenant alone — the spiking tenant experiences their own throttling, while the shared infrastructure's capacity for every other tenant stays exactly as available as before.

  3. Full suspension is a disproportionate response to a legitimate traffic spike (which might just mean the tenant's own product is having a busy day) — throttling to a fair quota, not shutting the tenant out entirely, is the appropriate response.

Add a Quota Enforcer: per-tenant rate limits and resource quotas mean a traffic spike from one tenant is contained to throttling at THEIR boundary, with zero impact on the latency or availability every other tenant experiences on the same shared infrastructure.

Why this piece earns its place

The unit you meter decides whether any of this works. Requests per second is the reflex and the wrong dial for an inference product, because one request can be a short lookup and the next a long document summarization — a tenant comfortably inside an RPS limit can still hold most of the shared model capacity. Meter the thing that actually runs out: tokens over a window, or concurrent in-flight inference slots. The enforcer then needs shared counter state on the hot path of every request, so decide explicitly what it does when that state is unreachable. Fail closed and a counter outage has become a platform outage. Fail open and a spike arriving inside that window is unbounded — on a platform that bills what it serves, that is attributable spend nobody authorized, arriving in the one window when the protection is off. Neither works as the only answer, which is the case for a local allowance each instance can enforce alone: loose, wrong at the edges, but bounded. The last call is queue or reject. Queuing a throttled tenant reads as friendlier, but held connections and buffered requests are themselves shared capacity, and a deep enough queue lets the neighbor back in through another door.

  • per-tenant quotacontains the blast radius
  • spiking tenantthrottled at their own limit
  • every other tenantunaffected

What the new pieces do

Quota Enforcerservice
Enforces per-tenant rate limits and resource quotas so one tenant's traffic can't degrade every other tenant's experience.

Step 5 · Attribute cost per tenant

Know exactly what each tenant actually costs

Tenants have vastly different usage patterns — some send a handful of requests a day, others thousands. Should pricing be flat regardless of actual usage?

Tenant-Aware APITenant-Isolated DataCost Attribution
New in this step: Cost Attribution. · swipe to pan the diagram

Tenant usage varies enormously. Should billing be a flat rate regardless of actual consumption?

  1. Flat pricing across wildly different usage levels either significantly overcharges light users (who may churn, feeling it's not worth the cost) or loses real money on heavy users (whose actual LLM/infra cost far exceeds what they're paying) — neither is sustainable at scale.

  2. Precise per-tenant cost tracking means pricing can actually reflect real consumption — light users pay less, heavy users pay proportionally more, and the platform's margins stay predictable regardless of how usage patterns vary across the tenant base.

  3. Aggregate-only cost tracking tells you the platform's overall margin but gives no way to price INDIVIDUAL tenants fairly or identify which specific tenants are actually profitable versus running at a loss.

Track Cost Attribution precisely per tenant, per request — the real LLM and infrastructure cost each tenant actually generates. This becomes the basis for usage-based billing that fairly reflects real consumption, rather than a flat rate that over- or under-charges depending on how each tenant actually uses the product.

Why this piece earns its place

Attribution is easy for costs that arrive per request and hard for everything else. Tokens through a model carry a tenant. The memory a vector index holds, an always-on embedding service, a reserved capacity floor you pay for whether or not anyone calls it — none of those decompose per request, so what you really have is a measured direct cost plus an allocated share, and the allocation rule you pick (by request count, by stored volume, by peak concurrency) decides which tenants look profitable. Write the rule down and keep it still, because changing it re-sorts the customer list with nothing real having moved. The subtler trap is that this meter and the Quota Enforcer look like one counter and are not. Enforcement needs a number in milliseconds and survives losing the odd event; an invoice needs a number that is exact at month end and can take an hour to settle. Build one counter for both and you get something too slow to gate a request with and too lossy to bill from. So run two paths off the same measurement, and decide in advance which of them the tenant is shown — because the throttling they felt this morning and the usage on next month’s invoice will not agree.

What the new pieces do

Cost Attributiondata
Tracks exact LLM/infra cost per tenant per request — the basis for accurate usage-based billing, not a flat guess.

Step 6 · Data residency and compliance

Some tenants' data can't leave a region

An enterprise tenant in the EU requires their data to stay within EU infrastructure for regulatory reasons. Can they be served from the same global deployment as every other tenant?

Tenant-Aware APITenant-Isolated DataRegion Router
New in this step: Region Router. · swipe to pan the diagram

Add a Region Router that directs a tenant's requests and storage to region-specific infrastructure when their compliance requirements demand it — a tenant with data-residency requirements is transparently routed to and stored in the correct region, while tenants without such requirements continue using the default global deployment. This is a per-tenant configuration choice, not a platform-wide architectural fork.

Why this piece earns its place

Routing is the easy half. The hard half is drawing the line between the control plane and customer content, because the platform needs one global view of who its tenants are, how they are configured and what they owe, while nothing they typed or uploaded may leave. Write that split down explicitly — identity and billing metadata global, content regional — because every feature built afterwards will be built by someone who has not thought about it, and a cross-region analytics table or a single support console listing everyone’s recent questions is the shape that fails an audit. The boundary therefore belongs in the data model, where a schema forces the question, and not in a routing table nobody opens. Two things get missed routinely. The inference call itself leaves the region if the model provider serves it from elsewhere, so your contract with that provider is part of the guarantee, not an implementation detail. And logs become customer content the moment a request body lands in one. Moving an existing tenant between regions is a migration with downtime, which is why the question is asked at signup.

What the new pieces do

Region Routerservice
Routes a tenant's requests and storage to region-specific infrastructure when their compliance requirements demand it.

Step 7 · Automated tenant onboarding

A new customer shouldn't need custom engineering

A new enterprise customer signs up. Should provisioning their isolated configuration, storage, and quotas require manual, bespoke engineering work each time?

Tenant Adminconfigures, onboardsTenant-Aware APIevery request scoped
New in this step: Tenant Admin → Tenant-Aware API.

Make onboarding self-service and automated: creating a new tenant's isolated config entry, data-store scope, and default quotas is a fast, automated provisioning flow the Tenant Admin can trigger themselves — not a manual, per-customer engineering task that doesn't scale as the platform grows its customer base.

Why this piece earns its place

Provisioning is a write across several systems — a config entry, a data-store scope, quota defaults, a billing record, an identity-provider entry — with no transaction spanning them. A failure halfway leaves a tenant that half exists, which is worse than one that does not, because some code paths find it and others do not. So make the flow idempotent and keyed on the tenant id, safe to re-run from the beginning after any failure, and keep the tenant unusable until a final step marks provisioning complete. Build it as a declared sequence of steps rather than a script and the audit comes free: what a tenant has is what the sequence says it has, so a drifted tenant is a diff rather than an investigation. The first tenants to show a diff are the ones created before the flow existed, assembled by hand and carrying a missing quota row or a config key nothing sets any more — backfill those the day you automate, because nothing else will ever look at them again. And self-service means a sign-up form now creates infrastructure. Put verification and a rate limit in front of it, or the tenant table becomes an abuse surface, and every abandoned tenant still carries whatever a tenant costs to hold.

Step 8 · The sharp edges

Defense in depth, and genuine deletion

Two of the highest-stakes real-world requirements: a bug in any single isolation layer must not cause a full cross-tenant breach, and when a tenant leaves, their data needs to actually, verifiably disappear.

Tenant UserTenant AdminTenant-Aware APITenant-Isolated DataPer-Tenant ConfigQuota EnforcerCost AttributionRegion Router
The system as it stands at this step. · swipe to pan the diagram

Isolation is enforced independently at multiple layers — API-level tenant scoping AND data-layer tenant verification — so a bug in either one alone doesn't produce a full breach, as demonstrated by the Step 2 chaos scenario. For tenant offboarding, deletion must be genuine and verifiable across every store the tenant's data touched (documents, embeddings, config, logs, backups) — not simply marking the tenant inactive while their data quietly persists, which fails compliance requirements like the GDPR right to erasure.

Why this piece earns its place

Two layers only buy you something if they can be wrong independently, and the easy mistake is to draw one layer twice. If the API check and the store check both read the tenant id out of the same header through the same shared helper, a bug in that helper is agreed with by both of them and the second check adds nothing but comfort. Independence has a price worth naming out loud: the identity the store verifies has to reach it in a form the API cannot simply retype — a token minted when the user authenticated and signed, carried with the request and checked by the store against its own key; or, where the store can only know what the connection tells it, credentials issued per tenant, which buys the independence by giving up the one shared pool. Then a wrong id upstream fails verification instead of being agreed with, and that disagreement is the whole product of the design. Deletion has the symmetric property: it is the one path that destroys data on purpose, so a scoping bug there is the only unrecoverable one in the system. Give it a grace period before anything really goes, and make it print what it would remove before it removes anything — the one code path worth a dry run every single time it runs.

You did it

You just designed a multi-tenant AI SaaS platform.

Tenant UserTenant AdminTenant-Aware APITenant-Isolated DataPer-Tenant ConfigQuota EnforcerCost AttributionRegion Router
The finished design, end to end. · swipe to pan the diagram

Everything you assembled, in order

  • An unscoped shared index risks one tenant's private data surfacing directly in another tenant's results — a severe breach, not a minor bug.
  • Tenant isolation is enforced independently at multiple layers (API and data store), so a bug in any one alone doesn't cause a full breach.
  • Per-tenant customization lives in configuration resolved at request time, on one shared runtime — not a separately deployed stack per tenant.
  • Per-tenant quotas contain the blast radius of any single tenant's traffic spike to that tenant alone.
  • Precise per-tenant cost attribution is the prerequisite for fair, usage-based billing across widely varying tenant usage patterns.
  • A region router transparently serves tenants with data-residency requirements from appropriate regional infrastructure.
  • Automated, self-service tenant onboarding is what lets the customer base scale independent of platform-engineering headcount.
  • Tenant offboarding requires genuine, verifiable deletion across every data store, not just marking a tenant inactive.

Deep cut · 24:52

Deleted in March. Leaked in May.

The interactive build above lays out a multi-tenant AI SaaS platform: tenant-scoped data checked at the API and again at the data layer, one shared runtime with per-tenant config, per-tenant quotas against the noisy neighbor, per-tenant cost attribution, a region router for data residency, and onboarding and genuine deletion. The film builds the same platform one fix at a time in a made-up app’s post room, then replays one leak from the request log to find why a company deleted in March was quoted to its rival in May, with both tenant checks working.

  • See the wreck: the answer cache was filed under the question alone. It sat in front of both tenant checks, served one company’s answer to another company’s customers, and carried no company name — so the delete button had nothing to find. The request log and the backups still held the deleted company too.
  • Take it into the interview: every copy carries the tenant ID — cache keys, logs, embeddings, backups — and every store checks it itself; keep a register of every place tenant data goes, have deletion walk it and verify each one, and shred each tenant’s backup key.

Where an interviewer pokes next

Getting the boxes right is the easy half. These are the questions that separate a candidate who drew the diagram from one who has run the thing. Answer each one out loud before you open it.

  1. Why enforce tenant scoping at the data layer when the API layer is supposed to already handle it correctly?

    "Supposed to" is exactly the gap defense-in-depth closes — any single layer, however carefully built, can have a bug: a missed check on one code path, a tenant id that gets dropped during an internal service call, an edge case in a refactor. If the data layer independently verifies tenant scoping on every query regardless of what the API layer intended, a bug in the API layer's logic gets caught before it becomes an actual cross-tenant data exposure, rather than being the SINGLE point of failure standing between correct behavior and a full breach.

  2. How would this design change for a tenant that wants their own fine-tuned model, not just a different prompt?

    The same "shared runtime, tenant-scoped configuration" principle extends naturally: Per-Tenant Config can reference which model/fine-tune to use for that tenant's requests, alongside their prompt and retrieval scope, and the shared runtime routes inference calls to the correct model per tenant at request time. The genuinely new engineering concern this introduces is on the MLOps side (see Model Registry) — now you're also versioning and managing per-tenant fine-tuned models, not just per-tenant text config — but the tenant-isolation and routing architecture itself doesn't need to fundamentally change, just the configuration's scope.

  3. Could per-tenant quotas themselves become a source of unfairness — what if a tenant's legitimate usage naturally exceeds their default quota?

    This is why quotas are typically tiered by subscription/contract level rather than one fixed number for every tenant — an enterprise customer with genuinely higher legitimate usage needs is provisioned a correspondingly higher quota as part of their plan, while quotas still exist specifically to prevent any ONE tenant (regardless of their tier) from degrading others beyond their own allocated share. The quota's purpose isn't to artificially cap legitimate usage — it's to make each tenant's resource consumption predictable and bounded relative to what they've actually been provisioned, which can and should differ by tenant.

  4. Why is verifiable deletion specifically hard, compared to just deleting a database row?

    A tenant's data typically exists in multiple places beyond a primary database — vector embeddings in a search index, cached copies, backup snapshots taken before the deletion request, logs that may have captured request content, and potentially data replicated across regions (Step 6). "Verifiable" deletion means actually confirming removal across ALL of these, including backups (which often have their own retention lifecycle) and any downstream systems that might have copied the data — a single database DELETE statement only addresses the most obvious, primary copy, which is exactly why this is a genuinely harder operational and compliance problem than it initially sounds.

  5. What's the relationship between the Region Router (Step 6) and the Quota Enforcer (Step 4) — could regional infrastructure end up unevenly loaded?

    Yes, this is a real operational consideration: if data-residency requirements route a disproportionate number of high-usage tenants to one specific region, that region's shared infrastructure could face different load characteristics than a region serving fewer or lighter tenants. The Quota Enforcer's per-tenant limits still apply consistently regardless of region, but CAPACITY PLANNING for each region's underlying infrastructure needs to account for its own tenant mix, not assume uniform load across every region — this is an operational/capacity concern layered on top of the architectural routing decision, not a flaw in the routing logic itself.

Check yourself — the answers, and why

Nine steps in, these are the calls you should be able to make cold. Pick one, then read why.

  1. An unscoped shared vector index across tenants is dangerous mainly because…

    Retrieval has no concept of tenant boundaries unless explicitly scoped — this is often the most severe possible failure for a B2B SaaS platform.

  2. Tenant isolation should be enforced…

    Defense in depth means a single point of failure can't cause a complete cross-tenant data breach.

  3. Per-tenant customization is handled via…

    Separating what varies (config) from what stays shared (application code) scales far better than duplicating infrastructure.

  4. Per-tenant quotas exist to…

    Shared infrastructure shouldn't mean a shared blast radius — this is the noisy-neighbor problem's standard fix.

  5. Tenant offboarding requires…

    Compliance requirements like the right to erasure demand real deletion, not just deactivation, across all copies of the data.

How you’d open this design in an interview

Before any boxes: agree what it must do, pin the qualities that shape everything, then build — naming each trade-off as you make it. The walkthrough above is that exact order.

What it must do

Agree on these before drawing a single box.

  • Serve tenants: answer each tenant user’s queries from shared infrastructure, scoped to their organization only.
  • Isolate data: every retrieval and data access is scoped to exactly one tenant, verified at the data layer.
  • Customize: per-tenant prompts, retrieval scope, and feature flags from config, on one shared runtime.
  • Meter usage: track LLM and infrastructure cost per tenant per request for usage-based billing.
  • Onboard / offboard: self-service tenant provisioning, and genuine, verifiable deletion when a tenant leaves.

The qualities that shape everything

Each one names the mechanism that buys it.

No tenant’s data ever surfaces in another’s answer
Tenant scoping enforced independently at the data layer — every query carries and verifies a tenant id, checked by the store itself, not trusted from upstream.
Survive a bug in any single isolation layer
Defense in depth — isolation enforced at both the API and data layers, so one missed check doesn’t become a full cross-tenant breach.
Different tenant behavior without N deployments
One shared runtime resolves per-tenant config (prompts, retrieval scope, feature flags) at request time; fixes ship once to everyone.
One tenant’s spike can’t degrade the rest
A Quota Enforcer applies per-tenant rate limits, so a noisy neighbor is throttled at their own boundary — shared infrastructure, not shared blast radius.
Bill fairly across wildly different usage
Per-tenant, per-request cost attribution tracks the real LLM/infra cost each tenant generates, as the basis for usage-based billing.
Meet data-residency / compliance rules
A Region Router directs a tenant’s requests and storage to region-specific infrastructure when their compliance requirements demand it.

The trade-offs you say out loud

Senior signal isn’t the boxes — it’s naming what you gave up and why it was the right price.

Independent data-layer scoping over trusting the API layer alone

A single enforcement point makes any bug in that one layer a full cross-tenant breach. The data store verifying tenant id on every query is an independent backstop the API layer can’t provide.

Shared runtime + per-tenant config over a separate deployed stack per tenant

Deploying a full stack per tenant means shipping every fix N times — unmanageable past a handful of customers. Config resolved per request differs behavior while code and fixes stay shared.

Per-tenant quotas over accepting cross-tenant impact as unavoidable

Throttle a spiking tenant at their own boundary and every other tenant is untouched. Shared infrastructure doesn’t have to mean a shared blast radius — the noisy-neighbor problem is solvable, not inherent.

Usage-based billing from per-tenant cost over a flat rate for everyone

Flat pricing over wildly varying usage overcharges light users into churn and loses money on heavy ones. Tracking real per-tenant cost lets price track consumption and keeps margins predictable.

Genuine, verifiable deletion over marking the tenant inactive

A tenant’s data lives in embeddings, caches, backups, logs and replicas — deactivation leaves it all in place. Right-to-erasure compliance needs confirmed removal across every store the data touched.

The answer, out loud

What a strong answer to “Design a Multi-Tenant AI SaaS Platform” sounds like, first question to last trade-off. It is about 7 minutes of talking; the whiteboard and the interviewer fill the rest of the 45. Read it aloud once, then close the page and give it yourself.

  1. 0–3 min

    Pin down who shares what

    Let me pin down the word tenant first: one customer organization with its own users, its own documents and its own contract. Many of them, one platform, one codebase. Before I draw I’d want to know how many tenants there are and how unequal they are, because a few large enterprises and a long tail of small accounts pull this in different directions. Then I’d build in the order the decisions constrain each other: isolation first, because it’s the one I can’t retrofit cheaply.

  2. 3–7 min

    The skeleton, and the leak it hides

    The skeleton is a tenant user, a tenant-aware API, and a data store holding documents, embeddings and history. The simplest version drops every tenant’s documents into one index behind one prompt, and retrieval hands back whatever sits closest to the question. Closest is the problem: nearness is computed over text, and nothing in a vector records who owns it, so tenant B’s question can surface tenant A’s contract inside B’s answer — and A and B may be bidding against each other. Isolation gets settled here.

    Built in step 1: One shared config, one shared index
  3. 7–12 min

    Scope every query, twice

    Every request carries a tenant id, and the tempting design has the API attach it and everything downstream trust it. I wouldn’t: trust means one place has to stay right forever. So the store checks too — a query arrives with a tenant id or it doesn’t run. What I’m buying isn’t a better check, it’s two that can disagree, and each refusal the store makes means something above it sent the wrong id. I’d rather hear that from an alert than from a customer.

    Built in step 2: Every query, independently scoped
  4. 12–17 min

    Different behavior, one runtime

    Tenants want their own prompt, their own knowledge base, sometimes their own features. What I’d avoid is a deployment each: the first security patch becomes a rollout I chase customer by customer, and the ones left behind are the ones who never ask for anything. So one runtime, with what differs read from config at request time. The second reason I want that is auditing — with a stack per tenant I’d have N copies of the check we just added and no guarantee they agree.

    Built in step 3: Different prompts, one shared runtime
  5. 17–22 min

    Contain the noisy neighbor

    Everyone is on the same capacity, so a tenant sending far more than usual is spending someone else’s latency. A quota enforcer gives each tenant a bound and makes them the one who feels it. Past fairness, it’s the only way I can promise anybody anything: a latency commitment on shared infrastructure isn’t backable unless every tenant’s consumption is bounded. The limit nobody revisits is the trap: a tenant who grows into theirs is throttled for succeeding, so raising one has to be an account decision, not an engineering ticket.

    Built in step 4: One tenant's spike shouldn't degrade everyone else
  6. 22–26 min

    Know what each tenant costs

    Tenants don’t use this evenly, and the spread is wide enough that no single price is right at both ends of it. So I’d attribute model and infrastructure cost per tenant, per request. On an AI product the cost of a request isn’t fixed: it’s mostly tokens, and tokens are partly a function of the config we just handed them — a tenant whose prompt drags half their knowledge base into context costs a multiple of one with a tight prompt. Per-tenant cost is the feedback loop on customization, not only a billing input.

    Built in step 5: Know exactly what each tenant actually costs
  7. 26–30 min

    Where the data is allowed to live

    Some tenants can’t let their data leave a region — an EU enterprise under regulatory requirements. Rather than fork the platform or drag everyone into the strictest region, a region router sends that tenant’s requests and storage to regional infrastructure while everyone else stays on the default. It’s a per-tenant setting with a bill attached: a second region is duplicated infrastructure serving fewer tenants, so the cost attribution from a minute ago tells me whether residency gets priced or absorbed.

    Built in step 6: Some tenants' data can't leave a region
  8. 30–34 min

    Onboarding without an engineer

    A customer signing up shouldn’t need anyone on my team. If standing a tenant up is a ticket rather than a flow, then how fast we can sell is set by who I can spare that week, and the answer to a good quarter is hiring engineers to type. I’d also push our own demos and trials through that same flow rather than a shortcut, because a provisioning path that only runs when a real customer signs is one nobody has exercised this month.

    Built in step 7: A new customer shouldn't need custom engineering
  9. 34–39 min

    Getting a tenant back out

    By now the isolation story is told, so what’s left is the other end. Marking a tenant inactive is a flag, not a deletion, and the difference only shows up when a regulator or the customer’s own lawyer asks. The hard part isn’t the delete — it’s being able to say a year later that it happened, to someone who wasn’t there and has no reason to take my word for it. So I’d build that path while there’s no urgency on it.

    Built in step 8: Defense in depth, and genuine deletion
  10. 39–43 min

    What I’d watch, and how it fails

    The dashboards here are per tenant or they’re lying to me: platform-wide latency and error rate stay green while one tenant is completely broken, because the large tenants set the average. So request rate against quota, throttling, cost and errors, sliced by tenant. And a tenant sitting on their quota is a commercial event, not an engineering one — the system is doing exactly what it was built to do, and the person who needs to know owns that account.

  11. 43–45 min

    Close on the trade-off

    To close: scoping at the API and again inside the store, one runtime shaped by per-tenant config, quotas and cost attribution per tenant, region routing for residency, onboarding and offboarding that run without me. Each trades some of the efficiency of sharing for isolation a tenant can rely on, and where it’s close I’d take isolation. With more time I’d take on the tenant who wants to hold their own encryption key — it makes offboarding a key I destroy rather than a process I run.

What this teaches

Learn AI system design by building an AI product serving many separate customer organizations from shared infrastructure. An interactive guide covering why unscoped shared retrieval risks one tenant's private data leaking into another's answers, enforcing tenant isolation at the data layer (not application trust), per-tenant customization via configuration instead of duplicated infrastructure, noisy-neighbor prevention with per-tenant quotas, per-tenant cost attribution for accurate billing, data-residency routing for compliance, automated tenant onboarding, and defense-in-depth isolation that survives a bug in any single layer.

Key takeaways

  • An unscoped shared index risks one tenant's private data surfacing directly in another tenant's results — a severe breach, not a minor bug.
  • Tenant isolation is enforced independently at multiple layers (API and data store), so a bug in any one alone doesn't cause a full breach.
  • Per-tenant customization lives in configuration resolved at request time, on one shared runtime — not a separately deployed stack per tenant.
  • Per-tenant quotas contain the blast radius of any single tenant's traffic spike to that tenant alone.
  • Precise per-tenant cost attribution is the prerequisite for fair, usage-based billing across widely varying tenant usage patterns.
  • A region router transparently serves tenants with data-residency requirements from appropriate regional infrastructure.
  • Automated, self-service tenant onboarding is what lets the customer base scale independent of platform-engineering headcount.
  • Tenant offboarding requires genuine, verifiable deletion across every data store, not just marking a tenant inactive.

Concepts covered

  • Shared infrastructure, zero cross-tenant leakage
  • One shared config, one shared index
  • Every query, independently scoped
  • Different prompts, one shared runtime
  • One tenant's spike shouldn't degrade everyone else
  • Know exactly what each tenant actually costs
  • Some tenants' data can't leave a region
  • A new customer shouldn't need custom engineering
  • Defense in depth, and genuine deletion
RUN IT YOURSELF

Overfetch turns the fleet average green and leaves the smallest tenants short on every query

One shared vector index over 600 rows owned by four tenants of very unequal size, searched two ways — the tenant filter applied after the search, and applied before it — running for real in your browser. It prints how full a page each tenant receives, then the fleet number an operator would actually look at, and the two disagree. Change OVERFETCH, hit Run, and watch the line between the tenants it fixes and the tenants it does not move with it.

HOW TO READ THE CODE — 5 IDEAS
  1. A similarity search ranks on distance alone — nothing in a vector records who owns it (step 1), so a tenant's expected rows inside a window of fetch is its share of the index × fetch, and its relevance never enters.
  2. That makes the shortfall a closed form, printed above: post-filtering fills the page for tenants whose share exceeds k/fetch and for nobody below it. At 10× overfetch the line is 10.0% — acme and borealis clear it, cinder and dunlin are short on 100% of their queries.
  3. The fleet row is where it hides. Overfetch takes fleet-wide short from 100% to 8% — which is precisely cinder + dunlin's 8.3% of the corpus, i.e. the smallest accounts and no one else. Nothing errors: the page is short, not broken, and the dashboard is green.
  4. Overfetch also converts empty into short rather than fixing it: dunlin goes from 72% of queries returning nothing to 0% — and 3.36 results out of the 10 it asked for, every time (step 1).
  5. Every fetched row that was not the caller's left the store and was dropped by the caller: 4.39 foreign rows per query, 46.38 under overfetch. That is the single point of enforcement step 2 refuses — pre-filtering makes the store scope the search, returns min(k, tenant size), and ranks zero foreign rows.
CPython · WebAssembly
built so a bug in one layer never lets tenant A see tenant B's data — make the calls, spike a noisy neighbor, run the gauntlet.
Finished this one? 0 / 65 AI System Designs done
NextDesign a Conversational AI Closest AI system design to this one

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More AI System Designs