Jhumur Chatterjee

Fullstack Developer

Scaling Rails Applications with Multiple Databases: A Complete Guide

As Rails applications grow in complexity and scale, managing all your data in a single database can become a bottleneck. Whether you're dealing with massive datasets, need to separate read and write operations, or want to isolate different business domains, Rails' multiple database support provides elegant solutions to these challenges.

In this comprehensive guide, we'll explore how to configure, implement, and optimize multiple databases in your Rails application.

Why Use Multiple Databases?

Before diving into implementation, let's understand the key scenarios where multiple databases make sense:

Read/Write Splitting: Distribute read operations across replica databases while keeping writes on the primary database. This improves performance and reduces load on your main database server.

Domain Separation: Isolate different business domains or microservices within a monolithic application. For example, keeping user authentication data separate from analytics data.

Legacy Integration: Connect to existing databases when migrating from legacy systems or integrating with third-party applications.

Performance Optimization: Distribute heavy analytical queries to dedicated databases, keeping your main application database responsive.

Setting Up Multiple Databases in Rails

Rails 6.0 introduced native support for multiple databases, making configuration straightforward. Let's start with the basic setup.

Database Configuration

First, update your config/database.yml to define multiple databases:

default: &default
  adapter: postgresql
  encoding: unicode
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  host: <%= ENV['DATABASE_HOST'] %>

development:
  primary:
    <<: *default
    database: myapp_development

  analytics:
    <<: *default
    database: myapp_analytics_development

  users:
    <<: *default
    database: myapp_users_development

production:
  primary:
    <<: *default
    database: myapp_production

  analytics:
    <<: *default
    database: myapp_analytics_production

  users:
    <<: *default
    database: myapp_users_production

Application Configuration

Configure your application to recognize the multiple databases by updating config/application.rb:

module MyApp
  class Application < Rails::Application
    config.load_defaults 7.0

    # Configure multiple databases
    config.active_record.multiple_databases = true
  end
end

Creating Database-Specific Models

With multiple databases configured, you need to tell your models which database to use. Rails provides several approaches for this.

Abstract Base Classes

Create abstract base classes for each database:

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :primary, reading: :primary }
end

# app/models/analytics_record.rb
class AnalyticsRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :analytics, reading: :analytics }
end

# app/models/users_record.rb
class UsersRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: { writing: :users, reading: :users }
end

Model Implementation

Now create your models inheriting from the appropriate base class:

# app/models/product.rb
class Product < ApplicationRecord
  has_many :orders
  validates :name, presence: true
end

# app/models/analytics/page_view.rb
class Analytics::PageView < AnalyticsRecord
  validates :url, presence: true
  validates :user_id, presence: true
end

# app/models/user.rb
class User < UsersRecord
  has_secure_password
  validates :email, presence: true, uniqueness: true
end

Database Migrations for Multiple Databases

Managing migrations across multiple databases requires some additional setup.

Migration Structure

Organize your migrations by database:

db/
├── migrate/                 # Primary database migrations
├── analytics_migrate/       # Analytics database migrations
└── users_migrate/          # Users database migrations

Migration Configuration

Update your migration paths in config/application.rb:

config.paths.add "db/analytics_migrate", glob: "*.rb"
config.paths.add "db/users_migrate", glob: "*.rb"

Running Migrations

Create a custom rake task to handle migrations across all databases:

# lib/tasks/db_multi.rake
namespace :db do
  namespace :multi do
    desc "Run migrations for all databases"
    task migrate: :environment do
      ActiveRecord::Base.connected_to(database: :primary) do
        Rake::Task["db:migrate"].invoke
      end

      ActiveRecord::Base.connected_to(database: :analytics) do
        ActiveRecord::MigrationContext.new("db/analytics_migrate").migrate
      end

      ActiveRecord::Base.connected_to(database: :users) do
        ActiveRecord::MigrationContext.new("db/users_migrate").migrate
      end
    end
  end
end

Read/Write Splitting Implementation

One of the most common use cases for multiple databases is implementing read/write splitting for better performance.

Configuration for Read Replicas

Update your database.yml to include read replicas:

production:
  primary:
    <<: *default
    database: myapp_production

  primary_replica:
    <<: *default
    database: myapp_production
    host: <%= ENV['READ_REPLICA_HOST'] %>
    replica: true

Model Configuration

Configure your models to use read replicas:

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true

  connects_to database: {
    writing: :primary,
    reading: :primary_replica
  }
end

Automatic Read/Write Splitting

Rails automatically routes reads to replica databases when configured:

# This will use the primary database (write)
User.create(name: "John", email: "john@example.com")

# This will use the replica database (read)
User.where(active: true).limit(10)

# Force writing database for reads when needed
User.connected_to(role: :writing) do
  User.find(params[:id])
end

Cross-Database Associations and Queries

Working with data across multiple databases requires careful consideration of associations and queries.

Handling Cross-Database Associations

Rails doesn't support associations across databases directly. You'll need to implement custom solutions:

