module Idempotency
extend ActiveSupport::Concern
included do
before_action :check_idempotency_key, only: [:create, :update]
after_action :store_idempotent_response, only: [:create, :update]
end
private
def check_idempotency_key
return unless idempotency_key.present?
cached = Rails.cache.read(idempotency_cache_key)
return unless cached
request_hash = compute_request_hash
if cached[:request_hash] != request_hash
render json: {
error: 'IDEMPOTENCY_KEY_MISMATCH',
message: 'Idempotency key was used with different request body'
}, status: :conflict
return
end
# Return cached response
render json: cached[:response_body], status: cached[:status]
end
def store_idempotent_response
return unless idempotency_key.present?
return unless response.successful?
Rails.cache.write(
idempotency_cache_key,
{
request_hash: compute_request_hash,
response_body: JSON.parse(response.body),
status: response.status
},
expires_in: 24.hours
)
end
def idempotency_key
@idempotency_key ||= request.headers['Idempotency-Key']
end
def idempotency_cache_key
"idempotency:#{idempotency_key}"
end
def compute_request_hash
Digest::SHA256.hexdigest(request.raw_post)
end
end
Network failures and client retries can cause duplicate request processing, leading to duplicate charges, double-created resources, or inconsistent state. Idempotency keys solve this by tracking processed requests and returning cached responses for duplicates. Clients send a unique Idempotency-Key header with each mutating request. The server stores a hash of the request body along with the response in Redis or a database table. If the same key arrives with identical body hash, I return the cached response. If the key exists but the body differs, that indicates a client bug—I return 409 Conflict to signal the problem. Keys should have reasonable TTL (hours to days) to balance deduplication effectiveness with storage costs.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
Share this code
Here's the card — post it anywhere.