class AddSlugToPosts < ActiveRecord::Migration[7.1]
def change
add_column :posts, :slug, :string, null: false
add_index :posts, :slug, unique: true
end
end
module Sluggable
extend ActiveSupport::Concern
included do
class_attribute :slug_source_attribute, instance_writer: false
before_validation :assign_unique_slug, on: :create
end
class_methods do
def sluggable_on(attribute)
self.slug_source_attribute = attribute
end
end
def to_param
slug
end
def assign_unique_slug
return if slug.present?
self.slug = next_available_slug
end
def next_available_slug
base = public_send(self.class.slug_source_attribute).to_s.parameterize
base = "post" if base.blank?
candidate = base
suffix = 1
while slug_taken?(candidate)
suffix += 1
candidate = "#{base}-#{suffix}"
end
candidate
end
private
def slug_taken?(value)
self.class.where(slug: value).where.not(id: id).exists?
end
end
class Post < ApplicationRecord
include Sluggable
MAX_SLUG_RETRIES = 5
sluggable_on :title
validates :title, presence: true
validates :slug, presence: true, uniqueness: true
def save_with_unique_slug
attempts = 0
begin
save!
rescue ActiveRecord::RecordNotUnique => e
raise unless e.message.include?("index_posts_on_slug")
attempts += 1
raise if attempts >= MAX_SLUG_RETRIES
self.slug = next_available_slug
retry
end
end
end
class PostsController < ApplicationController
def create
@post = Post.new(post_params)
if @post.save_with_unique_slug
redirect_to post_path(@post), notice: "Post published."
else
render :new, status: :unprocessable_entity
end
rescue ActiveRecord::RecordNotUnique
@post.errors.add(:base, "Could not generate a unique slug, please retry.")
render :new, status: :conflict
end
def show
@post = Post.find_by!(slug: params[:id])
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
Generating human-readable slugs like my-post-title is easy until two requests try to save the same title at the same moment. A naive slug = title.parameterize; slug += "-2" if Post.exists?(slug: slug) check has a time-of-check-to-time-of-use gap: both requests read that the slug is free, both pick the same value, and one insert blows up (or worse, both succeed if there is no constraint). The only reliable arbiter of uniqueness is the database itself, so this snippet leans on a unique index and treats the collision as an expected event to retry, rather than a rare exception to fear.
The Migration establishes the ground truth. It adds a slug column and a unique: true index; without that index the whole approach falls apart, because ActiveRecord validations alone cannot prevent races between separate connections. The index is the single source of truth that makes concurrent inserts safe.
Sluggable concern holds the reusable logic. The Sluggable module is mixed into any model and configured via sluggable_on :title, storing the source attribute in a class-level setting. A before_validation callback calls assign_unique_slug, which computes a base from parameterize and probes candidates: the bare base first, then base-2, base-3, and so on. Each candidate is checked against the scoped relation via slug_taken?, and to_param is overridden so URLs use the slug instead of the numeric id. This in-memory probe is only an optimization to pick a likely-free value cheaply — it is explicitly not trusted to be correct.
The real safety net lives in Post model. save_with_unique_slug wraps save in a retry loop that rescues ActiveRecord::RecordNotUnique. When the database rejects a duplicate slug, the code recomputes a fresh candidate and tries again, bounded by MAX_SLUG_RETRIES so a genuine bug cannot spin forever. This is the optimistic pattern: assume success, let the unique index enforce correctness, and handle the losing race by retrying. It avoids table locks and SELECT ... FOR UPDATE contention while remaining correct under heavy concurrency. The main pitfall is scope: the index and the slug_taken? query must agree on their uniqueness scope (global here, but often per-tenant), otherwise retries either never terminate or allow duplicates. Reach for this whenever a friendly, stable, collision-free identifier must be minted under concurrent writes.
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.