class Product < ApplicationRecord
  def page_views
    Analytics::PageView.where(product_id: id)
  end

  def total_views
    page_views.count
  end
end

class Analytics::PageView < AnalyticsRecord
  def product
    Product.find(product_id)
  end
end

Service Objects for Cross-Database Operations

Create service objects to handle complex cross-database operations:

class ProductAnalyticsService
  def self.product_performance(product_id)
    product = Product.find(product_id)

    analytics_data = Analytics::PageView
      .where(product_id: product_id)
      .group(:date)
      .count

    {
      product: product,
      views: analytics_data,
      total_views: analytics_data.values.sum
    }
  end
end

Connection Management and Performance

Proper connection management is crucial when working with multiple databases.

Connection Pool Configuration

Configure appropriate connection pools for each database:

production:
  primary:
    <<: *default
    database: myapp_production
    pool: 25

  analytics:
    <<: *default
    database: myapp_analytics_production
    pool: 10  # Fewer connections for analytics

Manual Connection Management

When you need fine-grained control over connections:

class ReportGenerator
  def generate_cross_database_report
    primary_data = nil
    analytics_data = nil

    ActiveRecord::Base.connected_to(database: :primary) do
      primary_data = Product.joins(:orders)
        .group(:category)
        .sum(:price)
    end

    ActiveRecord::Base.connected_to(database: :analytics) do
      analytics_data = Analytics::PageView
        .group(:product_category)
        .count
    end

    merge_report_data(primary_data, analytics_data)
  end
end

Testing with Multiple Databases

Testing applications with multiple databases requires additional setup.

Test Database Configuration

Configure test databases in database.yml:

test:
  primary:
    <<: *default
    database: myapp_test

  analytics:
    <<: *default
    database: myapp_analytics_test

  users:
    <<: *default
    database: myapp_users_test

Test Setup

Create a helper to manage test database setup:

# test/support/database_helper.rb
module DatabaseHelper
  def setup_test_databases
    ActiveRecord::Base.connected_to(database: :primary) do
      ActiveRecord::Schema.define do
        # Primary database schema
      end
    end

    ActiveRecord::Base.connected_to(database: :analytics) do
      ActiveRecord::Schema.define do
        # Analytics database schema
      end
    end
  end
end

Factory Setup

Configure factories to work with multiple databases:

# test/factories/products.rb
FactoryBot.define do
  factory :product do
    name { "Sample Product" }
    price { 19.99 }

    trait :with_analytics do
      after(:create) do |product|
        ActiveRecord::Base.connected_to(database: :analytics) do
          create(:page_view, product_id: product.id)
        end
      end
    end
  end
end

Monitoring and Maintenance

Monitoring multiple databases requires additional consideration.

Health Checks

Implement health checks for all databases:

class DatabaseHealthCheck
  def self.check_all
    databases = [:primary, :analytics, :users]

    databases.map do |db|
      {
        database: db,
        status: check_database(db),
        connection_count: connection_count(db)
      }
    end
  end

  private

  def self.check_database(db)
    ActiveRecord::Base.connected_to(database: db) do
      ActiveRecord::Base.connection.execute("SELECT 1")
      'healthy'
    end
  rescue => e
    'unhealthy'
  end
end

Performance Monitoring

Monitor query performance across databases:

class DatabaseMetrics
  def self.slow_queries_by_database
    databases = [:primary, :analytics, :users]

    databases.each_with_object({}) do |db, metrics|
      ActiveRecord::Base.connected_to(database: db) do
        metrics[db] = slow_query_count
      end
    end
  end
end

Best Practices and Common Pitfalls

Here are key best practices when working with multiple databases:

Plan Your Database Strategy: Carefully consider which data belongs in which database. Avoid splitting related data that needs to be queried together frequently.

Avoid Cross-Database Transactions: Rails doesn't support distributed transactions across databases. Design your application to handle this constraint.

Monitor Connection Pools: Each database connection consumes resources. Monitor and tune your connection pools appropriately.

Handle Replication Lag: When using read replicas, account for potential replication lag in your application logic.

Backup Strategy: Ensure your backup strategy covers all databases and maintains consistency across related data.

Testing Strategy: Test cross-database scenarios thoroughly, including failure cases where one database might be unavailable.

Conclusion

Multiple databases in Rails applications provide powerful solutions for scaling, performance optimization, and domain separation. While they introduce complexity, the benefits often outweigh the costs for growing applications.

The key to success is careful planning, proper configuration, and adherence to best practices. Start simple with clear separation of concerns, monitor performance closely, and be prepared to refactor as your application evolves.

Remember that multiple databases are a tool, not a requirement. Many successful Rails applications operate perfectly well with a single database. Consider your specific use case, performance requirements, and team expertise when making the decision to adopt multiple databases.

With Rails' robust multiple database support, you have the flexibility to design database architectures that grow with your application's needs while maintaining the developer productivity that makes Rails special.