Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
You start off with what seems like the obvious solution to a multi-tenant SaaS application.
We have tenant A. We have tenant B. We have one application and one database. Within that database, for every structure, whether that is a table, collection, or stream, we segregate things by a tenant ID.
It is simple. It is easy. It works.
Until it does not.
What happens when one tenant imports five million records? Or they run a bunch of reports, and some of those reports are massive? That shared infrastructure which seemed simple is also what is coupling everything together.
YouTube
Check out my YouTube channel, where I post all kinds of content on Software Architecture & Design, including this video showing everything in this post.
Shared Infrastructure Means Shared Problems
Let us use another basic example. We have multiple instances of our application in a scaled out environment, but now we need to make a schema change.
The first thing that happens is we make our schema change to the database. Then we do a rolling deployment where we update one of the application instances first. That instance is now updated. Its code matches what the schema is. That is what it is expecting.
Everything is fine.
Uh oh.
What about the second instance that has not been updated yet? It is still making requests at runtime while everything is being deployed. Of course, it is going to fail. It has zero expectation of what that schema change is. It does not even know about it.
If your schema changes are not backwards compatible, your old code and your new code are looking at the database in a different way. They are expecting a certain shape, and the database is not giving both of them what they expect.
Can you go down the road of blue green deployments? Yes.
Could you have canary deployments? Sure.
But you are still under the same circumstance. You need to understand the coupling involved and the trade offs being made.
The First Question Should Not Be Database Per Tenant
Almost always, when people talk about multi-tenancy, they immediately ask:
Should we have a shared database?
Should we have a database per tenant?
Should we have a schema per tenant?
Those are valid questions, but they should not be the first question.
A better question is this:
What are you actually trying to isolate?
Are you trying to isolate data? Compute? Deployments? Schema changes? Performance? Different types of failures?
Because multi-tenancy is not just about databases. It is about creating isolation through boundaries. A database is just one of those boundaries.
Every Shared Resource Creates Coupling
Every time tenants share something, they are coupling through it.
In the database example, yes, you are sharing the schema. You have to deal with shared migrations. You are also sharing the performance of that database.
With your API, tenants are sharing the compute that is actually running it. They are sharing deployments. They are sharing failures happening within that instance or those instances.
The same thing applies if you are using queues and background processing. The same thing applies to cache, what is in memory, invalidation, and the keys used in that cache. It applies to regions, where things are deployed, residency, and latency.
If you are sharing something between tenants, they are ultimately coupled through it.
A Shared Database Can Be Perfectly Fine
Back to the simple example: one application, one shared database, and tenant ID on the rows.
There is nothing inherently wrong with this.
If we have a single instance and a single shared database with segregation by tenant ID, it is simple. It works. When we add a new tenant, it is easy. We do not have to set up a new database. We do not have to roll out some new infrastructure.
We are sharing the schema, and that is a lot easier to manage. It is simpler to deal with. It is probably more cost effective.
Within our code, every time we deal with queries, we have some type of filtering. Depending on the libraries and frameworks you are using, that might be handled by query filters or something similar.
Again, there is nothing inherently wrong with this.
As long as you understand the trade offs.
The Tenant ID Has To Leak Everywhere
The trade off is that this information has to leak everywhere through your system.
It is not a bad thing. It is just the trade off.
If you are doing database access at the row level, the tenant ID has to be in every possible query, or you have to force it through some type of query filter.
Every report that you have anywhere in your system has to filter by tenant. It might not even be application code. It might be somewhere else entirely, but it still has to filter.
If you are using queues and messaging, tenant ID has to be part of every message because you are not segregating everything physically.
Your tenant ID has to be part of everything.
Again, not bad. Just the trade off.
Shared Schema Means Shared Migrations
If you are sharing the database, that means you are sharing the schema. You are sharing migrations.
That means if you upgrade your database, make some type of schema change, and are doing a rolling deployment, it might work for one instance, but it might not work for another if everything is not backwards compatible.
So you need to think about that.

The trade off is that you have to make schema changes backwards compatible. That means having a process. You expand the schema, make the change in a compatible way, deploy the new code, then backfill the data.
If you add columns that are nullable, and the new application code is going to start sending that data, you still have to assume that existing data needs to be backfilled.
That becomes part of your deployment pipeline.
Is it more complexity? Yes.
Is it more work? Yes.
Is it a trade off? Yes.
That is the whole point. You get the simplicity of maintaining one physical database, but the complexity moves into dealing with shared schema changes and rolling deployments.
Deployment Isolation Is Not Schema Isolation
There is also isolation at the application level.
You could have tenant A using version one of your application and tenant B using version two. That gives you isolation at the deployment level, almost like a canary deployment.
This can be totally fine.
Maybe you have a change coming in that you are unsure about. You give it to tenant B because they are a little more relaxed about it. Once you know it is solid and there are no issues, you roll it out further to tenant A or to more tenants.
But if they are still using the same underlying schema, you still have to make everything backwards compatible.
You are providing deployment isolation. You are not necessarily providing schema isolation.
Multi Tenant Architecture Is A Spectrum
When you hear somebody talk about multi tenant architecture, it is really about isolation.
But what kind of isolation?
It is not just the database.
At the database level, you have data isolation. At the API level, you have compute and deployment isolation. If you are using queues and messaging, you have background processing isolation. You might have different queues by priority or by tenant. The same thing applies to caching.
If you are using cloud regions, now you are also talking about residency and latency depending on where things are deployed relative to your tenants.
It is not just about data isolation.
All of these choices are on a spectrum. Depending on your needs, you can be on one side, in the middle, or on the other.

