Skip to content

NullPoint3rDev/idempotent-rest-api

Repository files navigation

Idempotent REST API

Java Spring Boot Redis Gradle Docker JUnit 5 Testcontainers License: MIT

Production-style idempotent payments API — clients send Idempotency-Key; duplicate requests return the cached response. Redis-backed store, request-body hash conflict detection, and one-command run with Docker Compose.

FeaturesArchitectureQuick StartAPITech Stack


✨ Features

Feature Description
Idempotency keys Client sends Idempotency-Key header; first request executes and caches the response; repeats return the same response without re-executing.
Redis-backed store Cached responses stored in Redis with configurable TTL (default 24h); survives restarts.
Request-body consistency Same key with a different request body returns 409 Conflict to prevent accidental misuse.
RESTful payments API POST /api/payments with customerId, currency, sum; returns payment with generated paymentId and timestamps.
Bean Validation Request DTOs validated with Jakarta Validation; invalid payloads yield 400.
Docker Compose Single command runs Redis and the app with health-based startup order.
Tests Unit tests for service and idempotency executor; integration tests with Testcontainers (optional: RUN_REDIS_IT=true).
Actuator Health and info endpoints for monitoring.

🏗 Architecture

flowchart LR
    subgraph Client
        C[HTTP Client]
    end
    subgraph "Spring Boot :8080"
        REST[REST API]
        REST --> Exec[IdempotentPaymentExecutor]
        Exec --> Store[(IdempotencyStore)]
        Exec --> Svc[PaymentService]
    end
    subgraph "Redis"
        R[(Cache by key)]
    end
    C -->|"POST + Idempotency-Key"| REST
    Store --> R
Loading

Flow: Client sends POST /api/payments with header Idempotency-Key: <key>. Executor checks Redis for that key. If found and request body hash matches → return cached response (same status and body). If found but body differs → 409 Conflict. If not found → call PaymentService, cache response in Redis with TTL, return 201 and the payment.


🚀 Quick Start

With Docker Compose (recommended)

git clone https://github.com/NullPoint3rDev/idempotent-rest-api.git
cd idempotent-rest-api
docker compose up -d --build

Wait for services to be healthy (~20–40 s). API: http://localhost:8080

# Create payment (first time)
curl -s -X POST http://localhost:8080/api/payments \
  -H "Idempotency-Key: key-1" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"alice","currency":"USD","sum":100}'

# Repeat same request → same response (idempotent)
curl -s -X POST http://localhost:8080/api/payments \
  -H "Idempotency-Key: key-1" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"alice","currency":"USD","sum":100}'

# Same key, different body → 409 Conflict
curl -s -w "\n%{http_code}" -X POST http://localhost:8080/api/payments \
  -H "Idempotency-Key: key-1" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"bob","currency":"EUR","sum":50}'

Local (JDK 21 + Gradle)

  1. Start Redis: docker run -d -p 6379:6379 redis:7-alpine (or use Redis installed locally).
  2. Run: ./gradlew bootRun. Default config uses localhost:6379.

📡 API

Method Path Description
POST /api/payments Create a payment. Required header: Idempotency-Key. Body: {"customerId": string, "currency": string, "sum": number}. Returns 201 and the created payment. Missing/invalid key → 400. Same key + different body → 409.
GET /actuator/health Health check (UP/DOWN).
GET /actuator/info Application info (if configured).

Example response (201):

{
  "paymentId": "f01822ac-4814-4d02-96cc-ebbfe0595845",
  "customerId": "alice",
  "currency": "USD",
  "sum": 100,
  "createdAt": "2026-02-27T13:10:16.991264170Z",
  "updatedAt": "2026-02-27T13:10:16.991264170Z"
}

📦 Tech Stack

Layer Technology
Language Java 21
Framework Spring Boot 3.2 (Web, Data Redis, Actuator, Validation)
Cache / store Redis 7 (Spring Data Redis, Lettuce)
Serialization Jackson (JSON); request-hash via ObjectMapper.valueToTree().hashCode()
Build Gradle 8 (Kotlin DSL)
Tests JUnit 5, Mockito, MockMvc, AssertJ; Testcontainers for Redis (optional)
Run Docker, Docker Compose

📁 Project Structure

idempotent-rest-api/
├── src/main/java/.../idempotent/
│   ├── IdempotentRestApiApplication.java
│   ├── controller/     # PaymentController (header validation, delegate to executor)
│   ├── component/     # IdempotentPaymentExecutor, RedisIdempotencyStore
│   ├── service/       # PaymentService (create payment)
│   ├── repository/    # IdempotencyStore interface
│   ├── dto/           # RequestDTO, ResponseDTO, CachedResponse
│   └── model/         # Payment
├── src/test/.../      # PaymentServiceTest, IdempotentPaymentExecutorTest, PaymentControllerIntegrationTest
├── docker-compose.yml
├── Dockerfile
└── README.md

🧪 Tests

./gradlew test
  • PaymentServiceTest — unit tests: response has generated id and timestamps; each call gets a different payment id.
  • IdempotentPaymentExecutorTest — unit tests with mocks: cache miss → service + put; cache hit same body → return cached; cache hit different body → 409.
  • PaymentControllerIntegrationTest — full stack with Testcontainers Redis. Skipped by default (requires Docker). Run with: RUN_REDIS_IT=true ./gradlew test.

📄 License

MIT — see LICENSE for details.


Star the repo if you find it useful.

About

Idempotent REST API with Redis — Idempotency-Key header, duplicate request handling, Spring Boot 21 & Docker Compose

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors