ruby 102 lines · 4 tabs

Idempotent Form Submissions in Rails with an Idempotency-Key Column and before_action Guard

Shared by codesnips Sep 2026
4 tabs
class CreateIdempotencyKeys < ActiveRecord::Migration[7.1]
  def change
    create_table :idempotency_keys do |t|
      t.string :key, null: false
      t.string :request_path, null: false
      t.datetime :locked_at
      t.integer :response_status
      t.jsonb :response_body
      t.references :user, foreign_key: true
      t.timestamps
    end

    add_index :idempotency_keys, [:key, :request_path], unique: true
    add_index :idempotency_keys, :created_at
  end
end
4 files · ruby Explain with highlit

Duplicate POST requests are one of the most common sources of double-charged orders and duplicate records: a user double-clicks submit, a proxy retries a timed-out request, or a mobile client replays a queued mutation. This snippet enforces exactly-once semantics at the application layer using an idempotency key persisted per client, so replaying the same request returns the original result instead of creating a second row.

The CreateIdempotencyKeys migration establishes the storage. Each row is keyed by a key string plus a request_path so the same UUID can be reused safely across different endpoints. A unique index on [:key, :request_path] is the real enforcement mechanism — the database, not Ruby, guarantees no two concurrent requests can both insert the same key. The locked_at, response_status, and response_body columns let the record double as both a lock and a cache of the completed response.

In IdempotencyKey model, claim! performs an atomic upsert: insert_all with unique_by and on_duplicate: :skip inserts the row only if it does not already exist, and the return value reveals whether this request won the race. store_response! records the final status and body once the action succeeds. Keeping this logic on the model keeps the controller thin.

The Idempotent concern provides require_idempotency_key, wired in as a before_action. It reads the Idempotency-Key header, rejects requests without one, and calls claim!. If the key was already claimed, it replays the stored response verbatim — or returns 409 Conflict if the original request is still in flight (locked_at set but no response yet). Otherwise the action runs normally.

OrdersController shows the integration: before_action :require_idempotency_key, only: :create, then a call to store_idempotent_response after the order is created. A subtle trade-off is deciding when to store — storing on both success and handled failures avoids re-running expensive side effects, but transient 500s should generally NOT be cached so genuine retries can succeed. Old keys should be swept periodically to bound table growth. This pattern shifts correctness from client discipline to a server-side guarantee backed by a unique constraint.


Related snips

Share this code

Here's the card — post it anywhere.

Idempotent Form Submissions in Rails with an Idempotency-Key Column and before_action Guard — share card
Link copied