Concurrent Duplicate Requests
Concurrent duplicate requests occur when multiple requests with the same logical key arrive before the first operation finishes. API idempotency requires coordinating those in-flight requests so they do not independently execute the same non-idempotent operation.
An idempotency key identifies requests that represent the same logical operation, such as creating a payment or submitting an order. If two requests with the same key arrive at nearly the same time, simply checking a database after the operation is too late: both requests can observe that no result exists and both can perform the side effect.
In-flight deduplication coordinates requests while the first operation is still running. A common design keeps a concurrent map from idempotency key to an in-progress `CompletableFuture`. The first request creates and stores the future; later requests find the existing future and wait for or reuse its result instead of starting another operation.
The coordination must be atomic. In Java, `ConcurrentHashMap.computeIfAbsent` can establish one shared entry for a key, although the supplied computation should be designed carefully because it may execute as part of the map update. The shared future should represent the complete operation, including the externally visible result needed by callers.
In-flight deduplication is different from durable idempotency. An in-memory map usually coordinates requests only within one application instance and only while its entry exists. A durable idempotency record, commonly stored in a database or distributed store, can survive process restarts and coordinate requests across multiple instances.
A production implementation also needs lifecycle management: completed entries must eventually be removed or retained according to the desired replay semantics, failures need defined behavior, and keys should have an appropriate scope and expiration policy.
Key points
- Use an idempotency key to identify the same logical operation.
- Coordinate concurrent requests before executing the non-idempotent side effect.
- Store an in-flight future so duplicate callers can share the first operation's result.
- Use atomic concurrent-map operations to avoid check-then-act races.
- In-memory coalescing is local and temporary; durable idempotency can coordinate across instances and restarts.
Interview questions
Why can two concurrent requests with the same idempotency key still execute the operation twice?
A simple existence check is not atomic with the subsequent side effect. Both requests can check for a missing key before either request records a result. They can therefore both proceed into the non-idempotent operation. The solution is to atomically establish ownership or an in-flight operation before executing the side effect.
How would you implement in-flight request coalescing in Java?
I would use a `ConcurrentHashMap` keyed by the idempotency key, with a `CompletableFuture` representing the shared operation. I would use `computeIfAbsent` so concurrent callers obtain the same future for a key. The first operation performs the side effect, while later callers wait on the existing future. I would also define cleanup and failure semantics explicitly.
What is the difference between request coalescing and durable API idempotency?
Request coalescing prevents simultaneous callers from independently starting the same operation while it is in progress. Durable API idempotency records the logical request and its result in persistent or shared storage so later retries can be recognized even after the original request completes. Coalescing is therefore mainly an in-flight optimization or coordination mechanism. A robust API may use both when duplicate retries and distributed deployment matter.
What failure cases must a production implementation consider?
The design must define what happens when the operation fails, times out, is cancelled, or the process crashes. A particularly important case is a crash after an external side effect succeeds but before the idempotency result is persisted. In-memory state cannot resolve that ambiguity after a restart. For distributed systems, I would use durable idempotency state and carefully define ownership, expiration, result replay, and recovery semantics.
Why is `ConcurrentHashMap.computeIfAbsent` useful for this problem, and what should you be careful about?
It combines lookup and insertion into an atomic map operation, which prevents separate callers from independently installing different values for the same key. That makes it useful for establishing one shared in-flight future. However, the mapping function should remain simple and should not perform long-running blocking work while computing the map entry. The actual operation can be represented by the future and executed asynchronously.