~learn.

Multi-tenancy and Security, on one page.

Prints on a single A4 or Letter page. Download PDF opens your browser's print dialog: choose “Save as PDF”.

System Design Mastery · World 09 cheat sheet

Multi-tenancy and Security

~/himanshufree course · himanshukukreja.in/learn

Day 1Tenant Isolation Strategies

The isolation spectrum. Shared tables with a tenant_id column, then a schema per tenant, a database per tenant, dedicated infrastructure and separate cloud accounts. Each step adds isolation and cost; no tenant identifier at all is never acceptable.

Tenant from the token. The verified JWT claim is authoritative; a subdomain or X-Tenant-ID header is only a hint and a mismatch is rejected. The tenant lives in a ContextVar, so concurrent requests can't see each other's tenant.

Fail loudly without a tenant. get_current_tenant() raises TenantContextError when no context is set. Any fallback, a default tenant or the previous request's, is exactly how a query ends up without its tenant filter.

RLS as defense in depth. Application repositories add tenant_id to every query; PostgreSQL RLS filters again in the database. FORCE makes policies apply to the table owner too, and app.current_tenant is cleared before a pooled connection is released.

Context must travel. Events carry tenant_id (also the partition key) and jobs take it as an explicit argument, because a ContextVar doesn't reach another process. Cache keys, file paths, search indexes, logs and metrics all include the tenant.

Millions · thousands · hundredsof tenants: shared tables, schema, database per tenant
500 × 10= 5,000 connections for 500 tenant databases with 10-connection pools
10 s · 100 · 50,000Salesforce limits per transaction: CPU, SOQL queries, records
5 minutestenant context cache TTL
Trade-off · Where tenants live
Shared tablesSchema per tenantDatabase per tenant
IsolationApplicationDatabasePhysical
BackupAll or nothingPer schemaPer tenant
Data residencyDifficultPossibleEasy
Enterprise readyRarelySometimesYes
  • A query without WHERE tenant_id = ?, returning every tenant's rows.
  • Trusting a client-supplied X-Tenant-ID, so an attacker picks the victim's tenant.
  • Cache keys like user:{user_id} without a tenant prefix: IDs repeat across tenants.

Day 2Noisy Neighbor Prevention

One tenant breaks everyone. One "export all data" report scanned 2.3 billion rows for 847 seconds: API latency went from 200ms to 15,000ms and 500 other tenants couldn't use the product. Legitimate use, no limits.

Rate limits aren't enough. Rate limiting stops request volume, not expensive operations: five requests a minute can still scan billions of rows. Add concurrency, compute and volume quotas, and enforce at the gateway, application, database and infrastructure.

Limits shared across servers. A token bucket refills up to burst_size: short bursts pass, the long-run rate holds. Per-server limiters give a tenant N times its limit, so one atomic Lua script updates a Redis sliding-window log.

Guard the database. Concurrency slots expire after their TTL, so a crashed worker can't leak one forever. EXPLAIN-based analysis rejects queries over 10 million estimated rows unless the plan is Enterprise; per-tenant pools set statement timeouts.

Schedule fairly. Round-robin across per-tenant queues: a tenant with 100 queued jobs can't make another wait for all of them, and each queue is capped. Weighted fair queuing (free 1, pro 5, enterprise 20) gives paying tiers a larger share.

60 · 1,000 · 10,000API calls a minute: free, pro, enterprise
5 · 50 · 200concurrent requests by plan
30 · 120 · 300 squery timeout by plan
80%of a quota: warn before the hard limit
Trade-off · Limit layers
BlocksExample
API gatewayRequest volumePer-tenant rate limit
ApplicationExpensive operationsConcurrency, query analysis
DatabaseRunaway statementsTimeouts, per-tenant pools
InfrastructureResource exhaustionCPU and memory quotas
  • One global limiter: a tenant at 900 req/sec leaves 100 for everyone else.
  • Throttling silently by sleeping, which hides the limit and holds connections; return 429 with Retry-After.
  • One limit for every operation: 1,000 exports melt the database.

Day 3Data Residency and GDPR

Four words to keep apart. Residency is where data is stored; sovereignty is which country's laws apply; localization is a legal requirement to keep data in-country; a transfer is moving data across borders, which needs a legal basis.

What counts as personal data. Names, emails, IP addresses, cookie and device IDs, location and behaviour. Pseudonymized data is still personal data, and backups and caches follow the same rules as the primary data.

Regional data, global control plane. The common SaaS pattern: each tenant is assigned a region at onboarding, and its database, storage and search live there. The global control plane holds only metadata: tenant registry, configuration, routing rules.

Consent done right. Freely given, specific per purpose, informed, unambiguous, and as easy to withdraw as to give. Keep an append-only log (exact text, version, IP, time) plus a current-status table; never asked counts as not granted.

Transfers need a basis. Adequacy decision first, then Standard Contractual Clauses with supplementary measures, or BCRs for corporate groups. Post-Schrems II, document a Transfer Impact Assessment and review it, because adequacy can be revoked.

€20M or 4%of global turnover, whichever is higher: top GDPR fine
2020 · 2023Schrems II ends Privacy Shield; EU-US Data Privacy Framework
1 hourtenant region cache TTL: invalidate on migration
365 daysuntil a transfer assessment is reviewed
Trade-off · Residency patterns
Single regionRegional deploymentsRegional data, global control
ComplianceCan't meet residencyStrongStrong
Cross-region featuresAllNoneSome
FitsNon-regulated dataStrict localizationMost SaaS
  • Keeping EU data in Frankfurt but sending backups to a US bucket.
  • Logging emails or user IDs to a global logging service.
  • Using US vendors without a DPA and a transfer mechanism, or assuming adequacy is permanent.

