Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Avoiding Déjà Vu: Building Resilient APIs with ...

Avoiding Déjà Vu: Building Resilient APIs with Idempotency

Users clicking "Submit" twice, poor network connections, proxies repeating requests unexpectedly - repeated requests can cause all kinds of problems in our applications if we're not careful! Idempotency ensures repeated API calls produce consistent results, saving systems from duplicate charges, mismanaged resources, and confused customers. This talk explains idempotency’s role in building reliable APIs, with practical Laravel-based examples to show you how to implement it effectively.

We’ll share real-world stories of what happens when idempotency goes wrong (or doesn’t exist), and go through how some of the largest companies in the world like Stripe and Shopify implement this technique. Whether you’re a seasoned API developer or new to the concept, you’ll leave with actionable insights and techniques to make your APIs smarter and more user-friendly.

Avatar for Paul Conroy

Paul Conroy

October 30, 2025

More Decks by Paul Conroy

Other Decks in Technology

Transcript

  1. From Dublin, Ireland Started playing with the web 30+ years

    ago (Notepad, Frontpage & Geocities!) CTO at Square1 conroyp.com / @conroyp Paul Conroy 👴 🌍 🇮🇪
  2. 💸 Double charges 📦 Duplicate orders 🐛 Data integrity issues

    How does Déjà Vu affect our API servers? 🤬 Angry customers
  3. 💸 Double charges 📦 Duplicate orders 🐛 Data integrity issues

    How does Déjà Vu affect our API servers? 🤬 Angry customers 🗣 Higher support costs
  4. Reasons for retries • Impatient users • Mobile apps retrying

    failed connections • Load balancer failovers • CDN fallbacks
  5. 💸 Double charges 📦 Duplicate orders 🐛 Data integrity issues

    🤬 Angry customers 🗣 Higher support costs Consequences of missing idempotency
  6. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE
  7. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE
  8. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE
  9. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE Observable side-effects
  10. Naturally idempotent: • GET • HEAD • OPTIONS Non-idempotent verbs:

    • POST • PATCH HTTP Verbs • PUT • DELETE Observable side-effects
  11. 🧑💻 Seen the request before? Retrieve response from cache Generate

    response Save response to cache Return response Yes No
  12. Use a unique hash per request, storing it as a

    cache key. But where do we get the hash from? How do we know if we’ve seen a request before?
  13. Request Body Build up hash using all parameters contained in

    the request order 🧑💻 🧑💻 🍔🍟 🍔🍟
  14. IP Address Include user’s public IP address in hash 🧑💻

    🧑💻 🍔🍟 🍔🍟 🏢 104.17.3.109 104.17.3.109
  15. user_id: 3792 User ID Logged-in user’s ID 🧑💻 🧑💻 🍔🍟

    🍔🍟 🏢 user_id: 2552 user_id: null user_id: null
  16. How do we know if we’ve seen a request before?

    Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  17. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  18. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! • IP address - but shared public IPs! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  19. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! • IP address - but shared public IPs! • User ID - but guest checkouts! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  20. How do we know if we’ve seen a request before?

    • Request body - but duplicate orders! • IP address - but shared public IPs! • User ID - but guest checkouts! Make the client do some work! Use a unique hash per request, storing it as a cache key. But where do we get the hash from?
  21. 🧑💻Idempotency-Key: 1 • Make the client pass a key with

    each idempotent request • Use this as the basis for our server-side cache • Typically use UUIDs to avoid collisions • Client SDKs help with key generation
  22. 🧑💻 Seen the request before? Retrieve response from cache Generate

    response Save response to cache Return response Yes No Idempotency-Key: 1
  23. Choose a TTL based on retry behaviour, business impact, and

    storage constraints How long should we cache for? ⏳ Short TTL (Mins - Hours) ⏱ Medium TTL (Hours to Days) 📅 Long TTL (Days to Weeks) 🔒 Infinite TTL (Persistent Storage)
  24. Scenario: Mobile app order processing (Food delivery application) User Behaviour:

    Users place orders on their phones while outside. Retry Pattern: The mobile app automatically retries failed requests up to 3 times within a 5-minute window. Key Selection Strategy: UUIDs generally sufficient. ⏳ Short TTL (Mins - Hours) • Most connectivity issues resolve within minutes • Retries typically happen almost immediately or within a few minutes • After this window, a new order attempt is likely genuinely new
  25. Scenario: Batch payment processing (B2B SaaS platform) User Behaviour: Businesses

    run scheduled payment batch jobs that process hundreds of transactions. Retry Pattern: Failed batches are retried same day or the next morning. Key Selection Strategy: UUIDs still generally sufficient - user/session identifiers may be helpful for multi-day caches. ⏱ Medium TTL (Hours to Days) • Business hours and operational patterns dictate retry windows • System failures may take several hours to resolve • Next-day retries are common in business workflows
  26. Scenario: Subscription Management System (B2B SaaS platform handling subscription payments)

    User Behaviour: Users change subscription tiers, add features, etc. Retry Pattern: Support teams need to reprocess failed changes days later. Key Selection Strategy: Consider additional business context added to the key. 📅 Long TTL (Days to Weeks) • Customer service tickets often take days to resolve • Subscription changes have billing cycle implications • Retry attempts may happen after delays with customer communication
  27. Scenario: Regulatory Compliance Reporting User Behaviour: System submits mandatory reports

    to govt agencies. Retry Pattern: Failed submissions retried indefinitely until successful, but must never be duplicated. Key Selection Strategy: Additional metadata about requester. 🔒 Infinite TTL (Persistent Storage) • Regulatory requirements prohibit both missed and duplicate reports • Legal penalties for non-compliance are severe • The reporting requirement never expires
  28. Should errors be cached? Should we allow retries? What about

    errors? https://docs.stripe.com/api/idempotent_requests
  29. Should errors be cached? Should we allow retries? What about

    errors? https://docs.stripe.com/api/idempotent_requests
  30. Should errors be cached? Should we allow retries? What about

    errors? https://docs.stripe.com/api/idempotent_requests
  31. Cache locking Using a lock for the process allows us

    to ensure only one process handles the request.
 • Try to get a cache lock • If we can, process the request then release the lock • If we can’t, the same request is being processed by another process. Wait until it’s done
  32. Implementing Idempotency • Decide on the endpoints which need it

    • Select appropriate key cache TTLs • Document idempotent operations in your API • Allow your users to trust an operation is idempotent
  33. Implementing Idempotency • Decide on the endpoints which need it

    • Select appropriate key cache TTLs • Document idempotent operations in your API • Allow your users to trust an operation is idempotent • Decide on the endpoints which need it • Select appropriate key cache TTLs • Document idempotent operations in your API
  34. Registered As Canonical Username User ID Bigbird bigbird 123 ᴮᴵᴳᴮᴵᴿᴰ

    BIGBIRD u'\u1d2e\u1d35\u1d33\u1d2e\u1d35\u1d3f\u1d30'
  35. Registered As Canonical Username User ID Bigbird bigbird 123 ᴮᴵᴳᴮᴵᴿᴰ

    BIGBIRD u'\u1d2e\u1d35\u1d33\u1d2e\u1d35\u1d3f\u1d30' 456
  36. • getCanonicalUsername relied on nodeprep.prepare • Input string not being

    valid Unicode 3.2 meant nodeprep.prepare was no longer idempotent!
 • Fixed by double-checking username, and subsequently a library update What went wrong?
  37. • Relying on server side hashes alone to identify repeat

    requests is risky • Make the client do the work! • Cache TTL appropriate to your use case • Don’t worry about verbs that are already idempotent • But don’t forget about DELETE! • Replay behaviour - document your choice! Takeaways