Skip to content

Scenario: multi-tenant SaaS

Pick what tenants share layer by layer, give shared workers turns, run migrations as N jobs, and scope every copy of a row.

Predict first

Before you read: predict

ExamHall grades essays from a shared job queue served by 4 workers, and each grading job takes 1 second. At 09:00:00 uni-a uploads a file of 8,000 essays: one API request that puts 8,000 jobs on the queue. At 09:01:00 uni-b uploads 20 essays the same way. The goal: uni-b's last essay is graded by 09:01:15, and no worker sits idle while any job waits.

Which change meets the goal?

Pick one

The 30-second version

The isolation model decides what tenants share: a pool shares everything, a silo gives a tenant its own stack, a bridge mixes them per layer. Each silo multiplies connections, releases and migrations. In a pool, one tenant's load reaches the others, so queues take turns. Row security guards only the table: copies of rows need their own scope.

Pick what tenants share layer by layer, give shared workers turns, run migrations as N jobs, and scope every copy of a row.

How it works

Step 1 of 12

Pool, bridge, silo (auth-multitenancy-rls). A pool shares every layer; a silo gives one tenant a full stack; a bridge mixes them per layer.
In the kitchen
  • crates in the shared cold room, each labelled with its vendor → pooled tables with a tenant column (step 1)
  • a lockable fridge in the same room; a vendor's own kitchen next door → a bridge; a silo (step 1)

Analogy: A food hall

Independent vendors cook under one roof and share the hall's services.

In the food hall In the SaaS
crates in the shared cold room, each labelled with its vendor pooled tables with a tenant column (step 1)
a lockable fridge in the same room; a vendor's own kitchen next door a bridge; a silo (step 1)
one vendor drops 400 trays at the dish pit; everyone's plates wait a noisy neighbour (step 5)
the dish pit washes one rack from each vendor in turn per-tenant queues served in turn (step 7)
a new allergen label applied fridge by fridge; one fridge's label is refused a per-tenant migration that leaves tenants on two versions (steps 10–11)
the pickup screen calls a dish but no vendor a copy of a row with no tenant on it (step 12)

Breaks down when: a customer reads the vendor's name on the tray. A copied row carries nothing that says whose it is (step 12).

Contrasting case: a vendor in its own building: no shared dish pit, no neighbour, and every service of its own to run (step 2).

In real terms: choose isolation per layer, and pay for each silo in releases and connections; share workers by turns; expect mixed versions mid-migration; scope every copy.

Examples, run for real

4.1 ExamHall EXH-3930: one university's upload delays another's

uni-x uploaded 6,000 essays in one request at 09:00:00 and uni-y uploaded 12 at 09:02:00, on one shared queue with 3 workers.

4.1 before: one shared queue, 3 workers · uni-y's 12 essays (09:02:00) last graded 09:33:24 · uni-x's 6,000 (09:00:00) last graded 09:33:20
4.1 MR !4230: a queue per university, taken in turn · uni-y's last graded 09:02:08 · uni-x's last graded 09:33:24 · idle worker-seconds while jobs waited 0 · all work done at 09:33:24 either way

MR !4230 gives each university a queue and lets workers take from them in turn. uni-y's essays no longer wait behind uni-x's file; uni-x's finishes seconds later, and all the work ends at the same moment, since no worker idled (steps 5, 7).

4.2 A migration that leaves one workspace behind (chat)

A team-chat service gives each of its 12 workspaces a schema. Release 42 adds a unique index on each workspace's message client_id. Its runner migrates each workspace in its own transaction, records each workspace's version, carries on past a failure and reports it.

4.2 PostgreSQL 18.3 (PGlite 0.5.8) · 12 workspace schemas · release 42 adds a unique index on messages (client_id) · ws05 holds two messages sent under one client_id
4.2 runner: one transaction per workspace, carry on, record versions · failed: ws05 23505 · on version 42: 11 · on version 41: ws05

The release left one workspace on the old version. Code that runs before the fix must read both (steps 10–11).

4.3 ExamHall EXH-3931: a test as tenant B with tenant A's ids

Each university numbers its own result exports from 1. The test signs in as uni-b and requests every id uni-a has just created, on each endpoint.

4.3 before: signed in as uni-b, requesting uni-a's new ids 1 and 2 on 4 endpoints · leaking: GET /exports/:id (file path exports/<id>.csv) · check "uni-b never sees uni-a's data": FAILS
4.3 MR !4231: path exports/<university>/<id>.csv · leaking endpoints 0 of 4 · check: PASSES

Row security kept every query in scope; the export store's path had no university in it. MR !4231 adds it (step 12).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 6

