Great — this is the right moment to learn idempotency, because it is one of the pillars of “code that doesn’t break”.
I’ll explain it in three layers:
- Theory (mental model)
- Database-level design (most important)
- Spring Boot practical patterns
No shortcuts, no magic — just solid engineering.
1️⃣ What Does “Idempotent” Mean (Properly)?
An operation is idempotent if executing it once or many times results in the same final state.
Example:
Action Result Call once Payment succeeds Call twice Still only one payment Call 10 times Still only one payment Idempotency is not optional for:
- Payments
- Orders
- Account updates
- Message consumers
2️⃣ The Core Rule (MEMORIZE THIS)
Idempotency must be enforced at the DATABASE level.
Application-only checks are never enough.
Why?
- Multiple instances
- Restarts
- Concurrency
- Crashes
Only DB sees truth.
3️⃣ Pattern #1 — Idempotency Key (Most Common)
Step 1: Client generates a unique key
Idempotency-Key: 9f2a-88b3-...
Step 2: DB enforces uniqueness
CREATE TABLE processed_requests (
request_id VARCHAR(50) PRIMARY KEY,
created_at TIMESTAMP
);
Step 3: Service logic
@Transactional
public void processPayment(String requestId) {
if (requestRepository.existsById(requestId)) {
return; // already processed
}
requestRepository.save(new ProcessedRequest(requestId));
// safe business logic
debit();
ledger();
}
Why this works
- Duplicate request → DB prevents double execution
- Safe under concurrency
- Works across instances
4️⃣ Pattern #2 — Unique Business Constraint
Used when business naturally defines uniqueness.
Example: One payment per order
UNIQUE (order_id)
@Transactional
public void pay(Long orderId) {
paymentRepository.save(new Payment(orderId));
}
Second call → DB rejects
5️⃣ Pattern #3 — State Machine (Advanced & Safe)
Only allow valid state transitions.
Example
CREATED → PAID → SETTLED
UPDATE orders
SET status = 'PAID'
WHERE id = :id AND status = 'CREATED';
If update count = 0 → already processed.
6️⃣ Pattern #4 — Message Consumer Idempotency
DB Table
processed_events (
event_id VARCHAR PRIMARY KEY
)
Consumer Logic
@Transactional
public void consume(Event event) {
if (eventRepo.exists(event.getId()))
return;
eventRepo.save(event.getId());
process(event);
}
Kafka/RabbitMQ will redeliver — your system stays safe.
7️⃣ What NOT to Do ❌
| Wrong Approach | Why it fails |
|---|---|
| In-memory flags | Fails on restart |
| Redis-only | Data loss risk |
| Synchronized | Not distributed |
| UI prevention | Backend retries ignore it |
8️⃣ Idempotency + Transactions (Critical)
Correct order:
- Insert idempotency record
- Perform business logic
- Commit transaction
If transaction fails → idempotency record rolls back.
9️⃣ Visual Flow
Request arrives
↓
Check idempotency key
↓
Already processed? → EXIT
↓
Execute logic
↓
Commit
? The One-Line Rule
If an operation changes state, it must be idempotent.
? Mini Exercise (Very Important)
Take one method you wrote and answer:
- What identifies this request uniquely?
- Where will I store that uniqueness?
- What happens on retry?