Chaturmind
LearnDSASystem DesignDevOpsEngineering GrowthBlog
Start learning
Chaturmind

Structured learning paths for engineers who want to go deep. Written by practitioners.

Learn

  • Java
  • DSA
  • System Design
  • Spring Boot
  • AI / ML
  • DevOps
  • Engineering Growth

Company

  • Blog
  • Contact

Legal

  • Privacy Policy
  • Terms of Service

© 2026 Chaturmind. All rights reserved.

Built for engineers who want to go deep.


← Spring Boot REST API Development

Spring Boot Basics

  • What is Spring Boot?
  • Dependency Injection
  • Building REST Controllers

Validation & Error Handling

  • Bean Validation with @Valid
  • Global Exception Handling

Spring Framework Internals

  • IoC Container Fundamentals
  • Bean Lifecycle In Detail
  • Component Scanning & Configuration
  • Auto-Configuration Mechanism
  • Spring AOP
  • @Transactional Deep Dive

Microservices Architecture

  • Monolith to Microservices Decomposition
  • API Gateway
  • Service Discovery
  • Inter-Service Communication Choices
  • Event-Driven Architecture Patterns
  • Messaging Technology Choices

Resilience Patterns

  • Configuration Management
  • Circuit Breaker Pattern
  • Retry & Backoff Strategies
  • Bulkhead & Rate Limiting
  • Timeout Strategy
  • Why Microservices Fail

Distributed Data & Consistency Patterns

  • Two-Phase Commit
  • Saga Pattern
  • Outbox Pattern
  • Eventual Consistency Design
  • CQRS Basics
  • CAP Theorem

Observability & Operations

  • Centralized Logging
  • Distributed Tracing
  • Metrics & Monitoring
  • Alerting Strategy
  • Health Checks

Payment Systems

  • Payment — Requirements
  • Payment — Core Flow
  • Payment — Idempotency Implementation
  • Payment — Failure Handling & Reconciliation
  • Payment — Security

Order Management System

  • OMS — Requirements
  • OMS — State Machine Design
Chaturmind
← Spring Boot REST API Development

Spring Boot Basics

  • What is Spring Boot?
  • Dependency Injection
  • Building REST Controllers

Validation & Error Handling

  • Bean Validation with @Valid
  • Global Exception Handling

Spring Framework Internals

  • IoC Container Fundamentals
  • Bean Lifecycle In Detail
  • Component Scanning & Configuration
  • Auto-Configuration Mechanism
  • Spring AOP
  • @Transactional Deep Dive

Microservices Architecture

  • Monolith to Microservices Decomposition
  • API Gateway
  • Service Discovery
  • Inter-Service Communication Choices
  • Event-Driven Architecture Patterns
  • Messaging Technology Choices

Resilience Patterns

  • Configuration Management
  • Circuit Breaker Pattern
  • Retry & Backoff Strategies
  • Bulkhead & Rate Limiting
  • Timeout Strategy
  • Why Microservices Fail

Distributed Data & Consistency Patterns

  • Two-Phase Commit
  • Saga Pattern
  • Outbox Pattern
  • Eventual Consistency Design
  • CQRS Basics
  • CAP Theorem

Observability & Operations

  • Centralized Logging
  • Distributed Tracing
  • Metrics & Monitoring
  • Alerting Strategy
  • Health Checks

Payment Systems

  • Payment — Requirements
  • Payment — Core Flow
  • Payment — Idempotency Implementation
  • Payment — Failure Handling & Reconciliation
  • Payment — Security

Order Management System

  • OMS — Requirements
  • OMS — State Machine Design
HomeLearnSpring BootSpring Boot REST API DevelopmentPayment Systems
✓ FreeAdvanced· 8 min read

Payment — Idempotency Implementation

A concrete Spring Boot implementation of idempotency keys for payment endpoints — the database-level unique constraint that makes it actually safe, the race condition a naive check-then-insert has, and how to handle a retry that arrives while the original is still in flight.

Published September 23, 2026


Payment — Idempotency Implementation

API Contract Design introduced idempotency keys generally; this lesson implements the mechanism specifically for a payment endpoint, where getting it wrong means a real duplicate charge.

The naive (broken) implementation

// BROKEN — race condition between check and insert
public Payment charge(String idempotencyKey, PaymentRequest request) {
    Payment existing = repository.findByIdempotencyKey(idempotencyKey);
    if (existing != null) return existing;          // step A: check
    Payment payment = processCharge(request);         // step B: process
    payment.setIdempotencyKey(idempotencyKey);
    return repository.save(payment);                  // step C: insert
}

