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.