Ein Timeout sagt nicht, ob eine Änderung ausgeführt wurde. Ein wiederholter Request darf deshalb nicht automatisch erneut wirken. Idempotenz hält einen stabilen Schlüssel mit Request-Fingerprint und Ergebnis. Eine Outbox löst eine andere Grenze: Datenbankänderung und Ereignisauftrag werden gemeinsam gespeichert, bevor ein Relay später an Kafka sendet. Beide Muster benötigen explizite Regeln für Parallelität, Abbruch und Wiederholung. Java Idempotency Kit und Transactional Outbox Relay zeigen diese Regeln mit PostgreSQL und Integrationstests.
01Die zwei Grenzen auseinanderhalten
Der kleine Simulator hält denselben Request-Key mit Ergebnis und Fingerprint. Er zeigt Wiederholung und Ablehnung eines geänderten Requests. Er ist nur im Speicher und kein konkurrierender PostgreSQL-Adapter. Das echte Kit ergänzt atomare Leases und sichere Besitzerprüfung.
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()); }
}
}
Starten / prüfen
java Idempotency.java02Die Referenzprojekte prüfen
Klone die zwei festen Codeversionen in getrennte Lernordner. ./mvnw verify prüft die Module. Für die PostgreSQL-Integration muss Docker tatsächlich verfügbar sein. Lies die Testausgabe und unterscheide bestanden von übersprungen.
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
03Atomare Request-Verarbeitung verfolgen
Lies PostgresIdempotencyStore. Ein Key wird atomar erworben. Ein weiterer Request mit anderem Fingerprint darf den ersten nicht als passende Wiederholung behandeln. Nach Ende einer Lease darf ein alter Besitzer nicht ungeprüft das Ergebnis eines neuen Besitzers überschreiben.
04Daten und Outbox zusammen committen
Im SQL-Lernbeispiel stehen Order und Ereignis im selben BEGIN/COMMIT. Scheitert ein INSERT, werden beide zurückgerollt. Ein direkter Kafka-Aufruf innerhalb eines ansonsten unabhängigen DB-Ablaufs bietet diese Garantie nicht. Der echte JdbcOutboxWriter erhält den passenden Transaktionskontext.
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;
05Relay, Retry und Duplikate verstehen
Das Relay beansprucht Arbeit mit einer Lease. Nach erfolgreicher Veröffentlichung markiert es den Auftrag. Stirbt es zwischen Veröffentlichung und Markierung, kann dieselbe Nachricht erneut erscheinen. Empfänger brauchen deshalb eine stabile Ereignis-ID und einen passenden Duplikatvertrag. At-least-once ist keine globale Exactly-once-Garantie.
06Die entscheidenden Fehlerfälle testen
Prüfe denselben Request-Key gleichzeitig, geänderten Fingerprint, abgelaufene Lease und verlorene Antwort. Für die Outbox prüfe DB-Rollback, Publisherfehler und Absturz nach erfolgreichem Senden. Ein Test mit echten PostgreSQL-Transaktionen muss zeigen, welche Wirkung tatsächlich einmal oder erneut auftreten kann.
Der Simulator erhöht seine fachliche Wirkung bei gleichem Key nur einmal. Das SQL-Beispiel schreibt Auftrag und Ereignis gemeinsam. Die echten Integrationstests prüfen Parallelitäts- und Leasegrenzen.
- stabiler Request-Key
- atomare Lease / Fingerprint
- Fachänderung + Outbox-Commit
- Relay / Retry
- duplikattoleranter Empfänger
Deine Übung
Starte zwei Threads gegen den Memory-Simulator mit demselben Key. Warum ist seine HashMap kein Beweis für sichere Idempotenz?
Lösung mit Erklärung anzeigen
Lesen, Wirkung und Schreiben sind dort keine atomare gemeinsame Operation. Beide Threads können den fehlenden Key sehen und die Wirkung ausführen. Ein synchronisierter Lernblock schützt nur diesen einen Prozess. Das echte PostgreSQL-Kit benötigt eine atomare Datenbankentscheidung, Fingerprintprüfung und Besitzerregeln über mehrere Prozesse hinweg.
Projektbezug und Quellen
Das Beispiel ist zum Lernen vereinfacht. Die verlinkte Codeversion zeigt die Anwendung im Projekt und die dortigen Prüfungen.