A caller can time out after a remote service commits a write but before its response reaches the caller.
Spring HTTP timeout after POST: the write may already exist
The checked ordering
The loopback /remote-reservations handler records an Idempotency-Key and increments a write counter before it waits on a latch. The RestClient POST uses an 80ms read timeout, so the first call throws ResourceAccessException while the server still records one write. The test waits for a server-side commit signal before asserting the counter. That is the important order.
The first response may fail when the server later writes to a socket the client closed. The client exception cannot tell whether the reservation exists. The next fixture sends the same key again and checks the server-side deduplication rule.
Carry uncertainty into the API contract
A public receipt endpoint should not translate this transport failure into a claim that the remote write rolled back. It may return a retryable error with a stable command identifier, or reconcile the remote state before reporting a final decision. The exact response policy depends on the upstream contract.
The fixture is not a payment provider and has no database. Its in-memory key set intentionally models one server process. A real integration needs durable deduplication, retention rules, tenant scoping and behavior under simultaneous requests or restarts. The local H2 command fixture covers a different, transactional replay boundary.
Checked source
String key = exchange.getRequestHeaders().getFirst("Idempotency-Key");
if (key != null && committedKeys.add(key)) {
writes.incrementAndGet();
firstWriteCommitted.countDown();
}Verification boundary
RestClientBoundaryTest.aTimedOutWriteMayHaveCommittedBeforeTheCallerRetries.
Costs and limits
The remote state is an in-memory set in one loopback process. No durable upstream, concurrent duplicate race or network partition is checked.
Common Mistakes
- Do not infer rollback from a client-side timeout.
- Do not generate a new command key for the same uncertain write.
- Do not treat an in-memory set as durable remote deduplication.
Read next
Spring HTTP replay key: two requests, one modeled reservation, Spring HTTP retries: only replay a command with a defined identity, Spring POST idempotency keys: bind replay to tenant and command, Spring transactional outbox: commit a receipt and event row together.