A ticketing platform plans a Postgres database per event organiser for its 2,000 organisers, all on one Postgres server. Each of its 6 API instances keeps a pool of up to 5 connections for each organiser database it uses, and every instance serves every organiser.

At a peak with every pool full, how many connections reach the server?

Pick one

Question 2 of 6

A ticketing platform gives each of its 40 event organisers a schema of its own, org01 to org40, each with an orders table. A release adds:

ALTER TABLE orders ADD CONSTRAINT qty_positive CHECK (qty > 0);

The migration runner visits the schemas in name order, runs the statement in its own transaction for each schema, and stops at the first error. org17 still holds an order with qty 0 from an old import.

Afterwards, how many schemas' orders tables have qty_positive?

Pick one

Question 3 of 6

A box-office platform keeps every organiser's orders in one orders table; each organiser numbers its events from 1. Row security on orders filters by app.organiser_id, which the code sets per transaction, and the API's login owns no table. Staff watch new orders live: each organiser's relay subscribes to the topic orders.event-<id> of each event it runs and shows every message on its dashboard. A job publishes new orders:

Organiser 4 has just sold a seat at its event 7 to Ana, and organiser 9 a seat at its own event 7 to Ben. At 10:00 the job runs for organiser 4, then for organiser 9, and organiser 9's dashboard shows Ana's order.

Which change fixes it?

Code for this question
async function publishNewOrders(organiserId) {  const rows = await inTenant(organiserId, (tx) =>    tx.query('SELECT event_id, buyer FROM orders WHERE published = false'));  for (const r of rows) bus.publish(`orders.event-${r.event_id}`, r);}
Pick one

Question 4 of 6

In a design round for ExamHall you put 1,999 universities in shared tables with row security, and gave the national ministry a database of its own, as its contract requires; the web and API tiers stay shared. The interviewer says the ministry "wants its database to stay on last term's release while the universities upgrade" and asks whether you agree.

Which answer holds up?

Pick one

Question 5 of 6

A payments platform keeps all 300 merchants' payouts in one shared payouts table (merchant_id, amount). A release runs:

ALTER TABLE payouts ADD CONSTRAINT amount_positive CHECK (amount > 0);

Merchant 117 has one payout with amount 0.

What does the release leave?

Pick one

Question 6 of 6

A team-chat service runs workspace exports from one queue per workspace, and its 3 workers take jobs from the workspaces' queues in turn; each export job takes 1 second. At 09:00:00 workspace A queues 900 jobs, then workspaces B and C queue 30 each.

When does B's last job finish?

Pick one

Go deeper

Pro tips
  1. Keep one test that signs in as tenant B and requests every id tenant A has just created, on every endpoint and export. When: a new endpoint, cache, file store or queue is added (step 12). Verify:
    tip as tenant B with tenant A's ids, 4 endpoints: before MR !4231 1 leak (/exports) · after 0
    Status: RAN.
Source

quote-check.mjs finds every passage below verbatim in its saved text. The row names exactly these two sources. Valid as of 2026-10-13. The AWS pages carry the banner "This whitepaper is for historical reference only. Some content might be outdated", read on the same day.

  • AWS whitepaper, SaaS Tenant Isolation Strategies: https://docs.aws.amazon.com/whitepapers/latest/saas-tenant-isolation-strategies/ (pages saved as aws-*.txt). The row's second source. Quoted, silo: "if you have a thousand tenants, that would likely begin to impact operational efficiency and agility." Pool: "Any activity from one tenant that puts heavy load on the system, for example, has the potential to impact other tenants." and "the notion of sharing infrastructure can create situations where customers may be unwilling to run in this model." Bridge: "your solution will require a mix of the silo and pool models." Tier: "SaaS providers will often require these premium customers to run the same version of the product is deployed to the pooled environment."; "Essentially, the silo environment becomes a clone of the pooled environment that happens to be supporting one tenant."; "If too many of your tenants fall into this model, you’ll begin to fall back to a fully siloed model and inherit many of the challenges that we outlined above."
  • PostgreSQL, 5.9. Row Security Policies (17 and 18): https://www.postgresql.org/docs/17/ddl-rowsecurity.html and https://www.postgresql.org/docs/18/ddl-rowsecurity.html (pg17-rowsecurity.txt, pg18-rowsecurity.txt). The row's first source; this lesson teaches none of its rules (auth-multitenancy-rls owns them) and quotes it twice, for step 12's boundary: "all normal access to the table for selecting rows or modifying rows must be allowed by a row security policy." and "Policy expressions are run as part of the query". A policy governs queries on the table, not a copy the application keeps elsewhere, which is what P3 turns on.

Checked against the tools and docs as of 2026-10-13.