Building an Idempotent Payment System with Spring Boot & React

Payment systems are at the core of modern web applications, but they come with a tricky problem: duplicate transactions. Whether it’s a user accidentally clicking “Pay” twice or a network retry causing multiple API calls, duplicate charges can quickly erode trust and create operational headaches.

In this blog, I’ll walk you through a full-stack idempotency payment system built with Spring Boot on the backend and React on the frontend. This project demonstrates how idempotency prevents duplicate payments, ensures safe transactions, and creates a seamless user experience.


🎯 What is Idempotency?

Idempotency is a property of an API where making the same request multiple times produces the same effect as making it once.

In payments, this means:

  • ❌ No accidental double charges
  • ❌ Safe handling of network retries
  • ❌ Users clicking “Pay” multiple times won’t trigger duplicate payments
  • ✅ Safe, reliable payment processing

It’s a small concept with a big impact on transactional systems.


🏗️ Project Architecture

Backend (Spring Boot)

idempotency/
├── controller/     # REST API endpoints
├── service/        # Business logic
├── model/          # Data entities
├── repository/     # Database access
└── config/         # CORS configuration

Frontend (React + Vite)

frontend/
├── components/     # React components
│   ├── PaymentForm.jsx
│   └── PaymentStatus.jsx
├── apis/           # API service layer
└── App.jsx         # Main application

The separation ensures clean architecture, easy maintenance, and testability.


🚀 Features Implemented

Payment Processing

  • Idempotency Key Generation: Each payment gets a unique UUID-based key
  • Duplicate Prevention: Submitting the same key twice is blocked
  • Payment Simulation: Demo with 80% success rate
  • Status Tracking: Real-time payment status updates

Modern UI/UX

  • Auto-formatting: Card numbers (4111 1111 1111 1111)
  • Expiry Dates: Formatted as MM/YY
  • Visual Feedback: Color-coded status messages
  • Responsive Design: Works on any screen size

Backend Security

  • CORS Handling: Enables safe frontend-backend communication
  • Request Validation: Ensures headers and payloads are correct
  • Error Handling: Returns clear messages for failures and duplicates

🛠️ Technology Stack

ComponentTechnologyPurpose
BackendSpring Boot 3.xREST API & Business Logic
FrontendReact 18 + ViteUser Interface
StylingTailwind CSS v4Modern UI Design
DatabaseMySQLData Persistence
Build ToolMavenBackend Build
Package ManagernpmFrontend Dependencies

📋 API Endpoints

POST /api/payment/process

Process a payment with idempotency protection:

Headers: X-Idempotency-Key: uuid-here
Body: { amount, userId, cardNumber, ... }

Responses:

  • 200 SUCCESS – Payment processed
  • 409 ALREADY_PROCESSED – Duplicate prevented
  • 400 FAILED – Payment rejected

GET /api/payment/status/{key}

Check payment status by idempotency key.


🏃‍♂️ Running the Project

Backend

cd idempotency
mvn spring-boot:run

Runs on: http://localhost:8080

Frontend

cd frontend
npm install
npm run dev

Runs on: http://localhost:5173


🎮 How to Test Idempotency

Scenario 1: Successful Payment

  1. Generate an idempotency key
  2. Fill out the payment form
  3. Submit payment
    ✅ Result: Payment processed successfully

Scenario 2: Duplicate Prevention

  1. Use the same idempotency key
  2. Submit payment again
    ⚠️ Result: “ALREADY_PROCESSED” – No double charge!

Scenario 3: Status Check

  1. Copy idempotency key from a successful payment
  2. Enter it in the status checker
    📊 Result: View transaction details and status

🔧 Implementation Details

Backend Idempotency Logic

Optional<Idempotency> existing = service.getIdempotency(key);
if (existing.isPresent()) {
    return ResponseEntity.status(409).body(cachedResult);
}
// Process new payment

Frontend Error Handling

if (!response.ok && response.status !== 409) {
    throw new Error(result.message);
}
return result; // 409 is expected for duplicates

Auto-formatting Inputs

const formattedCard = digits.replace(/(\d{4})(?=\d)/g, '$1 ');
const formattedExpiry = digits.slice(0,2) + '/' + digits.slice(2);

🌟 Achievements

  • Built a complete idempotency system
  • Prevented duplicate payments
  • Designed professional UI/UX
  • Implemented error handling and CORS
  • Created intuitive payment flow with status tracking

🚀 Future Enhancements

  • Add authentication & user management
  • Implement unit and integration tests
  • Add analytics for payment metrics
  • Integrate real payment gateways like Stripe or PayPal
  • Add real-time notifications for payments

🧠 Key Takeaways

Idempotency is critical in distributed systems, especially in payments. It ensures data integrity, prevents double charges, and improves user trust.

This project is a great starting point for anyone looking to implement safe, reliable payment systems in Java and React.
For code checkout my GitHub: https://github.com/riteshsmalviya/idempotent-payments

Leave a comment