ruby 45 lines · 10 steps

Idempotent payment creation in Rails

How a Rails controller uses an idempotency key to make retried POST requests safe to repeat.

Explained by highlit
1class PaymentsController < ApplicationController
2 before_action :require_idempotency_key
3 
4 def create
5 record = IdempotentRequest.find_or_initialize_by(
6 key: request.headers["Idempotency-Key"],
7 endpoint: "POST /payments"
8 )
9 
10 if record.persisted?
11 return render json: record.response_body, status: record.response_status
12 end
13 
14 record.request_fingerprint = Digest::SHA256.hexdigest(request.raw_post)
15 
16 ActiveRecord::Base.transaction do
17 record.save!
18 payment = Payment.create!(payment_params.merge(user: current_user))
19 record.update!(
20 response_status: :created,
21 response_body: PaymentSerializer.new(payment).as_json
22 )
23 end
24 
25 render json: record.response_body, status: :created
26 rescue ActiveRecord::RecordNotUnique
27 existing = IdempotentRequest.find_by!(
28 key: request.headers["Idempotency-Key"],
29 endpoint: "POST /payments"
30 )
31 render json: existing.response_body, status: existing.response_status
32 end
33 
34 private
35 
36 def require_idempotency_key
37 return if request.headers["Idempotency-Key"].present?
38 
39 render json: { error: "Idempotency-Key header is required" }, status: :bad_request
40 end
41 
42 def payment_params
43 params.require(:payment).permit(:amount_cents, :currency, :description)
44 end
45end
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1An idempotency key lets clients safely retry a request without creating duplicate side effects.
  2. 2Wrapping the record write and the real work in one transaction keeps them atomic — both happen or neither does.
  3. 3A unique constraint plus a RecordNotUnique rescue handles the race where two identical requests arrive at once.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent payment creation in Rails — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code