Skip to Content
Course content

213: Tenant-Specific Configuration Management

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

When you first move from building a single-client app to a multi-tenant SaaS, you'll hit a wall regarding configuration. In a standard app, you have a .env file or a config.php, and that's it. But when you have five hundred different companies using your software, and each one needs their own Stripe API key, a unique brand color for their dashboard, and a specific "feature flag" to enable a beta module, you can't just keep adding lines to a static file.

The Trap of the Giant Configuration Array

The first instinct for many developers is to create a massive associative array or a series of files named after the tenant. I've seen people do this: they create a tenants.php file where the keys are the tenant IDs and the values are arrays of their specific settings. It looks simple at first.

// The naive way
return [
    'acme_corp' => [
        'brand_color' => '#ff0000',
        'stripe_key' => 'sk_test_123',
        'max_users' => 50,
    ],
    'globex' => [
        'brand_color' => '#0000ff',
        'stripe_key' => 'sk_test_456',
        'max_users' => 100,
    ],
];

Here is why this is a disaster waiting to happen: it couples your infrastructure to your code. Every time you sign a new customer, you have to commit a code change and redeploy the entire application just to add a few configuration strings. If a customer wants to change their brand color via a settings page in your UI, you can't—because the configuration is hardcoded in a PHP file. You've essentially turned your version control system into a database, which is a recipe for merge conflicts and deployment anxiety.

Moving Logic into the Data Layer

The professional way to handle this is to treat tenant configuration as data, not code. You want a configuration manager—a service class—that resolves a setting based on the current tenant's context. I usually implement this as a "layered" lookup. You check for a tenant-specific override first; if that doesn't exist, you fall back to a global system default.

In this model, your settings live in a database table (usually a simple key-value pair table linked to a tenant_id). When you need a value, you ask your service for it. I prefer this because it allows you to build an admin panel where tenants can manage their own settings in real-time without you ever touching a terminal.

class TenantConfig {
    private $tenantId;
    private $db;
    private $cache = [];

    public function __construct(int $tenantId, PDO $db) {
        $this->tenantId = $tenantId;
        $this->db = $db;
    }

    public function get(string $key, $default = null) {
        // Simple local cache to avoid hammering the DB in one request
        if (isset($this->cache[$key])) {
            return $this->cache[$key];
        }

        $stmt = $this->db->prepare("SELECT value FROM tenant_settings WHERE tenant_id = ? AND setting_key = ?");
        $stmt->execute([$this->tenantId, $key]);
        $value = $stmt->fetchColumn();

        $this->cache[$key] = $value !== false ? $value : $default;
        return $this->cache[$key];
    }
}

The Performance Trade-off and the Cache Layer

Now, you might be thinking: "Wait, I'm now hitting the database every time I want to know what color the header should be?" You're right. That's the cost of flexibility. If you have a complex page that checks twenty different tenant settings, you've just added twenty queries to your request.

To solve this, don't just rely on the internal class cache I showed above. In a production environment, I'd wrap this in a PSR-16 cache (like Redis). When a tenant's settings are updated, you simply invalidate the cache key for that specific tenant. This gives you the best of both worlds: the speed of a static config file with the flexibility of a database. You're trading a small amount of architectural complexity for a system that can scale to thousands of tenants without slowing down.




📋 Practical Task

Build a Layered Tenant Configuration Resolver

Your goal is to create a ConfigResolver class that simulates the behavior of a production SaaS configuration system. You need to implement a system that checks for values in a specific order of priority: Tenant Override > Global Default > Hardcoded Fallback.

Requirements:

  • Create a class ConfigResolver that accepts an array of $tenantSettings (simulating a DB result) and an array of $globalDefaults in its constructor.
  • Implement a method get(string $key, $fallback = null).
  • The method must return the value from $tenantSettings if it exists.
  • If not in tenant settings, it must return the value from $globalDefaults.
  • If it's in neither, it must return the $fallback provided in the method argument.
  • Include a test script that instantiates the resolver for a "Premium Tenant" (who has a custom max_files limit) and a "Basic Tenant" (who relies on the global default), and print the results to the console to verify the hierarchy works.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.