Every SaaS I have reviewed eventually argues about “pool vs silo.” That argument is downstream of a simpler one: what is the unit of failure? If tenant A can run an unbounded export and lock a shared Aurora writer, you do not have a pool. You have a single-tenant outage with extra invoices.
Identity first
Cognito user pools map well to B2C and to B2B where the customer does not bring their own IdP. For B2B that does, Cognito federates SAML/OIDC and the application still issues its own access token with `tenant_id` and `roles` as claims. API Gateway JWT authorizers should validate that token; they should not be the only place tenant context is trusted. Every downstream query takes tenant id from the verified claims, never from the request body.
Authorization: Bearer <access_token>
Claims:
sub, tenant_id, roles[], token_use=access
Repository:
WHERE id = :id AND tenant_id = :claims.tenant_id
Never:
WHERE id = :id -- then “check tenant in the service layer”
tenant_id from JSON body
a shared IAM role that can ListObjects on s3://app-uploads/*Data isolation that matches the risk
- Pool with a tenant_id on every row (DynamoDB pk = TENANT#id, RDS RLS or always-present predicate). Default for early SaaS. Cheap, and dangerous if the predicate is optional.
- Schema-per-tenant on RDS when a customer requires logical separation and you can afford migrations that multiply. I avoid this until a contract demands it.
- Account-per-tenant (silo) for regulated workloads or when one customer’s load must not share noisy-neighbor fate. Control tower and pipelines become the product.
Noisy neighbors are a quota problem
Put per-tenant rate limits at WAF or API Gateway usage plans, and enforce a second limit in the worker: max concurrent jobs, max export rows, max websocket connections. S3 prefixes are `s3://bucket/t/{tenantId}/…` with a bucket policy that a session can only assume a role scoped to that prefix. DynamoDB on-demand absorbs bursts; provisioned capacity with a shared table does not, unless you over-provision for the largest tenant and make everyone pay.
Start pooled, with ruthless tenant predicates and quotas. Silo the few tenants whose contract or load requires it. Do not silo the architecture diagram and pool the database — that is how IDOR bugs ship with a nice Cognito login screen.