On this page
Before you start
This lesson uses ideas from these earlier lessons. Read them first if any idea is new to you.
01Why interviewers ask this
The interviewer says: "You build accounting software that many companies use. Each company must see only its own data. Some companies are huge, and most pay very little. How do you store the data?"
A weak answer says "one database for each customer" or "add a customer column", and stops. A strong answer compares the layouts on leaks, cost, schema changes, restore and delete. It also says how one big customer is kept from slowing the others, and how that customer can move to its own place when it grows.
At SDE-2 level you should know the layouts and how a leak happens. At SDE-3 level you should also handle the noisy neighbour, routing and moving a tenant.
02The idea in plain words
Think of a building. In one layout, everyone's boxes sit on the same shelves in one big hall, and every box has a name label. In another, each family has its own locker room in the same hall. In the third, each family has its own house.
A tenant is one customer organisation that has many users, for example one company that uses your software. There are three layouts for tenant data.
- Shared tables. All tenants use the same tables. Every row has a
tenant_idcolumn. - Schema per tenant. A schema is a named set of tables inside one database. Each tenant gets its own schema.
- Database per tenant. Each tenant gets its own database.
Compare them on seven questions. A restore brings data back from a backup.
| Question | Shared tables | Schema per tenant | Database per tenant |
|---|---|---|---|
| Leak | One missing filter leaks | Own tables, but a wrong schema name still leaks | Own database, strongest |
| Cost per tenant | Lowest | Low | Highest: a fixed cost for each database |
| Schema change | One change, on a huge table (lesson 41) | The change runs once for each schema | The change runs once for each database |
| Restore one tenant | Hard | Easier | Easiest, if each database has its own backup |
| Delete one tenant | Delete rows from every table | Drop the schema | Drop the database |
| One heavy tenant slows the others | Worst | Same server: no help | Helps only on its own server |
| Where the data lives | One place | One place | Any place you choose |
The mix is common. Small tenants share tables. Each large tenant, or each tenant with strict rules, gets its own database. A table that maps each tenant to its data keeps this simple.
Restore needs care. In Postgres, restoring to a point in time restores the whole database server, not one part of it (see Sources). So you restore a copy on the side and pull out one tenant. That is hard with shared tables, because the tenant's rows sit in many tables. With a schema or a database for each tenant, you copy one unit. A separate backup of each database makes this faster still. Deleting a tenant needs care too: a contract or a law may say the data must be gone. Old shared backups still hold it until they expire.
Keeping tenants apart in shared tables
Three rules, each one stops a different leak.
- Tenant ID first in every index that serves tenant queries. An index on
(tenant_id, created_at)finds one tenant's rows for a month at once (lesson 36). An index oncreated_atalone reads every tenant's rows for that month and drops the others after. Puttenant_idfirst in primary keys and unique rules too, so two tenants can both have invoice number 1. - Tenant ID in every cache key. A cache key is the name under which a cache stores a value. The key
dashboard:2026-03gives one tenant's dashboard to the next tenant who asks. Uset42:dashboard:2026-03. - Row-level security. The database adds the tenant filter to every query, so one forgotten filter cannot leak. In Postgres, once you switch it on for a table with no policy, nobody sees any row.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY; -- the owner obeys it too
CREATE POLICY same_tenant ON invoices
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::bigint);
-- At the start of each request, inside its transaction:
SELECT set_config('app.tenant_id', '42', true); -- true: lasts for this transaction onlyThe policy does not apply to a superuser or to a role with the bypass attribute. It does not apply to the owner of the table unless you force it. So the application must connect as an ordinary role. Two more traps:
- Connection pools. A pool gives the same connection to different requests. A setting that stays on the connection (a plain
SET) would give the next request the previous tenant. The setting above lasts for one transaction only. TheNULLIFmakes a request with no tenant see no rows, not an error. - Views. A view can run with its owner's rights and skip the policy.
Add a test for every table and view: read as tenant A and expect zero rows of tenant B.
The noisy neighbour
A noisy neighbour is a tenant that uses so much of a shared resource that others slow down. A schema for each tenant does not fix it, because all schemas share one database server. A per-tenant limit does: use a token bucket (lesson 84) with the tenant ID as the key, and set its size by the tenant's plan. A request limit does not stop one very heavy query, so also set a time limit for each query. A tenant with strong needs can get its own database server, which separates the load.
Finding the tenant's data
Each request must reach the right data. The tenant name comes from the sub-domain (acme.example.com) or from the login token. A lookup table then maps the tenant to its database. Each app server keeps a copy in memory for a few seconds, so a moved tenant is found quickly.
This is sharding by tenant (lesson 69): the shard key is the tenant ID. A hash spreads the tenants evenly, but tenants differ a lot in size. So a lookup table (a directory) is better here, because you can place one huge tenant alone. To move a large tenant, use the four steps of lesson 69: copy its rows, keep sending new changes, switch its entry in the table (pause that tenant's writes for a moment), delete the old copy.
Want to check your understanding? Try explaining this idea in your own words. You can continue reading first, or ask the AI tutor a question. Sign up free to ask the tutor
03Pause and predict
Pause. Guess first.
A shared table has 300,000,000 rows from 1,500 tenants over 36 months. An index on
created_atalone is used to find one tenant's rows for one month. About how many index entries does the database read, compared with an index on(tenant_id, created_at)?
Write your guess before you read on. The answer is in the worked example.
04Worked example
KhataDesk is accounting software. Assumptions (made up to teach with): 1,500 tenants. 1,470 are small and pay Rs 1,500 a month. 30 are large and pay Rs 100,000 a month. One table of invoice lines has 300,000,000 rows of 500 bytes, spread evenly over 36 months. The biggest tenant, Metro Traders, has 20,000,000 rows.
Step 1: the index (the predict box). Rows a month for all tenants: 300,000,000 / 36 = 8,333,333. For an average tenant: 300,000,000 / 1,500 = 200,000 rows, and 200,000 / 36 = 5,556 a month. Index on created_at alone: 8,333,333 entries read to keep 5,556, which is 1,500 times more (8,333,333 / 5,555.6). With tenant_id first: 5,556.
Step 2: cost. Assumption: the shared cluster costs Rs 300,000 (3 lakh) a month, which is 300,000 / 1,500 = Rs 200 for each tenant. We keep it in the mix (slightly high). A small database costs Rs 6,000 a month. For 1,500 tenants that is Rs 9,000,000, which is 30 times more. The 1,470 small tenants pay 1,470 x 1,500 = Rs 2,205,000, but their databases would cost 1,470 x 6,000 = Rs 8,820,000, four times their income. Assumption: a large database costs Rs 15,000. For 30 large tenants that is Rs 450,000, which is 15% of their Rs 3,000,000. The mix costs 300,000 + 450,000 = Rs 750,000.
Step 3: a schema change. With schemas, 1,500 x 40 tables = 60,000 tables. Assumption: one change takes 2 seconds in one schema. One after another: 1,500 x 2 = 3,000 seconds = 50 minutes. With 10 at a time: 5 minutes. If 1% fail, 15 tenants stay on the old shape, so the code must work with both shapes (lesson 41).
Step 4: restore one tenant. The shared database is 300,000,000 x 500 bytes = 150 GB. At 250 MB per second, copying the backup back on the side takes 150,000 / 250 = 600 seconds = 10 minutes (replaying the log adds time). Then you copy one tenant's rows back, table by table. An average tenant has 200,000 x 500 bytes = 100 MB, which loads in 100 / 250 = 0.4 seconds from the backup of its own database.
Step 5: delete Metro Traders. In shared tables: delete its 20,000,000 rows in this one table in batches of 10,000, which is 2,000 batches. At 0.2 seconds of work and 0.2 seconds of pause: 2,000 x 0.4 = 800 seconds = 13.3 minutes (lesson 41). With its own database: drop it.
Step 6: a noisy neighbour. Assumption: the database handles 3,000 queries per second, and normal load is 1,200. One tenant's script sends 2,500 per second. The total is 3,700, so everyone slows. A bucket of size 300 that refills 150 per second lets through at most 300 + 150 x 60 = 9,300 queries in a minute, not 150,000. The total stays near 1,350.
Step 7: move Metro Traders. Its rows in this table: 20,000,000 x 500 bytes = 10,000 MB. A slow copy at 20 MB per second takes 10,000 / 20 = 500 seconds = 8.3 minutes. Then the changes made during the copy are applied, and its entry in the lookup table is switched.
What companies wrote
- Netflix wrote in October 2026 (post listed 22 Oct 2026) that containers from many teams share its hosts. Here the tenants are teams, not customer companies. A few demanding real-time services run in a mode that gives each task its own virtual machine, with the same API and deployment path. The post says a shared host cannot separate every shared resource, such as memory bandwidth. Link: https://netflixtechblog.com/single-tenant-titus-vm-level-isolation-on-a-multi-tenant-container-platform-6d8b1909d163 (opens in a new tab)
- Notion wrote on 6 Oct 2021 that it splits data by workspace ID. It uses 480 logical shards, which are Postgres schemas, spread over 32 physical databases. A shard is not one customer: it holds the data of many workspaces. Link: https://www.notion.com/blog/sharding-postgres-at-notion (opens in a new tab)
05Trade-offs
Shared tables solve cost (Rs 200 a tenant, not Rs 6,000) and give one schema change. They cost leak risk, hard restore and delete (13.3 minutes of batches), and a noisy neighbour. A database for each tenant solves isolation and restore. It costs 30 times the bill if used for everyone, and one change many times. I would use shared tables for small tenants and a database for each large or regulated tenant. I would change a tenant's place when it uses a large share of the load, when a contract asks, or when its revenue pays for a database.
06Common misconceptions
"A schema for each tenant fixes the noisy neighbour." All schemas share one server. In step 6, only a limit or a separate server helped.
"Row-level security means no tenant filter can leak." A superuser, a bypass role or the table owner skips it. The application role must not be one of them, and every table needs a test.
"A database for each tenant is the safest, so use it for all." Here it cost four times the income of the small tenants, and a schema change ran 1,500 times.
07How to say it in the interview
Say it like this
I would keep small tenants in shared tables with the tenant ID first in its indexes and in every cache key, plus row-level security so one forgotten filter cannot leak. Large or regulated tenants get their own database, found through a lookup table, and every tenant has a request limit.
08Check your understanding
- Name the three layouts. Why must
tenant_idcome first in an index? What is a noisy neighbour? - A shared table has 120,000,000 rows from 800 tenants over 24 months. (a) How many entries does a query for one tenant and one month read with an index on
created_atalone, and with(tenant_id, created_at)? (b) A shared cluster costs Rs 200,000 a month and a database costs Rs 5,000. What is the cost for each of the 800 tenants in each layout? - A team turns on row-level security, and the app connects as the owner of the tables. A test with two tenants still passes. What is wrong?
09Answers
Show the answers
- Shared tables, schema per tenant, database per tenant. With
tenant_idfirst, the index finds one tenant's rows without reading the other tenants' rows. A noisy neighbour is a tenant that uses so much of a shared resource that others slow down. - (a) 120,000,000 / 24 = 5,000,000 rows a month. With
created_atalone, 5,000,000 entries. Withtenant_idfirst: 5,000,000 / 800 = 6,250. (b) Shared: 200,000 / 800 = Rs 250. Database: Rs 5,000, which is 20 times more. - The owner skips the policy unless the table is forced, so the test passes only because the app code still adds its own filter. Connect as an ordinary role, or force the policy. Then add a test that leaves out the app filter and expects zero rows of the other tenant.
10Summary card: multi-tenancy
One-page summary
- A tenant is a customer organisation. Layouts: shared tables (
tenant_idon every row), schema per tenant, database per tenant, and the mix (small shared, large alone).- Compare on leak, cost, schema change, restore, delete, heavy tenants and where the data lives. Example: Rs 200 against Rs 6,000 for each tenant; a schema change took 1,500 x 2 s = 50 minutes.
- In shared tables:
tenant_idfirst in tenant indexes, in every cache key, and in row-level security. The app role must not bypass it, and a pool must not keep the tenant between requests.- Noisy neighbour: a limit for each tenant (token bucket). A schema does not fix it.
- Find data with a lookup table from tenant to database; moving a tenant is copy, follow changes, switch, delete.
- Trap: trusting every query to add the filter.
11Sources
- Microsoft Learn, "Tenancy models for a multitenant solution" (page dated 11 Jun 2025; a commercial decision; a tenant-to-deployment mapping; dedicated resources help against noisy neighbours but cost more; mixed models; test isolation): https://learn.microsoft.com/en-us/azure/architecture/guide/multitenant/considerations/tenancy-models (opens in a new tab) (checked 26 Oct 2026). Microsoft Learn, "Noisy Neighbor antipattern" (quotas and throttling by tenant): https://learn.microsoft.com/en-us/azure/architecture/antipatterns/noisy-neighbor/noisy-neighbor (opens in a new tab) (checked 26 Oct 2026).
- PostgreSQL manual, row security policies (no policy means no rows; superusers, bypass roles and owners skip it unless forced): https://www.postgresql.org/docs/current/ddl-rowsecurity.html (opens in a new tab) (checked 26 Oct 2026). The setting in the code block is a common way to carry the tenant ID, not a quote from the manual. PostgreSQL manual, continuous archiving (a restore applies to the whole cluster, not a part): https://www.postgresql.org/docs/current/continuous-archiving.html (opens in a new tab) (checked 26 Oct 2026).
- Netflix: link in the box above (read on 26 Oct 2026 from the company's blog feed, because its page refused automated access). Notion: link in the box above (checked 26 Oct 2026).
- All tenant counts, prices, row counts, speeds, times and query rates are assumptions made for teaching. The arithmetic is calculation.
Try what you just learned
Put this idea into practice with a short exercise or a relevant tool. Free to try. No sign-up needed.
More practice. This needs a free account.
What to learn next
Continue learning with the lessons that build on this one.
- Collaborative editing: operational transformation or CRDT
- Cost engineering: cost per request as a design input
- How a log-based queue stores data: segments, offsets, retention, compaction, replication
Practise this idea
Have a question about this lesson?
Ask the AI tutor to explain a difficult idea, walk through an example or check your understanding. It uses the lesson to help you work through the topic.
A free account includes 10 AI tutor questions a day. The lesson remains free to read without an account.
All lessons in Module 23: Big systems in depth
Prefer another topic? Browse all 169 lessons.