class CreateReservations < ActiveRecord::Migration[7.1]
def up
enable_extension "btree_gist" unless extension_enabled?("btree_gist")
create_table :reservations do |t|
t.references :room, null: false, foreign_key: true
t.datetime :starts_at, null: false
t.datetime :ends_at, null: false
t.tstzrange :during, null: false
t.timestamps
end
execute <<~SQL
ALTER TABLE reservations
ADD CONSTRAINT reservations_no_overlap
EXCLUDE USING gist (
room_id WITH =,
during WITH &&
);
SQL
end
def down
drop_table :reservations
end
end
class Reservation < ApplicationRecord
belongs_to :room
validates :starts_at, :ends_at, presence: true
validate :ends_after_starts
before_validation :build_during
private
def build_during
return if starts_at.blank? || ends_at.blank?
# half-open [starts_at, ends_at): back-to-back slots do not collide
self.during = Range.new(starts_at, ends_at, true)
end
def ends_after_starts
return if starts_at.blank? || ends_at.blank?
errors.add(:ends_at, "must be after the start time") if ends_at <= starts_at
end
end
class ReservationsController < ApplicationController
rescue_from ActiveRecord::StatementInvalid, with: :handle_statement_invalid
def create
reservation = Reservation.new(reservation_params)
if reservation.save
render json: reservation, status: :created
else
render json: { errors: reservation.errors }, status: :unprocessable_entity
end
end
private
def reservation_params
params.require(:reservation).permit(:room_id, :starts_at, :ends_at)
end
def handle_statement_invalid(error)
raise error unless error.cause.is_a?(PG::ExclusionViolation)
render json: {
errors: { during: ["overlaps an existing reservation for this room"] }
}, status: :unprocessable_entity
end
end
This snippet shows how to guarantee that no two reservations for the same room can overlap in time, enforced at the database level rather than in application code. The classic naive approach reads existing rows, checks for overlap in Ruby, then inserts — a pattern that races under concurrency because two requests can both pass the check before either commits. Postgres solves this cleanly with an EXCLUDE constraint backed by a GiST index over range types.
The CreateReservations migration enables the btree_gist extension, which is required to mix a scalar equality (room_id WITH =) with a range overlap operator (during WITH &&) in the same exclusion. The during column is a tstzrange, a timestamp-with-timezone range, and the constraint reads: reject any new row whose room_id equals an existing row's and whose during overlaps it. Because the check lives in the index, concurrent inserts are serialized by Postgres and overlap is impossible regardless of interleaving. The migration is wrapped so it can be expressed in raw SQL via execute, since Rails' schema DSL doesn't model exclusion constraints directly.
In Reservation model, during is assembled from starts_at and ends_at in a before_validation hook using Range.new(starts_at, ends_at), and the [) bounds semantics matter: half-open ranges mean a booking ending at 10:00 and another starting at 10:00 do not overlap, which is the intuitive behavior for back-to-back slots. The model also adds cheap presence and ordering validations so obviously bad input fails fast without a database round trip.
The interesting part is rescue_from handling in ReservationsController. When the constraint fires, Postgres raises a PG::ExclusionViolation, which ActiveRecord wraps as ActiveRecord::StatementInvalid. Catching it in create and translating it to a 422 with a friendly message turns a low-level integrity error into a clean API response, so the database is the single source of truth while the UI still gets a sensible error.
The trade-off is that overlap failures surface as exceptions rather than pre-checked validations, so callers must handle them; the payoff is correctness under load with zero locking code. This pattern fits any resource with mutually exclusive time or numeric spans — rooms, equipment, schedules, or seat assignments.
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
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
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.