ruby 98 lines · 4 tabs

Database-Backed Unique Slugs with Retry

Shared by codesnips Jan 2026
4 tabs
class AddSlugToPosts < ActiveRecord::Migration[7.1]
  def change
    add_column :posts, :slug, :string, null: false
    add_index :posts, :slug, unique: true
  end
end
4 files · ruby Explain with highlit

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

Share this code

Here's the card — post it anywhere.

Database-Backed Unique Slugs with Retry — share card
Link copied