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.
Features • Architecture • Quick Start • API • Tech Stack
| 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. |
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
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.
git clone https://github.com/NullPoint3rDev/idempotent-rest-api.git
cd idempotent-rest-api
docker compose up -d --buildWait 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}'- Start Redis:
docker run -d -p 6379:6379 redis:7-alpine(or use Redis installed locally). - Run:
./gradlew bootRun. Default config useslocalhost:6379.
| 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"
}| 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 |
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
./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.
MIT — see LICENSE for details.
Star the repo if you find it useful.