Day 4Right to Deletion

What Article 17 asks. Respond within one month (two more for complex cases), delete every copy including backups eventually, and have processors delete too. You may refuse for legal obligations, public interest or legal claims, and must explain why.

Deleting is not marking. Soft delete leaves the data in storage and backups, so it's only a step before hard delete. Pseudonymized data is still personal; truly anonymous aggregates ("users made 10,000 orders") may be kept.

Inventory, then orchestrate. Map every place personal data lives, then run one executor per system in dependency order: search, files, analytics, third parties, the primary database (it holds the user_id mapping and foreign keys), and caches last.

Stop, retry, verify. Targets at the same priority run in parallel; after a failed group the run stops and a retry resumes at that point. The request completes only after every system is re-checked and the data is confirmed gone.

Keep what the law requires. Orders are financial records, so they're anonymized (user_id NULL, addresses 'DELETED'), not deleted. Shared content shows "Deleted User". The deletion audit trail is kept, with the user ID redacted, as proof.

Backups expire, restores re-delete. You can't remove one user from a backup, so backups expire under a retention policy that's tracked. If a backup is restored first, the pending deletions are re-applied before the data goes live.

1 month (+2)to respond to an erasure request
30 daysdeletion request deadline in the service
k > 10typical k-anonymity for aggregates you may keep
7 yearstax retention for anonymized transaction records
Trade-off · Ways to "delete"
Hard deleteAnonymizePseudonymize
Data afterwardsGoneCan't identify anyoneStill personal data
Satisfies erasureYesYes, if truly anonymousNo
Use forMost personal dataFinancial records, statsProtecting active data
  • Deleting the users row and forgetting search, analytics, caches and third parties.
  • Reporting "deleted" without a verification pass over every system.
  • Deleting audit logs with the user, leaving no proof of what happened.

Day 5Security Architecture

Defense in depth. Five layers, each assuming the others might fail: perimeter (WAF, DDoS protection, rate limits), network (VPC, private subnets), application (authn, authz, validation), data (encryption, keys), monitoring (audit logs, anomaly detection).

Zero trust. Never trust, always verify: network location grants nothing, every connection is authenticated and every request authorized. Least privilege, assume breach, micro-segmentation. Google built BeyondCorp after Operation Aurora.

Secrets never in code. Not in code, config files, container images or environment variables. Fetch from Vault or Secrets Manager at connection time so rotation needs no restart. A leaked secret is already copied everywhere: rotate it.

Encrypt at every layer. TLS 1.2+ externally and mTLS between services; TDE and SSE-KMS at rest; field-level encryption for SSNs and cards, because TDE decrypts for any query. A key hierarchy (HSM master, tenant keys, data keys) limits blast radius.

Revocable tokens. 15-minute access JWTs and 7-day refresh tokens, plus a server-side session checked on every request, so deleting the session revokes access at once. Five failed logins mean a 15-minute lockout; errors stay generic.

15 minutes · 7 daysaccess and refresh token lifetimes
5 attempts · 15 minutesfailed logins before lockout, and its length
12+bcrypt cost factor
5 minutessecrets cache TTL
Trade-off · STRIDE
ThreatMitigation
SpoofingPretending to be someoneMFA, certificates
TamperingModifying data or codeValidation, signing
RepudiationDenying an actionAudit logs
Disclosure, DoS, privilegeLeaks, outages, escalationEncryption, rate limits, least privilege
  • Trusting frontend validation: amount=-500 sent straight to the API adds money.
  • Fetching documents by ID alone, without AND tenant_id = ? from the authenticated user.
  • allow_origins=["*"] with credentials, or logging passwords, card numbers and auth headers.

BossDesign a Multi-Tenant Enterprise SaaS Platform

Size it first. ~100M documents at 500KB is 50TB; with metadata 60TB, 180TB with 3x replication. Extracted text of 10KB each is a 1TB index, ~3TB in Elasticsearch. 1,000 requests a second at peak, 60% of them searches.

Regional data planes, global control plane. US, EU and APAC each run a full stack: API, PostgreSQL, Elasticsearch, S3, Redis, Kafka. The global control plane holds only the tenant registry, flags, quotas and routing. No tenant data crosses regions.

Five isolation layers. Tenant from the verified JWT (the subdomain must match); repositories that add tenant_id; RLS in PostgreSQL; a search index per enterprise tenant or a filtered shared index; S3 paths and IAM by tenant, with per-tenant keys.

Quotas and fair queues. Per-plan quotas on API calls, searches, uploads and query timeouts. Each tenant gets its own capped job queue; dividing the plan weight by (1 + depth * 0.1) shrinks a deep queue's share.

Uploads go straight to S3. The upload service issues a presigned URL for the tenant's regional bucket, so bytes skip the API servers; text extraction and thumbnails run asynchronously from a queue. The server picks the bucket, never the client.

500ms · 200msp99 targets for search and API calls
< 4 hours · < 1 hourRTO and RPO
7 dayslifetime of a GDPR export download link
30 daysoffboarding grace period before full deletion
Trade-off · Standard vs enterprise tenants
StandardEnterprise
DatabaseShared tables with RLSDedicated database
SearchShared index, tenant_id filterIndex per tenant
Isolation levelSHARED (RLS)DATABASE, own KMS key
Index corruption hitsEvery tenant on the indexOne tenant
  • Reading tenant_id from the request body so admins can "switch tenants".
  • Putting user emails in the global control plane for faster login routing.
  • Deleting the primary database first, leaving search, file and analytics copies you can no longer find.