For data isolation, you could have shared tables with a tenant ID. You could have a shared database, but separate schemas. You could have different pools where some tenants are on one database instance and some tenants are on another. Or you could go as far as having a database per tenant.

The same goes for compute. You could have shared instances where everybody is using everything. You could have pooled instances where a subset of tenants uses a subset of compute. Or you could have completely dedicated compute.
You do not necessarily have to fit on one side or the other. It is often a mix and match depending on your needs.
Efficiency Versus Control
The trade off is efficiency versus control.
On one side, you have efficiency. A shared database is simple to maintain. You have one instance. You have one place for backups, migrations, upgrades, and operational concerns.
On the other side, you have control. A database per tenant gives you more control, but that control is not free. There is cost and complexity in maintaining multiple databases, different pools, dedicated compute, or dedicated infrastructure.
The same thing applies to compute.
Shared compute can be very efficient and cost effective. But if you have pooled or dedicated compute for particular tenants, now you are dealing with every particular instance for every tenant or group of tenants.
There is just more complexity involved.
Noisy Neighbors
Here is one reason you might not want to share compute.
Let us say we have tenant A and tenant B.
Tenant A is making requests. Everything is working fine. Happy path. Everything is good.
Tenant B starts making a massive number of requests. Maybe they are importing data. Maybe they are running requests against the database that are really heavy. Maybe what they are doing is CPU or memory intensive.
Now tenant B is affecting the database. They might also be affecting the API.
That is the noisy neighbor problem.
Tenant B is flooding requests to the app or to the database, and tenant A is affected by what tenant B is doing.
Is that a reason to isolate or pool compute differently? It could be.
But there are also alternatives.
Rate Limiting Is One Tool
One alternative is rate limiting.
That means every tenant has their own scope. It does not necessarily mean we have to separate compute and have some compute sitting idle, not doing much.
Both tenants could still use the exact same underlying instance of the app or API, but we rate limit by tenant ID as part of the request.
That can help solve the noisy neighbor problem, but it does not necessarily solve it entirely. Depending on the request being made, you might save the API, but that does not mean you are saving the database.
Rate limiting is just one tool.
You could do the same kind of thing with database connection pools and group those by tenant. Again, they are just tools.
What you really need to identify is what resources are being exhausted.
Tenants Are Not All The Same
The type of system you are building and the customer base you have matters.
At the beginning, tenants might seem like they are all the same, but they probably are not.
Some tenants might have a small number of users. Larger tenants might have a much larger set of users. Some tenants might run heavier workloads. Some might import more data. Some might run more reports.
Over time, you might realize that you need to pool small tenants together, pool large tenants together, or move very large tenants into dedicated infrastructure.
Again, it goes back to mix and match.
At first, you might think all tenants are the same, but they are not.
When Should You Isolate?
This leads to the actual decision you need to make: what should you isolate, and when?
You isolate when you need a smaller blast radius. You isolate when you do not want noisy neighbors at a given level, whether that is compute, database, background processing, or something else.
You isolate when you have tenant specific requirements. That could be compliance. You might have a requirement that a tenant needs to be isolated at the database level for compliance reasons.
You isolate when a tenant needs dedicated capacity. Maybe they do not want to share resources because they care about availability or performance guarantees.
You isolate when you want safer rollouts. Maybe you want to provide one version to a subset of tenants, and then roll it out later to more sensitive tenants or a larger pool.
It is isolation at various levels because you want to avoid noisy neighbors, reduce the blast radius, and have protection against specific types of failures.
Multi-Tenancy Is About Boundaries
Multi-tenant architecture is not a database pattern.
It is about isolation.
The real question is: what should be isolated?
Sharing gives you efficiency. It is usually simpler and more cost effective.
Isolation gives you more control. But that control comes with more cost and more complexity.
So if you are building or living in a multi-tenant system, do not start with “shared database or database per tenant?”
Start with what you are actually trying to isolate.
Then decide where sharing makes sense, where isolation makes sense, and what trade offs you are willing to take.
Join CodeOpinon!
Developer-level members of my Patreon or YouTube channel get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.





