{"id":2343,"date":"2026-05-27T18:15:57","date_gmt":"2026-05-27T18:15:57","guid":{"rendered":"https:\/\/tucumandevelopers.com\/index.php\/2026\/05\/27\/the-better-primary-key-a-guide-to-ulids-for-rails-developers\/"},"modified":"2026-05-27T18:15:57","modified_gmt":"2026-05-27T18:15:57","slug":"the-better-primary-key-a-guide-to-ulids-for-rails-developers","status":"publish","type":"post","link":"https:\/\/tucumandevelopers.com\/index.php\/2026\/05\/27\/the-better-primary-key-a-guide-to-ulids-for-rails-developers\/","title":{"rendered":"The Better Primary Key: A Guide to ULIDs for Rails Developers"},"content":{"rendered":"<div>\n<div><\/div>\n<div data-article-id=\"3766058\" id=\"article-body\">\n<p>In a previous article, I talked about <strong>Snowflake IDs<\/strong>. They are great, but they require a bit of configuration because you need to manage &#8220;Worker IDs.&#8221; <\/p>\n<p>If you want something simpler that gives you the same benefits &#8211; secure URLs and fast database sorting\u2014you should look at <strong>ULIDs<\/strong> (Universally Unique Lexicographically Sortable Identifiers).<\/p>\n<h2> <a name=\"why-not-just-use-uuids\" href=\"#why-not-just-use-uuids\"> <\/a> Why not just use UUIDs? <\/h2>\n<p>Most developers switch from auto-incrementing integers to <strong>UUIDs<\/strong> because they want to hide their business volume. If your order ID is <code>order\/550e8400-e29b...<\/code>, no one knows if you have 1 customer or 1 million.<\/p>\n<p>But UUIDs have a major problem: <strong>they are completely random.<\/strong> <br \/> When you insert millions of random UUIDs into a database, the &#8220;B-Tree&#8221; index gets fragmented and slow. Your database has to jump all over the hard drive to find where to put the new data.<\/p>\n<p><strong>ULIDs fix this.<\/strong><br \/> A ULID is 128 bits (just like a UUID), but the first part of the ID is a <strong>timestamp<\/strong>. This means ULIDs are chronological. They are unique like a UUID, but they sort like an Integer.<\/p>\n<p>Here is how to implement ULIDs in your Rails 8 app in 4 easy steps.<\/p>\n<h2> <a name=\"step-1-install-the-gem\" href=\"#step-1-install-the-gem\"> <\/a> STEP 1: Install the Gem <\/h2>\n<p>We will use the <code>ulid<\/code> gem to generate the strings. Add this to your Gemfile: <\/p>\n<div>\n<pre><code><span>gem<\/span> <span>\"ulid\"<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Run <code>bundle install<\/code> in your terminal.<\/p>\n<h2> <a name=\"step-2-the-ulid-concern\" href=\"#step-2-the-ulid-concern\"> <\/a> STEP 2: The ULID Concern <\/h2>\n<p>We want to make it very easy to add ULIDs to any model. The best way to do this is with a <strong>Concern<\/strong>. This code will ensure that every time we create a new record, a ULID is generated and assigned to the ID.<\/p>\n<p>Create a new file at <code>app\/models\/concerns\/has_ulid.rb<\/code>: <\/p>\n<div>\n<pre><code><span># app\/models\/concerns\/has_ulid.rb<\/span> <span>module<\/span> <span>HasUlid<\/span> <span>extend<\/span> <span>ActiveSupport<\/span><span>::<\/span><span>Concern<\/span> <span>included<\/span> <span>do<\/span> <span># Before we save to the DB, generate the ULID<\/span> <span>before_create<\/span> <span>:set_ulid<\/span> <span>end<\/span> <span>private<\/span> <span>def<\/span> <span>set_ulid<\/span> <span># ULID.generate creates a string like: 01ARZ3NDEKTSV4RRFFQ69G5FAV<\/span> <span>self<\/span><span>.<\/span><span>id<\/span> <span>||=<\/span> <span>ULID<\/span><span>.<\/span><span>generate<\/span> <span>end<\/span> <span>end<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h2> <a name=\"step-3-the-migration\" href=\"#step-3-the-migration\"> <\/a> STEP 3: The Migration <\/h2>\n<p>When you create a new model, you need to tell Rails that the <code>id<\/code> is a <code>string<\/code>, and you must disable the default auto-increment logic. <\/p>\n<div>\n<pre><code>rails g model Product name:string <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>Open the migration file and modify it like this: <\/p>\n<div>\n<pre><code><span># db\/migrate\/XXXXXXXXXXXXXX_create_products.rb<\/span> <span>class<\/span> <span>CreateProducts<\/span> <span>&lt;<\/span> <span>ActiveRecord<\/span><span>::<\/span><span>Migration<\/span><span>[<\/span><span>8.0<\/span><span>]<\/span> <span>def<\/span> <span>change<\/span> <span># id: false stops the automatic integer ID<\/span> <span>create_table<\/span> <span>:products<\/span><span>,<\/span> <span>id: <\/span><span>false<\/span> <span>do<\/span> <span>|<\/span><span>t<\/span><span>|<\/span> <span># We use string for ULID primary key<\/span> <span>t<\/span><span>.<\/span><span>string<\/span> <span>:id<\/span><span>,<\/span> <span>primary_key: <\/span><span>true<\/span> <span>t<\/span><span>.<\/span><span>string<\/span> <span>:name<\/span> <span>t<\/span><span>.<\/span><span>timestamps<\/span> <span>end<\/span> <span>end<\/span> <span>end<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h2> <a name=\"step-4-update-the-model\" href=\"#step-4-update-the-model\"> <\/a> STEP 4: Update the Model <\/h2>\n<p>Now, just include the concern we wrote in Step 2. <\/p>\n<div>\n<pre><code><span># app\/models\/product.rb<\/span> <span>class<\/span> <span>Product<\/span> <span>&lt;<\/span> <span>ApplicationRecord<\/span> <span>include<\/span> <span>HasUlid<\/span> <span>end<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<h2> <a name=\"step-5-see-it-in-action\" href=\"#step-5-see-it-in-action\"> <\/a> STEP 5: See it in action <\/h2>\n<p>Open your Rails console (<code>bin\/rails c<\/code>) and create a few products: <\/p>\n<div>\n<pre><code><span>Product<\/span><span>.<\/span><span>create<\/span><span>(<\/span><span>name: <\/span><span>\"Laptop\"<\/span><span>)<\/span> <span>Product<\/span><span>.<\/span><span>create<\/span><span>(<\/span><span>name: <\/span><span>\"Monitor\"<\/span><span>)<\/span> <span>Product<\/span><span>.<\/span><span>create<\/span><span>(<\/span><span>name: <\/span><span>\"Keyboard\"<\/span><span>)<\/span> <span># Check the IDs<\/span> <span>Product<\/span><span>.<\/span><span>pluck<\/span><span>(<\/span><span>:id<\/span><span>)<\/span> <span># =&gt; [\"01HQV...\", \"01HQV...\", \"01HQV...\"]<\/span> <\/code><\/pre>\n<div>\n<\/p><\/div>\n<\/p><\/div>\n<p>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 <code>Product.order(:id)<\/code> and they will be in the correct chronological order!<\/p>\n<h2> <a name=\"why-i-like-ulids-for-rails\" href=\"#why-i-like-ulids-for-rails\"> <\/a> Why I like ULIDs for Rails <\/h2>\n<ol>\n<li> <strong>Better Performance:<\/strong> Because the IDs are sortable, PostgreSQL (or SQLite) can append them to the end of the index. This is much faster for &#8220;Write-Heavy&#8221; apps than random UUIDs.<\/li>\n<li> <strong>Readability:<\/strong> ULIDs use a special alphabet (Crockford&#8217;s Base32) that excludes confusing letters like &#8220;I&#8221;, &#8220;L&#8221;, and &#8220;O&#8221;. This makes them easier to read if a user has to type one in.<\/li>\n<li> <strong>No Setup:<\/strong> Unlike Snowflake IDs, you don&#8217;t need to configure server IDs or worker nodes. You just install the gem and go.<\/li>\n<\/ol>\n<p>That&#8217;s pretty much it. It\u2019s a small architectural change that makes your app feel much more professional and scalable.<\/p>\n<\/p><\/div>\n<\/div>\n<\/div>\n<\/div>\n<p>Fuente: <a href=\"https:\/\/dev.to\/zilton7\/the-better-primary-key-a-guide-to-ulids-for-rails-developers-424h\">Art\u00edculo original<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In a previous article, I talked about Snowflake IDs. They are great, but they require a bit of configuration because you need to manage &#8220;Worker IDs.&#8221; If you want something simpler that gives you the same benefits &#8211; secure URLs and fast database sorting\u2014you should look at ULIDs (Universally Unique Lexicographically Sortable Identifiers). Why not [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2342,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":"","jetpack_publicize_message":"","jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":true,"jetpack_social_options":{"image_generator_settings":{"template":"highway","default_image_id":0,"font":"","enabled":false},"version":2}},"categories":[41],"tags":[],"class_list":["post-2343","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devto"],"jetpack_publicize_connections":[],"_links":{"self":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2343","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/comments?post=2343"}],"version-history":[{"count":0,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/posts\/2343\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media\/2342"}],"wp:attachment":[{"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/media?parent=2343"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/categories?post=2343"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tucumandevelopers.com\/index.php\/wp-json\/wp\/v2\/tags?post=2343"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}