Hook: Interviewers love this word because it separates “works once” code from “survives retries in production” code.
Question: What is idempotency in Spring Boot and microservices?
Answer: Idempotency means that if the same request is sent more than once, the final result is the same as if it was sent only once. This matters a lot in microservices because networks fail, clients retry, and load balancers can resend requests. In Spring Boot, you usually implement it by using a stable business key or an Idempotency-Key header and then storing the first successful result so later duplicates return the same response.
Interview-Ready Answer: “Idempotency means repeated requests do not cause repeated side effects. In Spring Boot microservices, I usually handle it with an Idempotency-Key header plus server-side storage, so if a payment request is retried after a timeout, I return the original response instead of charging the customer twice. That is important because retries are normal in distributed systems, and idempotency makes them safe.”
Detailed Explanation: A simple way to think about idempotency is: “same input, same outcome, no extra damage.” The tricky part is that this is about the effect on the system, not just the HTTP response. A request can return the same JSON twice and still be non-idempotent if it created two database rows behind the scenes. That is why interviewers care about business effects, not just status codes.
Idempotency-Key header or a natural business key like orderId.409 Conflict, because reusing the key for a different action is almost always a client bug.You use idempotency for operations where duplicate execution is dangerous: payments, order creation, coupon redemption, ticket booking, and message consumption. It is less critical for safe reads like GET requests, because reads should not change state anyway. In microservices, idempotency is a safety net for unstable networks, client timeouts, and “at least once” delivery, which means a message may be delivered more than one time.
| Approach | What it guarantees | Best for | Gotcha |
|---|---|---|---|
| Idempotent request | Same effect on repeats | Retries | Must store key/result |
| Non-idempotent request | No duplicate safety | Simple creates | Can double charge |
| Dedup by DB constraint | Prevents duplicate rows | Natural unique ids | May still need response replay |
| Transactional outbox | Reliable event publish | Messaging workflows | Solves a different problem |
In Spring Boot, the simplest demo is an in-memory ConcurrentHashMap. That is good for learning, but not enough for production because it is lost on restart and does not work across multiple instances. In production, store the idempotency record in Redis, a database table, or another shared store. A common TTL, or time-to-live, is 24 hours for payments, though real systems may use 1 hour, 7 days, or longer depending on business rules and compliance.
With a hash map or Redis lookup, the check is about O(1) average time. The side effect itself is usually the expensive part: a database write can be 5 to 20 ms, and a remote payment call can take 100 to 800 ms or more. Space is proportional to the number of active keys, so if you keep 1 million keys and each record is roughly 300 bytes, that is about 300 MB before storage overhead. In a busy system, you also need cleanup so the store does not grow forever.
409 Conflict or 422 Unprocessable Entity.Memory Hook: “An idempotency key is like a concert wristband: once you have checked in, showing the same wristband again gets you the same result, but it should never buy a second ticket.”
Real-World Example: Imagine a checkout service in a Spring Boot e-commerce system. The mobile app submits POST /payments with an Idempotency-Key because the user is on flaky Wi‑Fi. The first request charges the card and stores the result; a second retry after a timeout returns the same payment response and does not charge again.
What goes wrong when this is misunderstood? A team treats “same HTTP method” as enough and makes the payment endpoint non-idempotent. During a network hiccup, the client retries twice, the logs show two “payment created” lines with different transaction ids, and the customer sees two card charges. Support tickets spike, the monitoring dashboard shows duplicate orders, and engineers spend hours reversing transactions instead of shipping features.
A subtle symptom is that everything may look fine in API logs because both responses are 200 OK. The real clue is in business data: duplicated rows, repeated Kafka events, or twice-executed downstream calls. In production, the safest mental check is: “If this request is replayed by accident, can the user pay twice, book twice, or receive twice?”
package com.example.idempotency;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.time.Instant;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@SpringBootApplication
public class IdempotencyApplication {
public static void main(String[] args) {
SpringApplication.run(IdempotencyApplication.class, args);
}
@RestController
@RequestMapping("/api")
static class PaymentController {
private final IdempotencyService service = new IdempotencyService();
@PostMapping("/payments")
public ResponseEntity<PaymentResponse> createPayment(
@RequestHeader(value = "Idempotency-Key", required = false) String idempotencyKey,
@RequestBody PaymentRequest request) {
if (idempotencyKey == null || idempotencyKey.isBlank()) {
// Without a stable key, the server cannot safely deduplicate retries.
return ResponseEntity.badRequest().body(null);
}
IdempotencyResult result = service.process(idempotencyKey, request);
return ResponseEntity.status(result.status()).body(result.response());
}
@GetMapping("/payments/{idempotencyKey}")
public ResponseEntity<PaymentResponse> getByKey(@PathVariable String idempotencyKey) {
PaymentResponse response = service.find(idempotencyKey);
if (response == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
}
return ResponseEntity.ok(response);
}
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<Map<String, String>> handleBadRequest(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(Map.of("error", ex.getMessage()));
}
}
static class IdempotencyService {
private final ConcurrentHashMap<String, StoredRecord> store = new ConcurrentHashMap<>();
public IdempotencyResult process(String key, PaymentRequest request) {
String fingerprint = fingerprint(request);
StoredRecord newRecord = new StoredRecord(fingerprint,
new PaymentResponse(UUID.randomUUID().toString(), request.orderId(), request.amount(), "APPROVED", Instant.now()));
StoredRecord existing = store.putIfAbsent(key, newRecord);
if (existing == null) {
// First time: do the side effect once and remember the exact response.
return new IdempotencyResult(HttpStatus.CREATED, newRecord.response);
}
// Same key but different payload: that is a client bug, not a retry.
if (!Objects.equals(existing.fingerprint, fingerprint)) {
throw new IllegalArgumentException("Idempotency-Key reused with a different request body");
}
// Safe retry: return the original response so the user is not charged twice.
return new IdempotencyResult(HttpStatus.OK, existing.response);
}
public PaymentResponse find(String key) {
StoredRecord record = store.get(key);
return record == null ? null : record.response;
}
private String fingerprint(PaymentRequest request) {
return request.orderId() + "|" + request.amount();
}
}
record StoredRecord(String fingerprint, PaymentResponse response) { }
record PaymentRequest(String orderId, BigDecimal amount) {
PaymentRequest {
if (orderId == null || orderId.isBlank()) {
throw new IllegalArgumentException("orderId is required");
}
if (amount == null || amount.signum() <= 0) {
throw new IllegalArgumentException("amount must be greater than zero");
}
}
}
record PaymentResponse(String paymentId, String orderId, BigDecimal amount, String status, Instant createdAt) { }
record IdempotencyResult(HttpStatus status, PaymentResponse response) { }
}
Follow-up & Tricky Questions:
Idempotency-Key, save the first successful response, and make inserts atomic so only one request wins.409 Conflict, because the key is supposed to identify one logical action, not multiple different actions.SETNX-style insert, or a transaction with a uniqueness guarantee so only one node stores the first record.Common Mistakes:
Idempotency-Key for the same logical action.Memory Hook: “Same key, same outcome.” Think of idempotency like a stamped ticket: showing it again should not create a second entry.
Cheat Sheet:
Idempotency-Key + shared storage + atomic insert.Practice Tasks: