A proxy call that times out after sending a write has an uncertain outcome; retries belong to the operation contract.
Spring HTTP service client retries: an interface cannot prove the server did not commit
A timeout is an observation
The client sends a stock adjustment, waits, and times out. The upstream service may have committed before the response was lost. Wrapping an @HttpExchange method in a generic retry can issue the adjustment again. That risk is identical to handwritten RestClient code; the proxy does not change HTTP semantics. The uncertain-commit boundary applies to every client style.
Separate safe reads and protected writes
A bounded retry for an idempotent GET can recover from a transient connection failure if its total delay fits the caller's deadline. A POST needs a server-enforced idempotency key or a reconciliation endpoint, and only the server can enforce one logical outcome. Keep the key stable across attempts. The receipt flow shows the database uniqueness rule behind that promise.
Test the lost-response window
Make the stub accept the first POST and drop its response. Assert the second attempt carries the same key, then test against the real service's idempotency table to prove one stored command. Also test exhausted retry budget and 4xx rejection. The snippet names the request header; it does not implement server deduplication or claim exactly-once delivery.
Implementation sketch
@HttpExchange("/v1/stock")
interface StockClient {
@PostExchange("/adjustments")
AdjustmentReceipt adjust(
@RequestHeader("Idempotency-Key") String commandId,
@RequestBody StockAdjustment adjustment);
}Cost and verification
Retries multiply downstream load and can extend tail latency. An idempotency table adds a write and retention policy, but prevents a duplicate business mutation after a lost response.
Common Mistakes
- Do not infer rollback from a client timeout.
- Do not generate a new idempotency key for each attempt.
- Do not apply the same retry rule to reads and non-idempotent commands.
Read next
Spring HTTP service client: the interface is a contract, not a transport policy, Spring HTTP timeout after POST: the write may already exist, Spring HTTP retries: only replay a command with a defined identity, Spring POST idempotency keys: replay the same receipt result.
