Skip to Content
Course content

210: Database-per-Tenant vs Shared Database Approaches

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was consulting for a developer who had built a promising SaaS for independent medical clinics. He'd gone with the simplest path: one giant database where every single table—patients, appointments, billing—had a clinic_id column. It worked beautifully until the day he accidentally pushed a bug to production that omitted a WHERE clause on a critical reporting query. For about forty minutes, Clinic A was able to see the patient lists and billing totals for Clinic B. In the medical world, that's not just a bug; it's a catastrophic HIPAA violation. He spent the next three days in a panic, realizing that while his architecture was easy to build, a single typo could leak every piece of data in the entire system.

The Simplicity and Risk of the Tenant ID

When you're starting out, the shared database approach (often called multi-tenancy via discriminator columns) is the path of least resistance. You have one database, one set of migrations, and one connection string in your .env file. To keep data separate, you simply tag every row with a tenant_id. When a user logs in, you store their tenant ID in the session and append WHERE tenant_id = ? to every single query you write.

From a developer's perspective, this is a dream for maintenance. If you need to add a "middle name" column to your users table, you run one migration, and every single client on your platform is updated instantly. However, as my friend discovered, the "blast radius" is huge. If you forget that WHERE clause—or if a junior developer joins the team and doesn't realize the convention—you've got a data leak. You're also dealing with "noisy neighbor" syndrome: if one massive client runs a resource-heavy report that locks a table, every other client on your platform feels the lag.

Hard Isolation and the Migration Nightmare

The alternative is the database-per-tenant approach. Here, every time a new client signs up, your PHP code triggers a script that creates a brand new database (e.g., tenant_123_db) and runs the full schema setup. Your application determines which database to connect to based on the request—usually by looking at the subdomain (like client1.myapp.com) or a custom header.

// A simplified conceptual look at switching connections
$tenantId = $request->getSubdomain();
$dbConfig = $mainDb->query("SELECT db_name, db_user, db_pass FROM tenants WHERE id = ?", [$tenantId]);

$pdo = new PDO("mysql:host=localhost;dbname=" . $dbConfig['db_name'], $dbConfig['db_user'], $dbConfig['db_pass']);

This gives you absolute peace of mind regarding data isolation. It's physically impossible for Clinic A to see Clinic B's data because they aren't even connected to the same database. It also makes backups and restores trivial; if Client C accidentally deletes their data, you can restore just their specific database without rolling back the entire platform. But be warned: the maintenance overhead is grueling. Imagine having 500 clients and needing to add a single column to a table. You can't just run one migration; you have to loop through 500 databases and run the migration 500 times. If the process crashes halfway through, you now have a "split-brain" schema where half your clients are on version 1.2 and the other half are on 1.3.

Choosing Your Poison Based on the Client

So, which one do you choose? I usually tell people that the decision depends more on your legal requirements and your price point than on the technology itself. If you are building a low-cost tool for freelancers, a shared database is almost always the right choice. The overhead of managing hundreds of databases will kill your productivity, and the risk is relatively low.

However, if you are selling to enterprises, banks, or healthcare providers, they will often demand isolated data. They might even insist that the database be hosted on their own infrastructure. In those cases, the "migration nightmare" is simply the cost of doing business. I've found that if you go the isolated route, investing heavily in an automated migration runner—something that can handle failures gracefully and log exactly which tenants were updated—is not optional; it's a requirement for your sanity.




📋 Practical Task

Exercise: Building a Tenant-Aware Database Connection Switcher

In this exercise, you will implement a TenantManager class that handles the logic of switching database connections based on a provided tenant identifier. This simulates the "Database-per-Tenant" approach.

Requirements:

  • Create a TenantManager class that accepts a PDO connection to a "Master" database in its constructor.
  • Implement a method getConnectionForTenant(string $tenantIdentifier): PDO.
  • This method must:
    1. Query the Master database to find the database name, username, and password associated with that $tenantIdentifier.
    2. If the tenant doesn't exist, throw a custom TenantNotFoundException.
    3. Return a new PDO instance connected to that specific tenant's database.
  • Write a small test script that attempts to connect to a "Client_A" and "Client_B" and executes a simple SELECT 1 query to verify the connection is active.

Constraints: Do not hardcode the credentials in the TenantManager; they must be retrieved dynamically from the Master database.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.