Back to Blog
Lesson 13 of the Advanced Laravel: Architecture, Scaling & Performance course
LaravelJune 27, 20263 min read

Handling Multi-Database Connections in Laravel: Scaling SaaS

Learn how to dynamically swap database connections and manage cross-database transactions in Laravel to build robust, scalable multi-tenant SaaS architectures.

LaravelDatabaseMulti-tenancyTransactionsArchitecturephpbackend

Previously in this course, we explored Read/Write Database Splitting to improve query throughput. While that pattern focuses on scaling a single logical database, this lesson tackles the reality of complex SaaS: managing data across physically or logically separated databases within a single request lifecycle.

Whether you are implementing a "database-per-tenant" strategy or aggregating data from microservices, you need to master how to dynamically swap connections and handle the inherent risks of cross-database transactions.

The Architecture of Multi-Database Connections

In Laravel, the config/database.php file defines your available connections. While most applications rely on the default connection, a sophisticated architecture often requires runtime switching. Laravel’s DB manager allows you to access any configured connection via DB::connection('name').

To move beyond static access, we rely on the Illuminate\Database\DatabaseManager. When you need to support multi-tenancy where each user has their own database, you shouldn't hardcode connections. Instead, you should dynamically add them to the configuration at runtime or use a "tenant resolver" pattern.

Dynamically Swapping Connections

The most common mistake is attempting to change the default connection globally. Instead, inject the connection dynamically based on the current context.

PHP
#6A9955">// TenantAwareRepository.php
public function forTenant(Tenant $tenant)
{
    #6A9955">// Update the config dynamically for the current request
    config(['database.connections.tenant.database' => $tenant->db_name]);
    
    #6A9955">// Purge the connection to ensure the next call uses the new config
    DB::purge('tenant');
    DB::reconnect('tenant');
    
    return DB::connection('tenant');
}

By purging the connection, you force Laravel to re-read the configuration. This is critical in long-running processes (like queue workers) or when multiple tenants are processed in a single lifecycle.

Managing Transaction Consistency Across Databases

The biggest challenge with multi-database architectures is that standard database transactions are local to the connection. DB::beginTransaction() only locks the database it is currently communicating with.

If your business logic requires updating a record in the tenant database and a record in your central (main) database, a standard transaction won't prevent partial failures.

The Two-Phase Commit Problem

True distributed transactions (Two-Phase Commit) are notoriously difficult to implement in PHP and often introduce significant latency. In a production SaaS environment, we prefer Eventual Consistency or Compensating Actions over distributed locks.

However, if you must perform atomic operations, you should wrap your operations in a manual orchestration layer:

PHP
public function executeCrossDbAction(callable $callback)
{
    DB::connection('central')->beginTransaction();
    
    try {
        $result = $callback();
        
        DB::connection('tenant')->beginTransaction();
        #6A9955">// ... perform tenant operations
        DB::connection('tenant')->commit();
        
        DB::connection('central')->commit();
    } catch (\Exception $e) {
        DB::connection('tenant')->rollBack();
        DB::connection('central')->rollBack();
        throw $e;
    }
}

Note: This is a "best effort" approach. If the second commit() fails, your central database remains updated while the tenant database is rolled back. In production, always pair this with a background job that reconciles state.

Hands-on Exercise: Implementing a Tenant Connection Switcher

  1. Create a TenantConnectionManager service.
  2. Implement a method setTenant(Tenant $tenant) that updates the tenant connection configuration.
  3. Use a middleware to resolve the tenant from the request (e.g., via a subdomain or header) and call your manager.
  4. Verify that Eloquent models explicitly set to the tenant connection are correctly routed to the new database.

Common Pitfalls

  • Connection Leaks: If you use DB::reconnect() without DB::purge() in a long-running process, you may exhaust your database connection pool.
  • Eloquent Model Caching: If you have models that cache schema information, changing the database schema (e.g., adding a column) on a tenant database might not be reflected immediately. Always clear your cache when switching between drastically different tenant schemas.
  • The "Default" Trap: Never rely on the default connection in a multi-tenant environment. Always explicitly define the connection on your base models (e.g., protected $connection = 'tenant';).

Summary

Multi-database architectures allow for granular isolation and massive scaling, but they introduce complexity in transaction management. By mastering DB::purge() and DB::reconnect(), you can safely swap connections at runtime. When dealing with cross-database operations, prioritize atomic operations where possible, but prepare your system for eventual consistency when transactions span multiple database boundaries.

Up next, we will dive into Eloquent Caching Strategies to ensure that our multi-database queries remain performant even under heavy load.

Similar Posts