Most multi-tenant systems don't fail because someone wrote bad code. They fail because a decision that was reasonable at ten customers quietly stops being reasonable at a hundred, and nobody revisits it until an incident forces the question.
Isolation belongs in the database too
The most common one we see is tenant isolation handled entirely at the application layer, with no enforcement at the database level. It works fine until a single missing WHERE clause in a new feature branch leaks one customer's data into another's dashboard.
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id')::uuid);Row-level security policies in Postgres cost you an afternoon to set up properly. Treated as a backstop rather than the only line of defense, they turn a catastrophic bug into a non-event.
A decision that was reasonable at ten customers quietly stops being reasonable at a hundred.
Size jobs for your biggest tenant
The second mistake is sizing background jobs for the average tenant instead of the largest one. A nightly reconciliation job that takes four minutes for a typical account can take four hours for your biggest customer and now it's colliding with the next night's run.
Design for tenants you don't have yet
None of this requires a rewrite. It requires treating the tenants you don't have yet as a design constraint, not a future problem.
- Architecture
- SaaS
- PostgreSQL
Written by Mustafa Halvadwala
Engineering Lead at HASANI.


