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.