A timeout does not tell us whether a change was applied. A repeated request therefore must not automatically act again. Idempotency stores a stable key, request fingerprint and result. An outbox addresses another boundary: the database change and event intent are stored together before a relay later sends to Kafka. Both patterns need explicit rules for concurrency, cancellation and retry. Java Idempotency Kit and Transactional Outbox Relay demonstrate them using PostgreSQL and integration tests.
01Distinguish the two boundaries
The small simulator keeps a request key with result and fingerprint. It demonstrates replay and rejection of a changed request. It is in-memory only, not a concurrent PostgreSQL adapter. The real kit adds atomic leases and safe ownership checks.
import java.util.HashMap;
import java.util.Map;
public class Idempotency {
record Saved(String fingerprint, int result) {}
final Map<String, Saved> results = new HashMap<>();
int effects = 0;
int execute(String key, String fingerprint) {
Saved previous = results.get(key);
if (previous != null) {
if (!previous.fingerprint().equals(fingerprint)) throw new IllegalArgumentException("Key conflict");
return previous.result();
}
int result = ++effects;
results.put(key, new Saved(fingerprint, result)); return result;
}
public static void main(String[] args) {
var service = new Idempotency();
System.out.println(service.execute("command-1", "same-input"));
System.out.println(service.execute("command-1", "same-input"));
if (service.effects != 1) throw new AssertionError("Duplicate effect");
try { service.execute("command-1", "different-input"); throw new AssertionError("Conflict accepted"); }
catch (IllegalArgumentException expected) { System.out.println(expected.getMessage()); }
}
}
Run / check
java Idempotency.java02Check the reference projects
Clone the two pinned source versions into separate learning folders. ./mvnw verify checks their modules. Docker must actually be available for PostgreSQL integration. Read output and distinguish passed from skipped.
git clone https://github.com/wasiliy-strecker/java-idempotency-kit.git java-idempotency-kit-learning
cd java-idempotency-kit-learning
git checkout 24dd30d1b5c50bc33ed5fde3df7ca4af48a31b90
./mvnw verify
03Trace atomic request processing
Read PostgresIdempotencyStore. A key is acquired atomically. Another request with a different fingerprint must not be treated as matching replay. After lease expiry, an old owner must not blindly overwrite the new owner’s result.
04Commit data and outbox together
The SQL example puts order and event inside one BEGIN/COMMIT. If an insert fails, both roll back. A direct Kafka call inside an otherwise independent database workflow does not provide this guarantee. The real JdbcOutboxWriter receives the appropriate transaction context.
CREATE TABLE learning_orders(id UUID PRIMARY KEY, amount_cents BIGINT NOT NULL);
CREATE TABLE learning_outbox(id UUID PRIMARY KEY, order_id UUID NOT NULL REFERENCES learning_orders(id), payload JSONB NOT NULL, published BOOLEAN NOT NULL DEFAULT false);
BEGIN;
INSERT INTO learning_orders VALUES ('00000000-0000-0000-0000-000000000010', 12990);
INSERT INTO learning_outbox(id,order_id,payload) VALUES (
'00000000-0000-0000-0000-000000000011',
'00000000-0000-0000-0000-000000000010',
'{"type":"OrderCreated"}');
COMMIT;
SELECT o.amount_cents,e.payload FROM learning_orders o JOIN learning_outbox e ON e.order_id=o.id;
05Understand relay, retry and duplicates
The relay claims work using a lease. After successful publication it marks the job. If it dies between publication and marking, the same message can appear again. Receivers therefore need a stable event ID and an appropriate duplicate contract. At-least-once is not a global exactly-once guarantee.
06Test the decisive failure cases
Test the same request key concurrently, changed fingerprints, expired leases and lost responses. For the outbox, test database rollback, publisher failure and a crash after successful sending. A test using real PostgreSQL transactions must show which effects can occur once or again.
The simulator increments its business effect only once for the same key. The SQL example writes order and event together. Real integration tests check concurrency and lease boundaries.
- Stable request key
- Atomic lease / fingerprint
- Business change + outbox commit
- Relay / retry
- Duplicate-tolerant receiver
Your exercise
Run two threads against the in-memory simulator with the same key. Why is its HashMap not proof of safe idempotency?
Show the solution and explanation
Reading, applying the effect and writing are not one atomic operation there. Both threads may see the missing key and apply the effect. A synchronised learning block protects only one process. The real PostgreSQL kit needs an atomic database decision, fingerprint validation and ownership rules across processes.
Project context and sources
The example is simplified for learning. The linked source version shows its use in the project and the checks applied there.