module Middleware
class DatabasePinning
PIN_WINDOW = 5.seconds
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
last_write = request.session[:last_write_at]
if pin_to_primary?(last_write)
ActiveRecord::Base.connected_to(role: :writing) do
@app.call(env)
end
else
@app.call(env)
end
end
private
def pin_to_primary?(last_write)
return false if last_write.blank?
Time.zone.at(last_write) > PIN_WINDOW.ago
rescue ArgumentError, TypeError
false
end
end
end
class ApplicationController < ActionController::Base
WRITE_METHODS = %w[POST PATCH PUT DELETE].freeze
after_action :mark_write
private
def mark_write
return unless WRITE_METHODS.include?(request.request_method)
return unless response.successful? || response.redirect?
session[:last_write_at] = Time.current.to_i
end
end
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
connects_to database: {
writing: :primary,
reading: :primary_replica
}
end
production:
primary:
<<: *default
host: <%= ENV["DB_WRITER_HOST"] %>
database: app_production
username: <%= ENV["DB_USER"] %>
password: <%= ENV["DB_PASSWORD"] %>
primary_replica:
<<: *default
replica: true
host: <%= ENV["DB_READER_HOST"] %>
database: app_production
username: <%= ENV["DB_RO_USER"] %>
password: <%= ENV["DB_RO_PASSWORD"] %>
migrations_paths: db/migrate
Read replicas absorb read traffic, but they lag behind the primary by milliseconds to seconds. That lag breaks a very common expectation: after a user submits a form (a POST), the immediate redirect and follow-up GET should reflect what was just written. If that GET is served from a stale replica, the user sees their edit vanish — the classic "read your writes" violation. This snippet implements session-based primary pinning so that reads following a recent write are routed to the primary for a short window.
In DatabasePinning middleware, the Rack middleware reads a last_write_at timestamp stored in the encrypted session cookie. If a write happened within PIN_WINDOW, it wraps the request in ActiveRecord::Base.connected_to(role: :writing) so every query in that request hits the primary. Otherwise it lets Rails' automatic role switching route reads to a replica. The window is deliberately short (a few seconds) — just long enough to cover the redirect-and-render cycle while replication catches up, so the vast majority of read traffic still benefits from replicas.
The write side lives in ApplicationController. The after_action callback mark_write inspects request.request_method and stamps session[:last_write_at] after any non-idempotent verb (POST, PATCH, PUT, DELETE). Storing the marker in the session ties pinning to a single user rather than pinning the whole app, which would defeat the purpose of having replicas. Because the value lives in the signed/encrypted cookie, it survives the redirect without any server-side state.
config/database.yml shows the two-connection setup: a primary pointing at the writer endpoint and a primary_replica marked replica: true sharing the same migrations_paths. This is what lets connects_to database: { writing: :primary, reading: :primary_replica } and automatic role switching work.
The main trade-off is that pinning is time-based, not replication-aware: it assumes lag is under PIN_WINDOW. Under heavy replication delay a stale read can still slip through, so the window should be tuned to the observed p99 lag. A pitfall to watch is background jobs and async work spawned inside a request — they run outside the middleware and won't inherit the pin, so any read-your-writes guarantees there must be enforced explicitly with connected_to.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
package dbutil
import (
"context"
"github.com/jackc/pgconn"
Retry Postgres serialization failures with bounded attempts
Share this code
Here's the card — post it anywhere.