production:
primary:
adapter: postgresql
url: <%= ENV['DATABASE_URL'] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
primary_replica:
adapter: postgresql
url: <%= ENV['DATABASE_REPLICA_URL'] %>
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
replica: true
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
connects_to database: { writing: :primary, reading: :primary_replica }
end
module Api
module V1
class PostsController < BaseController
around_action :use_read_replica, only: [:index, :show]
def index
posts = Post.published.includes(:author).page(params[:page])
render json: posts
end
def create
# Automatically uses primary for writes
post = current_user.posts.create!(post_params)
render json: post, status: :created
end
private
def use_read_replica
ActiveRecord::Base.connected_to(role: :reading) do
yield
end
end
end
end
end
As applications grow, read operations often dominate database load. Directing reads to replica databases while keeping writes on the primary reduces contention and improves response times. Rails makes this straightforward with connects_to and role-based routing. I configure database.yml with separate connection pools for primary and replica, and use ActiveRecord::Base.connected_to(role: :reading) blocks to route queries to replicas. The framework automatically routes writes to primary regardless of the current role. For API endpoints that only read data, I set a controller-level around_action to use the reading role by default. Replica lag is a consideration—I use primaries for reads immediately after writes to avoid stale data.