If you are a CTO or technical founder evaluating how to architect a multi-tenant SaaS platform, this guide is written for you. We have designed and built multi-tenant systems across multiple B2B SaaS products — using different approaches for different contexts. This is not a theoretical overview. It is a practical breakdown of what each approach actually looks like in production, where it works, and where it breaks down.
Multi-tenancy means a single deployment of your software serves multiple customers — called tenants — while keeping their data and experience completely isolated from each other. It is the architectural foundation of almost every B2B SaaS product.
Getting this decision wrong early is expensive. Changing your tenancy model after you have live customers is one of the most painful refactors a SaaS team can face. The choice you make at the start shapes your database design, your deployment model, your security posture, your onboarding flow, and your ability to scale. There is no universally correct answer — the right approach depends on your product, your customers, your compliance requirements, and your growth trajectory.
Each tenant gets their own database schema within a shared PostgreSQL instance. Tables are identical across schemas — the structure is the same, but data is completely separated at the database level. Strong isolation, moderate operational overhead.
All tenants share the same tables. Every table has a tenant_id column. The application enforces isolation by filtering on tenant ID in every query. Simplest to operate at scale — lowest overhead per tenant.
Each tenant gets their own branded subdomain — acme.yourplatform.com — with their logo, color scheme, and configuration. Primarily a frontend and identity concern, typically combined with one of the data isolation approaches above.
Each tenant is provisioned their own schema within a shared PostgreSQL database. When a request comes in, the application identifies the tenant from the request context — typically from a JWT claim, a subdomain, or a header — and sets the PostgreSQL search_path to that tenant's schema before executing any query.
Database: saas_platform ├── Schema: tenant_acme │ ├── users │ ├── orders │ └── settings ├── Schema: tenant_globex │ ├── users │ ├── orders │ └── settings └── Schema: public (shared config, tenant registry)
In a Spring Boot application, this is handled through a custom TenantContext that holds the current tenant identifier in a thread-local variable, combined with a Hibernate CurrentTenantIdentifierResolver and a MultiTenantConnectionProvider that switches the schema on each connection. A filter or interceptor resolves the tenant from the incoming request and sets it in the context before any service layer code runs.
public class TenantContext { private static final ThreadLocal<String> currentTenant = new ThreadLocal<>(); public static void setTenant(String tenantId) { currentTenant.set(tenantId); } public static String getTenant() { return currentTenant.get(); } }
Works well for: Platforms with a moderate number of high-value enterprise tenants, strong data isolation requirements, or compliance obligations (healthcare, government, financial services). If your customers will ask "where is our data stored and is it separate from other customers," schema-per-tenant gives you a clean, defensible answer.
Breaks down when: You scale to hundreds or thousands of tenants — schema sprawl, connection pooling complexity, and migration orchestration become significant operational burdens.
All tenants share the same tables. Every table has a tenant_id column that identifies which tenant each row belongs to. The application enforces isolation by including WHERE tenant_id = ? in every query. In Spring Boot with Spring Data JPA, this is typically implemented using a @Filter annotation on entities, activated via TenantContext on each session — or more robustly, through a custom repository base class that enforces tenant scoping on all queries.
Table: users | id | tenant_id | name | email | |----|-----------|------------|--------------------| | 1 | acme | John Smith | john@acme.com | | 2 | globex | Jane Doe | jane@globex.com | Table: orders | id | tenant_id | amount | status | |----|-----------|--------|----------| | 1 | acme | 5000 | complete | | 2 | globex | 8000 | pending |
Works well for: High-volume B2B SaaS with many small-to-medium tenants where operational simplicity and onboarding speed matter. Consumer-facing SaaS, SME-focused tools, and platforms where tenants are unlikely to have enterprise data compliance requirements. Adding a new tenant is just inserting a record — no schema provisioning required.
Critical risk: Row-Level Security (RLS) in PostgreSQL is an increasingly popular complement — it enforces tenant isolation at the database level as a safety net even if application-level filtering has a bug. Application-level filtering alone is necessary but not sufficient. A single missing WHERE clause can expose all tenant data. All indexes on tenanted tables must include tenant_id as the leading column or query performance will degrade dramatically at scale.
Each tenant gets their own branded subdomain — acme.yourplatform.com, globex.yourplatform.com — with a customized experience: their logo, color scheme, domain, and potentially their own configuration. The application resolves the tenant from the subdomain on each request. This is often combined with one of the two data isolation approaches above — subdomain provisioning is primarily a frontend and identity concern, not a data storage decision.
Incoming request: https://acme.yourplatform.com/dashboard
│
▼
Middleware extracts subdomain: "acme"
│
▼
Tenant config loaded: { logo, theme, features, tenantId }
│
▼
Request proceeds with tenant context set
The key infrastructure components are: a wildcard DNS record (*.yourplatform.com) routing all subdomains to your load balancer, a wildcard TLS certificate covering all subdomains, a tenant resolver middleware extracting the subdomain from the Host header, and a database table or cache mapping subdomains to tenant branding, settings, and feature flags.
Works well for: B2B SaaS where white-labeling is a core product requirement — agency tools, reseller platforms, clinic or franchise management systems. If your sales pitch includes "your customers won't know you're using our platform," this approach is essential. Plan carefully for custom domains — if tenants want their own domain, certificate automation becomes a significant engineering concern.
| Factor | Schema-Per-Tenant | Shared Schema | Subdomain Provisioning |
|---|---|---|---|
| Data isolation | Strong | Moderate | Depends on data layer |
| Operational complexity | Medium | Low | Medium–High |
| Onboarding speed | Slower | Fast | Medium |
| Scale to many tenants | Moderate | High | High |
| Enterprise compliance | Strong | Weaker | Neutral |
| Custom branding | Not applicable | Not applicable | Strong |
| Migration complexity | Per-schema | Single migration | Single migration |
| Cross-tenant analytics | Complex | Simple | Depends on data layer |
| Implementation effort | High | Medium | Medium–High |
If you plan to have thousands of tenants paying small amounts, the operational overhead of schema-per-tenant will cost you more than you make per tenant. Shared schema is the right call.
Application-level tenant filtering is necessary but not sufficient. A single missing WHERE clause can expose all tenant data. RLS at the database level provides a critical safety net that is not optional.
Wildcard subdomains are straightforward. Custom domains — where tenants bring their own domain — require certificate automation, CNAME verification, and DNS propagation handling. Plan for this explicitly if it is on your roadmap.
In shared schema implementations, every query filters by tenant_id. If your indexes do not lead with tenant_id, query performance will degrade dramatically as your data volume grows — often only discovered in production.
This is the most expensive mistake. Retrofitting multi-tenancy into a single-tenant application is one of the most disruptive refactors a SaaS team can undertake. Make the decision before you write your first entity class.