In a previous article, I talked about Snowflake IDs. They are great, but they require a bit of configuration because you need to manage "Worker IDs."
If you want something simpler that gives you the same benefits - secure URLs and fast database sorting—you should look at ULIDs (Universally Unique Lexicographically Sortable Identifiers).
Why not just use UUIDs?
Most developers switch from auto-incrementing integers to UUIDs because they want to hide their business volume. If your order ID is order/550e8400-e29b..., no one knows if you have 1 customer or 1 million.
But UUIDs have a major problem: they are completely random.
When you insert millions of random UUIDs into a database, the "B-Tree" index gets fragmented and slow. Your database has to jump all over the hard drive to find where to put the new data.
ULIDs fix this.
A ULID is 128 bits (just like a UUID), but the first part of the ID is a timestamp. This means ULIDs are chronological. They are unique like a UUID, but they sort like an Integer.
Here is how to implement ULIDs in your Rails 8 app in 4 easy steps.
STEP 1: Install the Gem
We will use the ulid gem to generate the strings. Add this to your Gemfile:
gem "ulid"
Run bundle install in your terminal.
STEP 2: The ULID Concern
We want to make it very easy to add ULIDs to any model. The best way to do this is with a Concern. This code will ensure that every time we create a new record, a ULID is generated and assigned to the ID.
Create a new file at app/models/concerns/has_ulid.rb:
# app/models/concerns/has_ulid.rb
module HasUlid
extend ActiveSupport::Concern
included do
# Before we save to the DB, generate the ULID
before_create :set_ulid
end
private
def set_ulid
# ULID.generate creates a string like: 01ARZ3NDEKTSV4RRFFQ69G5FAV
self.id ||= ULID.generate
end
end
STEP 3: The Migration
When you create a new model, you need to tell Rails that the id is a string, and you must disable the default auto-increment logic.
rails g model Product name:string
Open the migration file and modify it like this:
# db/migrate/XXXXXXXXXXXXXX_create_products.rb
class CreateProducts < ActiveRecord::Migration[8.0]
def change
# id: false stops the automatic integer ID
create_table :products, id: false do |t|
# We use string for ULID primary key
t.string :id, primary_key: true
t.string :name
t.timestamps
end
end
end
STEP 4: Update the Model
Now, just include the concern we wrote in Step 2.
# app/models/product.rb
class Product < ApplicationRecord
include HasUlid
end
STEP 5: See it in action
Open your Rails console (bin/rails c) and create a few products:
Product.create(name: "Laptop")
Product.create(name: "Monitor")
Product.create(name: "Keyboard")
# Check the IDs
Product.pluck(:id)
# => ["01HQV...", "01HQV...", "01HQV..."]
If you look closely, the IDs all start with the same characters because they were created in the same minute. Because they are sortable, you can still run Product.order(:id) and they will be in the correct chronological order!
Why I like ULIDs for Rails
- Better Performance: Because the IDs are sortable, PostgreSQL (or SQLite) can append them to the end of the index. This is much faster for "Write-Heavy" apps than random UUIDs.
- Readability: ULIDs use a special alphabet (Crockford's Base32) that excludes confusing letters like "I", "L", and "O". This makes them easier to read if a user has to type one in.
- No Setup: Unlike Snowflake IDs, you don't need to configure server IDs or worker nodes. You just install the gem and go.
That's pretty much it. It’s a small architectural change that makes your app feel much more professional and scalable.
Top comments (0)