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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1An idempotency key lets clients safely retry a request without creating duplicate side effects.
- 2Wrapping the record write and the real work in one transaction keeps them atomic — both happen or neither does.
- 3A unique constraint plus a RecordNotUnique rescue handles the race where two identical requests arrive at once.
Related explainers
ruby
require "csv" class SalesReport def initialize(path)
Aggregating CSV sales data in Ruby
data-aggregation
memoization
group_by
Intermediate
6 steps
ruby
module DurationFormatter UNITS = [ ['week', 604_800], ['day', 86_400],
Turning seconds into human-readable durations in Ruby
greedy-decomposition
modular-arithmetic
formatting
Intermediate
7 steps
ruby
class Comment < ApplicationRecord belongs_to :post belongs_to :author, class_name: "User"
Live-updating comments with Turbo in Rails
turbo-streams
callbacks
associations
Intermediate
8 steps
ruby
require 'json' require 'set' class SensitiveScrubber
Recursively scrubbing secrets from JSON
recursion
data-masking
pattern-matching
Intermediate
7 steps
ruby
class ReportBatcher BATCH_SIZE = 500 def initialize(account)
Batching monthly email summaries in Rails
batching
service-object
background-jobs
Intermediate
7 steps
ruby
class Comment < ApplicationRecord belongs_to :commentable, polymorphic: true, counter_cache: true belongs_to :author, class_name: "User"
How polymorphic comments work in Rails
polymorphic-association
concerns
counter-cache
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/idempotent-payment-creation-in-rails-explained-ruby-0497/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.