Building a Multi-Tenant SaaS from Scratch
Tenant isolation, configuration management, and the architecture decisions that let one codebase serve a hundred customers without forking.
The Requirement
The product was a B2B SaaS for the garments industry, and the requirement was to serve a hundred customers from a single codebase, each with their own data, their own configuration, and their own branding, without forking the code per customer. Multi-tenancy is a common requirement for SaaS products, but the specific shape of multi-tenancy depends on the product, and the garments industry has specific characteristics that shaped the architecture: each customer's data is sensitive (production data, financial data, employee data), the data volume per customer is moderate (not Facebook-scale, but not trivial), and the customization needs are significant (each factory has its own workflows, reports, and permission structures).
The first architectural decision was the tenancy model: shared database with tenant ID columns, shared database with tenant-specific schemas, or database-per-tenant. We chose shared database with tenant ID columns, because it's the simplest to operate (one database to back up, migrate, and monitor) and the data volume per tenant didn't justify the complexity of separate schemas or databases. The trade-off is that every query must filter by tenant ID, and a bug that forgets the filter leaks data across tenants — a catastrophic failure that's also easy to cause accidentally. The mitigation is to enforce tenant filtering at the data access layer rather than relying on every developer to remember it, which we did by making the tenant ID a required parameter on every repository method and by adding automated tests that verify no query can execute without a tenant filter.
Configuration Management
Configuration was the harder problem. Each tenant needed its own theme, its own enabled modules, its own permission sets, its own workflow customizations, and its own integrations. The temptation is to make everything configurable, but that path leads to a system where the configuration surface is larger than the code surface, and where every new tenant requires a configuration expert to set up. We took the opposite approach: a default configuration that works for most tenants, a set of well-defined customization points (theme, enabled modules, permission roles) that cover the common variations, and a clear policy that anything outside those customization points requires a code change rather than a configuration change.
That policy — "configuration for the common cases, code for the rest" — is the one I'd recommend to anyone building a multi-tenant SaaS. The alternative — making everything configurable — feels like it gives you flexibility, but it actually gives you complexity, because every configuration combination is a state the system can be in, and testing all combinations is impossible. By keeping the configuration surface small and handling the rest in code, we kept the system testable (the number of configurations is bounded) and the onboarding fast (a new tenant is set up in minutes, not days). The tenants who needed custom behavior that wasn't covered by configuration got it through feature flags and code changes, which were reviewed, tested, and deployed like any other code change.
Tenant Isolation
Tenant isolation isn't just about data — it's about performance, too. A single tenant running a heavy report shouldn't degrade performance for other tenants, and a spike in traffic from one tenant shouldn't affect others. We achieved performance isolation through a combination of rate limiting (per-tenant request limits), query timeouts (no query can run longer than a configurable threshold), and background job queues (per-tenant queues so one tenant's batch jobs don't block others). The rate limiting was the most important, because it's the mechanism that prevents a single tenant from consuming disproportionate resources, and it's the mechanism that lets you give each tenant a predictable experience regardless of what other tenants are doing.
The lesson from this project is that multi-tenancy is as much an organizational discipline as a technical architecture. The technical patterns — tenant ID filtering, configuration management, rate limiting — are well-understood and straightforward to implement. The organizational discipline — saying no to "just this once" customizations, enforcing the configuration surface, treating the shared codebase as a product rather than a collection of tenant-specific branches — is harder, because every tenant has a request that feels reasonable and every "no" feels like bad customer service. The discipline to say "no" is what keeps the system maintainable as the tenant count grows, and it's the discipline that most multi-tenant SaaS projects lack, which is why most of them eventually become unmaintainable.
