Idempotent Payment
Problem
A POS terminal pays an electricity bill through our Spring service. Terminals sit on flaky 3G, so they
retry the same POST /payments whenever they do not get a response. Implement PaymentService.pay(request)
so a bill is charged at most once no matter how many copies of the request arrive — including two copies
hitting different pods at the same time. Each request carries a terminal-generated requestId, a
billRef and an amount. The first call performs the debit and stores the outcome; every later call with
the same requestId returns the same outcome without touching the biller. Reusing a requestId with a
different amount must be rejected.
Examples
Example 1 — pay({requestId: "t9-0012", billRef: "EL-4471", amount: 350}) → {status: "PAID", txnId: "TX1"}; the identical call again → {status: "PAID", txnId: "TX1"} and the biller was called
exactly once.
Example 2 — pay({requestId: "t9-0012", billRef: "EL-4471", amount: 400}) after Example 1 →
{status: "REJECTED", reason: "IDEMPOTENCY_MISMATCH"}.
Constraints
- Two identical requests may hit two pods within the same millisecond; the database is the only shared state
- The biller call can take up to 8 s; do not hold a DB lock for its duration
- Idempotency records expire after 24 h
What they look for
A unique constraint on request_id used as the lock (insert an IN_PROGRESS row first and catch the
duplicate-key exception), a state machine IN_PROGRESS → PAID / FAILED, what to return while a twin
request is still in progress (409 or wait), and how @Transactional boundaries interact with the slow
biller call. Bonus: hash the payload to detect mismatches instead of comparing fields one by one.