If two requests with the same idempotency key arrive concurrently (a genuine, common scenario — a client times out and retries while the original request is still processing), both can pass step A's check simultaneously (neither has been saved yet), and both proceed to charge the card — the exact duplicate-charge bug idempotency exists to prevent. The bug isn't in the idea of checking first; it's that check-then-insert isn't atomic.

The correct implementation: a database-level unique constraint

@Document(collection = "payments")
@CompoundIndex(name = "idempotency_key_unique", def = "{'idempotencyKey': 1}", unique = true)
class Payment { /* ... */ }

public Payment charge(String idempotencyKey, PaymentRequest request) {
    try {
        Payment placeholder = new Payment();
        placeholder.setIdempotencyKey(idempotencyKey);
        placeholder.setStatus(PaymentStatus.PROCESSING);
        repository.insert(placeholder);   // atomic — the unique index rejects a duplicate key HERE
    } catch (DuplicateKeyException e) {
        // another request already owns this idempotency key — poll/return its current state
        return waitForResultOrReturnCurrentState(idempotencyKey);
    }
    // this request WON the race — it's the only one that proceeds to actually charge
    Payment result = processCharge(request);
    result.setIdempotencyKey(idempotencyKey);
    repository.save(result);
    return result;
}

The fix is moving the uniqueness guarantee to the database itself (a unique index on idempotencyKey), not application-level logic — the database's own atomicity guarantee is what actually closes the race window that check-then-insert leaves open. Whichever concurrent request's insert succeeds "wins" and proceeds to charge; every other concurrent request with the same key gets a DuplicateKeyException and must NOT charge again.

Handling a retry that arrives while the original is still in-flight

The losing request (the one that got DuplicateKeyException) has a genuine problem: the winning request might still be mid-flight (waiting on the payment processor's response), so there's no final result to return yet. Two reasonable approaches:

  • Poll briefly: check the placeholder's status every short interval (with a timeout) until it moves from PROCESSING to a final state, then return that result.
  • Return 409/202 with a status-check reference: tell the client the request is already being processed and give them a way to poll for the result themselves, rather than blocking the losing request's connection indefinitely.

Either is defensible; what's NOT acceptable is silently proceeding to charge again just because the first attempt's result wasn't immediately available — that reintroduces the exact bug idempotency was implemented to prevent.

Idempotency key generation and retention

The idempotency key must be client-generated (not server-generated) — if the server generated it, a client retry after a lost response would get a NEW key from the server and the duplicate-prevention would never trigger at all. The client generates one key per logical charge attempt (typically a UUID) and reuses that exact same key on any retry of that same attempt. As covered in API Contract Design, keys are retained for a bounded window (matching realistic retry timeframes) rather than forever.

Follow-up questions this topic invites — and their answers

Q: Why insert a PROCESSING placeholder before calling the payment processor, rather than after? A: Inserting first is what claims the idempotency key atomically before any external call happens — if the charge were attempted first and only saved afterward, two concurrent requests could both pass an early check, both call the processor, and both succeed at the processor level before either save happens, defeating the entire purpose.

Q: What if the process crashes after successfully charging the processor but before updating the Payment record from PROCESSING to CAPTURED? A: This is precisely the scenario Payment — Failure Handling & Reconciliation exists to catch — the local record is stuck in an ambiguous PROCESSING state while the processor's own records show a completed charge; reconciliation (comparing local records against the processor's transaction log) is what detects and resolves this specific gap.

Q: Does every payment-related endpoint need an idempotency key, or just the charge endpoint? A: Any endpoint with a real external side effect that a client might retry — charge, refund, and capture all qualify; a pure read endpoint (checking payment status) doesn't need one, since reads are naturally idempotent and retrying them causes no harm.

Q: Could the database-level unique constraint approach have performance implications at very high payment volume? A: A unique index lookup/insert is a fast, well-optimized database operation and isn't a meaningful bottleneck at realistic payment volumes — the far bigger latency cost in this flow is the synchronous call to the external payment processor itself, not the local idempotency-key check.

Previous

Payment — Core Flow

Next

Payment — Failure Handling & Reconciliation

AI Tutor

Lesson: Payment — Idempotency Implementation

Quick actions

AI responses can be inaccurate. Verify critical information.