Rails

Rails, PostgreSQL, and UUID primary keys

UUID primary keys are useful when independent generation, merge safety, or public identifier opacity matters. They are not a default upgrade.

Rails Paulo Fidalgo

UUID primary keys help when records need identifiers that can be generated independently across systems. They are not automatically better than sequential IDs.

The tradeoff matters. UUIDs can make data merging, replication, and public identifiers easier. They can also make sorting less obvious and indexes larger. Use them when those properties are worth the cost.

When UUIDs make sense

UUIDs are a good fit when records may be created in more than one place and later merged.

They also help when exposing sequential IDs would leak information you do not want to expose.

Common examples:

  • systems that synchronize data across databases.
  • applications that shard or archive records.
  • public URLs where sequential IDs reveal volume or ordering.
  • integrations where external systems need stable identifiers.

If none of those constraints exist, an integer primary key may still be the simpler choice.

Enable UUID support in PostgreSQL

Rails with PostgreSQL can use database-generated UUIDs. In older Rails applications this commonly meant enabling pgcrypto:

class EnablePgcryptoExtension < ActiveRecord::Migration[7.1]
  def change
    enable_extension "pgcrypto" unless extension_enabled?("pgcrypto")
  end
end

Then configure generators to create UUID primary and foreign keys:

module MyApp
  class Application < Rails::Application
    config.generators do |g|
      g.orm :active_record,
        primary_key_type: :uuid,
        foreign_key_type: :uuid
    end
  end
end

A generated table can then use UUID IDs:

create_table :orders, id: :uuid do |t|
  t.references :customer, type: :uuid, null: false, foreign_key: true
  t.decimal :total, precision: 10, scale: 2, null: false
  t.timestamps
end

The important detail is consistency. If the referenced table uses UUIDs, the foreign key must also use UUIDs.

Migrating existing applications

For an application still in early development, it can be reasonable to recreate local databases and adjust the migrations before real data exists.

For a live application, treat primary-key migration as a real data migration.

You need a plan for foreign keys, indexes, application code, background jobs, API clients, and any external references.

That work is rarely just a migration file.

Caveats

UUIDs are not sortable by creation order. If the application needs chronological ordering, use an explicit timestamp:

scope :oldest_first, -> { order(created_at: :asc) }

Avoid adding a default scope just to recover .first and .last semantics. Default scopes tend to leak into unrelated queries and surprise future code.

The better habit is to order explicitly where the ordering matters.

The practical rule

Use UUIDs when independent generation, merge safety, or public identifier opacity matters. Do not use them because they feel more sophisticated.

The right identifier strategy is the one that matches the system’s data lifecycle.

Addendum: Rails 8, PostgreSQL 18, and UUIDv7

The original version of this note was written for Rails 5-era applications. The shape is different now.

Rails 8 applications can still configure UUID primary keys through generators:

config.generators do |g|
  g.orm :active_record, primary_key_type: :uuid
end

That gets new tables using UUID IDs, but it does not answer the whole question.

You still need to decide where the UUID is generated and which UUID version you want.

PostgreSQL 18 changes the tradeoff because it includes native UUIDv7 generation:

create_table :events, id: :uuid, default: -> { "uuidv7()" } do |t|
  t.timestamps
end

UUIDv7 is time-ordered. That makes it friendlier to B-tree indexes than random UUIDv4 values.

It still keeps the distributed-ID properties that make UUIDs useful.

Ruby also has Random.uuid_v7, so applications can generate UUIDv7 values outside the database.

That can fit databases without native UUIDv7 support. It can also fit workflows where the ID must exist before a record is inserted.

The choice today is:

  • use PostgreSQL’s uuidv7() when the database should own ID generation.
  • use application-generated UUIDv7 when the ID must exist before persistence or the database lacks native support.
  • keep UUIDv4 when ordering and index locality do not matter enough to justify changing.

For MySQL and SQLite, Rails can still model UUID primary keys. The storage and generation details are adapter-specific.

Treat that as a database design decision, not just a Rails generator setting.

Useful references: