Alle zugehörigen Inhalte befinden sich auf dieser einen großen Seite. Kapitel und Beispiele sind standardmäßig geschlossen und lassen sich gezielt öffnen.
0 von 330 offen
30 Hauptkapitel · 300 enthaltene Lernbereiche · keine Navigation durch Einzeldateien nötig.
Qualitätsstatus:300 von 300 Refactorings ausführlich vertieft. Nur Hauptkapitel sind klappbar; alle Unterabschnitte bleiben sichtbar.
Kapitel 1 Methoden extrahieren und Abläufe zerlegen10 Refactorings
Zehn grundlegende Refactorings werden als sichere Transformationen gezeigt: vom Code Smell über kleine Schritte bis zum kompilierbaren Java-21-Zielcode und Test.
1. Extract Method
Fachliche Einordnung
Eine lange Methode vermischt Berechnung, Validierung und Ausgabe. Einzelne Abschnitte besitzen zwar eine erkennbare Aufgabe, sind aber nicht benannt.
Ein zusammenhängender Codeabschnitt wird in eine Methode mit fachlich aussagekräftigem Namen verschoben. Der Aufrufer beschreibt danach den Ablauf, die extrahierte Methode erklärt das Detail.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Zuerst durch Tests das beobachtbare Verhalten sichern
Dann Ein- und Ausgaben des Fragments bestimmen, lokale Variablen als Parameter oder Rückgabewert behandeln, extrahieren, kompilieren und nach jedem kleinen Schritt testen.
Wann sinnvoll?
Sinnvoll bei wechselnden Abstraktionsebenen, wiederholten Kommentaren oder schwer isolierbaren Teilberechnungen. Nicht sinnvoll, wenn nur eine triviale Zeile ohne eigenen Begriff verschoben wird.
Risiken und Fehlanwendungen
Ein häufiger Fehler ist, mechanisch sehr kleine Methoden ohne fachlichen Namen zu erzeugen. Ebenso problematisch sind lange Parameterlisten; dann ist oft Introduce Parameter Object der nächste Schritt.
Enterprise-Java-Beispiel
In einem InvoiceService wird die Prüfung der Rabattberechtigung aus calculateInvoice() als determineDiscount(...) extrahiert. Dadurch kann die Regel separat getestet und später durch eine Policy ersetzt werden.
Vorhandener Java-21-Zielcode
InvoiceTotalCalculator.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
import java.util.List;
// Refactoring: Extract Method
// Zweck: Rabattberechnung benennen und die Hauptmethode auf einer Abstraktionsebene halten.
public final class InvoiceTotalCalculator {
public BigDecimal total(List<BigDecimal> positions, boolean premium) {
BigDecimal subtotal = positions.stream().reduce(BigDecimal.ZERO, BigDecimal::add);
return subtotal.subtract(discountFor(subtotal, premium));
}
private BigDecimal discountFor(BigDecimal subtotal, boolean premium) {
return premium ? subtotal.multiply(new BigDecimal("0.10")) : BigDecimal.ZERO;
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
2. Inline Method
Fachliche Einordnung
Eine Methode delegiert nur, besitzt keinen eigenständigen Begriff oder versteckt den tatsächlichen Ablauf hinter unnötiger Indirektion.
Der Methodenrumpf wird an den Aufrufstellen eingesetzt und die bedeutungslose Methode entfernt. Das reduziert Navigation und macht eine spätere, sinnvollere Neuaufteilung möglich.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Alle Aufrufer ermitteln, Verhalten absichern, Methodenrumpf einsetzen, Namenskonflikte bereinigen, kompilieren und erst danach die alte Methode entfernen.
Wann sinnvoll?
Sinnvoll bei reinem Durchreichen oder nach missglückter Überextraktion. Nicht anwenden, wenn die Methode einen wichtigen Fachbegriff ausdrückt oder eine stabile Erweiterungsgrenze bildet.
Risiken und Fehlanwendungen
Gefährlich ist das Inlining, wenn dadurch Duplikate entstehen oder eine fachliche Entscheidung wieder in technische Abläufe zurückwandert.
Enterprise-Java-Beispiel
Ein CustomerService besitzt isPreferredCustomer(), das nur customer.preferred() zurückgibt. Wenn der Name keinen Mehrwert bietet, kann der Aufruf direkt verwendet werden.
Vorhandener Java-21-Zielcode
CustomerPriority.java
package com.aydinsude.workbench.catalog;
// Refactoring: Inline Method
// Zweck: Eine bedeutungslose Weiterleitung entfernen, wenn der Ausdruck selbst klarer ist.
public final class CustomerPriority {
public boolean isPriority(int openCases, boolean vip) {
return vip || openCases >= 5;
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
3. Extract Variable
Fachliche Einordnung
Ein komplexer Ausdruck enthält mehrere fachliche Teilgedanken und muss beim Lesen mental zerlegt werden.
Teilresultate erhalten sprechende, unveränderliche Variablen. Der Code dokumentiert damit die Bedeutung der Berechnung und erleichtert Debugging sowie gezielte Tests.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Ausdruck schrittweise zerlegen, aussagekräftige Namen wählen, Seiteneffekte ausschließen, nach jeder Ersetzung kompilieren und Verhalten testen.
Wann sinnvoll?
Sinnvoll bei verschachtelten Bedingungen, langen Berechnungen oder wiederverwendeten Teilwerten. Nicht sinnvoll für offensichtliche Ausdrücke, deren Lesbarkeit durch zusätzliche Namen sinkt.
Risiken und Fehlanwendungen
Schlechte Namen wie tmp oder value verschieben nur die Unklarheit. Außerdem darf die Reihenfolge von Ausdrücken mit Seiteneffekten nicht unbemerkt verändert werden.
Enterprise-Java-Beispiel
Eine Preisberechnung benennt netAmount, taxAmount und grossAmount, statt alle Operationen in einem einzigen return-Ausdruck zu verschachteln.
Vorhandener Java-21-Zielcode
ShipmentEligibility.java
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Variable
// Zweck: Fachliche Teilbedingungen sichtbar und unabhängig prüfbar machen.
public final class ShipmentEligibility {
public boolean eligible(int weightKg, boolean hazardous, boolean addressVerified) {
boolean withinWeightLimit = weightKg <= 30;
boolean safeGoods = !hazardous;
return withinWeightLimit && safeGoods && addressVerified;
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
4. Inline Variable
Fachliche Einordnung
Eine lokale Variable wiederholt nur einen klaren Ausdruck, wird einmal genutzt und erschwert andere Refactorings.
Die Verwendung wird durch den ursprünglichen Ausdruck ersetzt; die Variable entfällt.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Sicherstellen, dass der Ausdruck keine Seiteneffekte besitzt und billig reproduzierbar ist
Dann Verwendung ersetzen, kompilieren, testen und Deklaration entfernen.
Wann sinnvoll?
Sinnvoll bei bedeutungslosen Zwischenvariablen oder wenn sie Extract Method verhindert. Nicht sinnvoll, wenn der Name fachliche Bedeutung trägt oder eine teure Berechnung mehrfach ausgeführt würde.
Risiken und Fehlanwendungen
Das Inlining darf keine mehrfachen Datenbank-, Zeit- oder Zufallsaufrufe erzeugen.
Enterprise-Java-Beispiel
Eine Variable eligible = policy.isEligible(order) kann bei einmaliger Nutzung inline stehen; ein berechneter Wechselkurs sollte dagegen bewusst gespeichert bleiben.
Vorhandener Java-21-Zielcode
OrderReference.java
package com.aydinsude.workbench.catalog;
// Refactoring: Inline Variable
// Zweck: Eine Variable entfernen, die keinen zusätzlichen Fachbegriff trägt.
public final class OrderReference {
public String format(String tenant, long sequence) {
return tenant.toUpperCase() + "-" + sequence;
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
5. Change Function Declaration
Fachliche Einordnung
Name, Parameter oder Rückgabetyp einer Funktion drücken ihren Vertrag nicht klar aus.
Die Deklaration wird so geändert, dass sie den fachlichen Zweck, notwendige Eingaben und ein typisiertes Ergebnis eindeutig vermittelt.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Öffentliche Nutzung ermitteln, Tests ergänzen, neuen Vertrag parallel anbieten oder atomar umstellen, alle Aufrufer migrieren und alten Vertrag kontrolliert entfernen.
Wann sinnvoll?
Sinnvoll bei unklaren Namen, primitiven Parametern oder fehleranfälligen Rückgabewerten. Vorsicht bei veröffentlichten APIs; dort sind Kompatibilitätsadapter und Deprecation nötig.
Risiken und Fehlanwendungen
Ein bloßes Umbenennen ohne Verbesserung der Semantik bringt wenig. Zu viele Parameter sind oft ein Hinweis auf Parameter Object oder Context Object.
Enterprise-Java-Beispiel
process(String, int) wird zu approveClaim(ClaimId, ApprovalLimit) und liefert ApprovalDecision statt boolean.
Vorhandener Java-21-Zielcode
PaymentAuthorizer.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Change Function Declaration
// Zweck: Namen und Parameter machen den Autorisierungsvertrag explizit.
public final class PaymentAuthorizer {
public Authorization authorizePayment(String accountId, BigDecimal amount) {
if (accountId == null || accountId.isBlank()) throw new IllegalArgumentException("accountId");
return new Authorization(accountId, amount, amount.signum() > 0);
}
public record Authorization(String accountId, BigDecimal amount, boolean approved) {}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
6. Encapsulate Variable
Fachliche Einordnung
Ein veränderlicher Wert ist direkt von vielen Stellen erreichbar; Änderungen und Invarianten sind nicht kontrollierbar.
Zugriffe laufen über eine klar definierte API. Lesen und Schreiben können validiert, protokolliert oder später durch eine andere Speicherung ersetzt werden.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Direkte Zugriffe suchen, Getter oder fachliche Operationen einführen, Schreibzugriffe migrieren, Feldzugriff einschränken und Tests für Invarianten ergänzen.
Wann sinnvoll?
Sinnvoll bei globalem Zustand, öffentlich veränderbaren Feldern oder Konfiguration. Nicht als Ausrede für wahllose Getter/Setter verwenden.
Risiken und Fehlanwendungen
Technische Setter ohne Fachsprache kapseln die Variable nur syntaktisch. Besser sind Operationen wie activate(), reserve() oder changeAddress().
Enterprise-Java-Beispiel
Ein öffentliches creditLimit wird privat; Änderungen erfolgen nur noch über adjustCreditLimit(...) mit Berechtigungs- und Grenzprüfung.
Vorhandener Java-21-Zielcode
ExchangeRateTable.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal; import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
// Refactoring: Encapsulate Variable
// Zweck: Änderungen validieren und interne Repräsentation verbergen.
public final class ExchangeRateTable {
private final Map<String, BigDecimal> rates = new ConcurrentHashMap<>();
public void update(String currency, BigDecimal rate) {
if (rate.signum() <= 0) throw new IllegalArgumentException("rate");
rates.put(currency, rate);
}
public BigDecimal rateFor(String currency) { return rates.get(currency); }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
7. Rename Variable
Fachliche Einordnung
Ein Variablenname beschreibt Herkunft, Einheit oder fachliche Bedeutung nicht zuverlässig.
Der Name wird so gewählt, dass Leser Zweck und Einheit ohne zusätzliche Kommentare verstehen.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Bedeutung im Kontext klären, alle Verwendungen automatisiert umbenennen, öffentliche Verträge separat betrachten und Tests ausführen.
Wann sinnvoll?
Sinnvoll bei Abkürzungen, generischen Namen und verwechselbaren Einheiten. Nicht überpräzisieren, wenn der enge Kontext die Bedeutung bereits eindeutig macht.
Risiken und Fehlanwendungen
Namen dürfen keine falsche Sicherheit erzeugen: amountInCents ist nur korrekt, wenn der Typ und alle Aufrufer tatsächlich Cent verwenden.
Enterprise-Java-Beispiel
amt wird zu invoiceTotal, timeout zu requestTimeoutMillis oder besser zu Duration requestTimeout.
Vorhandener Java-21-Zielcode
RiskScore.java
package com.aydinsude.workbench.catalog;
// Refactoring: Rename Variable
// Zweck: Fachliche Bedeutung statt Implementierungsabkürzungen ausdrücken.
public final class RiskScore {
public int calculate(int missedPayments, int fraudSignals) {
int paymentRiskPoints = missedPayments * 10;
int fraudRiskPoints = fraudSignals * 25;
return paymentRiskPoints + fraudRiskPoints;
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
8. Introduce Parameter Object
Fachliche Einordnung
Mehrere Parameter treten wiederholt gemeinsam auf und bilden fachlich eine Einheit.
Die zusammengehörigen Werte werden in einem unveränderlichen Typ gebündelt. Dadurch wird der Vertrag kürzer und Regeln finden einen natürlichen Ort.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Parametergruppe identifizieren, Value Object oder Record mit Validierung anlegen, neuen Parameter hinzufügen, Aufrufer schrittweise migrieren und alte Parameter entfernen.
Wann sinnvoll?
Sinnvoll bei Data Clumps und langen Signaturen. Nicht sinnvoll, wenn Werte nur zufällig einmal gemeinsam vorkommen oder der neue Typ keine kohärente Bedeutung besitzt.
Risiken und Fehlanwendungen
Ein bloßer Datencontainer kann zum Sammelbecken werden. Der Typ sollte Invarianten, Einheiten und sinnvolle Operationen ausdrücken.
Enterprise-Java-Beispiel
startDate, endDate und timezone werden zu BillingPeriod; der Typ verhindert ein Enddatum vor dem Startdatum.
Vorhandener Java-21-Zielcode
DeliveryPlanner.java
package com.aydinsude.workbench.catalog;
import java.time.LocalDate;
// Refactoring: Introduce Parameter Object
// Pattern: Value Object
// Zweck: Zusammengehörige Lieferparameter konsistent validieren und weiterreichen.
public final class DeliveryPlanner {
public String plan(DeliveryRequest request) {
return request.postalCode()+":"+request.requestedDate()+":"+request.express();
}
public record DeliveryRequest(String postalCode, LocalDate requestedDate, boolean express) {
public DeliveryRequest { if (postalCode == null || postalCode.isBlank()) throw new IllegalArgumentException("postalCode"); }
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
9. Combine Functions into Class
Fachliche Einordnung
Mehrere Funktionen arbeiten ständig auf denselben Daten und reichen dieselben Parameter weiter.
Daten und eng zusammengehörige Operationen werden in einer Klasse gebündelt. Gemeinsamer Zustand und Fachsprache werden explizit.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Gemeinsame Daten identifizieren, Klasse oder Value Object anlegen, Funktionen schrittweise verschieben, Aufrufer migrieren und Duplikate entfernen.
Wann sinnvoll?
Sinnvoll bei hoher Kohäsion und wiederkehrenden Parametergruppen. Nicht sinnvoll, wenn Funktionen unabhängig bleiben oder die Klasse nur ein Utility-Sammelbecken würde.
Risiken und Fehlanwendungen
Die neue Klasse darf nicht zur God Class wachsen. Verantwortungsgrenzen und unveränderlicher Zustand sind entscheidend.
Enterprise-Java-Beispiel
PricingContext bündelt Positionen, Kundensegment und Datum; calculateBasePrice(), discount() und tax() werden als klar getrennte Operationen modelliert.
Vorhandener Java-21-Zielcode
TemperatureReading.java
package com.aydinsude.workbench.catalog;
// Refactoring: Combine Functions into Class
// Zweck: Berechnungen besitzen einen gemeinsamen unveränderlichen Fachkontext.
public record TemperatureReading(double celsius) {
public double fahrenheit() { return celsius * 9 / 5 + 32; }
public boolean freezing() { return celsius <= 0; }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
10. Combine Functions into Transform
Fachliche Einordnung
Mehrere Ableitungen werden aus denselben Eingangsdaten berechnet und sollen gemeinsam als Ergebnis verfügbar sein.
Eine Transformationsfunktion erzeugt ein neues, unveränderliches Ergebnisobjekt mit allen abgeleiteten Werten.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Eingabe und gewünschte Ableitungen definieren, Result-Typ einführen, Berechnungen in eine reine Transformation verschieben und Aufrufer auf das Ergebnisobjekt umstellen.
Wann sinnvoll?
Sinnvoll für Berichte, Mapping und reine Ableitungen. Nicht sinnvoll, wenn Zustand geändert oder fachliche Lebenszyklen gesteuert werden.
Risiken und Fehlanwendungen
Versteckte I/O oder Mutation macht die Transformation schwer testbar. Außerdem darf das Resultat nicht zum unstrukturierten Allzweckobjekt werden.
Enterprise-Java-Beispiel
Aus OrderData entsteht OrderSummary mit net, tax, gross und margin; alle Werte werden in einem deterministischen Schritt berechnet.
Vorhandener Java-21-Zielcode
OrderSummaryTransform.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Combine Functions into Transform
// Zweck: Abgeleitete Werte in einem deterministischen Transformationsschritt bündeln.
public final class OrderSummaryTransform {
public Summary transform(BigDecimal net, BigDecimal taxRate) {
BigDecimal tax = net.multiply(taxRate);
return new Summary(net, tax, net.add(tax));
}
public record Summary(BigDecimal net, BigDecimal tax, BigDecimal gross) {}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
Kapitel 2 Phasen trennen und Datenflüsse klären10 Refactorings
Daten-, Schleifen- und Vertragsrefactorings als kleine, kompilierbare Transformationen.
11. Split Phase
Fachliche Einordnung
Eine Methode vermischt zwei Verarbeitungsschritte mit unterschiedlichen Regeln, Fehlern oder Änderungsgründen.
Der Ablauf wird in klar getrennte Phasen mit einem expliziten Zwischenmodell zerlegt.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Phasengrenze bestimmen, Zwischenobjekt einführen, erste Phase extrahieren, zweite Phase auf Zwischenobjekt umstellen und Fehlerverträge je Phase testen.
Wann sinnvoll?
Sinnvoll bei Parsing plus Fachentscheidung, Laden plus Berechnung oder Validierung plus Persistenz. Nicht sinnvoll bei sehr kurzen linearen Abläufen ohne eigenständige Grenze.
Risiken und Fehlanwendungen
Ein untypisiertes Map-Zwischenmodell verschiebt das Problem. Besser ist ein validierter Record mit klarer Semantik.
Enterprise-Java-Beispiel
CSV-Import wird in parseRow() und createOrderCommand() geteilt; technische Syntaxfehler und fachliche Ablehnungen bleiben unterscheidbar.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Split Phase
// Zweck: Technische Aufbereitung und fachliche Entscheidung getrennt verändern und testen.
public final class ShipmentImportPhases {
public ImportResult importLine(String line) {
ParsedShipment parsed = parse(line);
return classify(parsed);
}
private ParsedShipment parse(String line) {
String[] parts = line.split(";");
return new ParsedShipment(parts[0].trim(), Integer.parseInt(parts[1].trim()));
}
private ImportResult classify(ParsedShipment shipment) {
return new ImportResult(shipment.reference(), shipment.weightKg() <= 30);
}
record ParsedShipment(String reference, int weightKg) {}
public record ImportResult(String reference, boolean standardRoute) {}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
12. Replace Derived Variable with Query
Fachliche Einordnung
Ein abgeleiteter Wert wird als veränderlicher Zustand gespeichert und kann gegenüber seinen Quelldaten veralten.
Der Wert wird bei Bedarf aus der maßgeblichen Quelle berechnet. Dadurch entfällt Synchronisationslogik.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Alle Schreibstellen des abgeleiteten Feldes finden, reine Berechnungsfunktion einführen, Leser umstellen und Feld samt Aktualisierungslogik entfernen.
Wann sinnvoll?
Sinnvoll bei billig berechenbaren Werten. Nicht sinnvoll bei teuren Berechnungen, die bewusst als Cache mit klarer Invalidierung modelliert sind.
Risiken und Fehlanwendungen
Eine Query darf keine Seiteneffekte besitzen. Bei Performanceproblemen braucht es eine explizite Cache-Policy statt eines versteckten Feldes.
Enterprise-Java-Beispiel
Order speichert total nicht mehr separat, sondern berechnet total() aus den Positionen.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal; import java.util.ArrayList; import java.util.List;
// Refactoring: Replace Derived Variable with Query
// Zweck: Keine synchron zu haltende Gesamtsumme speichern.
public final class CartTotalQuery {
private final List<BigDecimal> prices = new ArrayList<>();
public void add(BigDecimal price) { prices.add(price); }
public BigDecimal total() { return prices.stream().reduce(BigDecimal.ZERO, BigDecimal::add); }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
13. Change Reference to Value
Fachliche Einordnung
Ein gemeinsam referenziertes Objekt besitzt keine echte Identität und erzeugt unnötige Lebenszyklus- oder Mutationskopplung.
Das Objekt wird zu einem unveränderlichen Value Object mit Wertgleichheit. Kopien sind gleichwertig und können sicher geteilt werden.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Identitätsnutzung prüfen, Mutation entfernen, Konstruktion validieren, equals/hashCode oder Record-Semantik einführen und Referenzregister auflösen.
Wann sinnvoll?
Sinnvoll für Geld, Adressen, Zeiträume und Codes. Nicht sinnvoll, wenn Historie, individuelle Identität oder unabhängiger Lebenszyklus relevant sind.
Risiken und Fehlanwendungen
Ein Value Object darf keine versteckte Mutation enthalten. Datenbank-IDs allein machen ein Fachobjekt nicht automatisch zur Entity.
Enterprise-Java-Beispiel
CurrencyCode wird als validierter Record modelliert; zwei Instanzen mit EUR sind fachlich gleich.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Change Reference to Value
// Pattern: Value Object
// Zweck: Adressen werden unveränderlich und anhand ihres Inhalts verglichen.
public record CustomerAddressValue(String street, String city, String postalCode) {
public CustomerAddressValue {
if (street == null || city == null || postalCode == null) throw new IllegalArgumentException("address");
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
14. Change Value to Reference
Fachliche Einordnung
Mehrere Value-Instanzen repräsentieren tatsächlich dasselbe identifizierbare Fachobjekt und müssen konsistent geändert werden.
Ein Repository oder Identity Map liefert eine kanonische Referenz mit stabiler Identität.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sinnvoll bei Kunden, Verträgen oder Produkten mit eigenem Lebenszyklus. Nicht sinnvoll für reine Werte ohne Identität.
Risiken und Fehlanwendungen
Ein globales Registry-Singleton erzeugt versteckte Kopplung. Die Referenzquelle sollte explizit injiziert und testbar sein.
Enterprise-Java-Beispiel
Aus mehrfach kopierten Customer-Daten wird eine Customer-Entity, die über CustomerRepository anhand CustomerId geladen wird.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Map; import java.util.concurrent.ConcurrentHashMap;
// Refactoring: Change Value to Reference
// Pattern: Identity Map / Registry
// Zweck: Pro Kunden-ID wird eine kanonische Instanz verwendet.
public final class CustomerReferenceRegistry {
private final Map<String, Customer> customers = new ConcurrentHashMap<>();
public Customer customer(String id, String name) { return customers.computeIfAbsent(id, key -> new Customer(key,name)); }
public static final class Customer { private final String id; private final String name; Customer(String id,String name){this.id=id;this.name=name;} public String id(){return id;} public String name(){return name;} }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
15. Split Loop
Fachliche Einordnung
Eine Schleife berechnet mehrere unabhängige Resultate und vermischt verschiedene Zwecke.
Die Schleife wird in getrennte Durchläufe mit jeweils einem klaren Ziel aufgeteilt.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Verhalten und Performance messen, unabhängige Resultate identifizieren, erste Schleife kopieren, Aufgaben trennen und Tests vergleichen.
Wann sinnvoll?
Sinnvoll, wenn Verständlichkeit und spätere Extraktion gewinnen. Nicht sinnvoll bei sehr großen Datenmengen oder Stream-Quellen, bei denen zusätzliche Durchläufe teuer sind.
Risiken und Fehlanwendungen
Nicht blind anwenden: Bei Performancekritik kann eine Single-Pass-Aggregation mit benanntem Accumulator besser sein.
Enterprise-Java-Beispiel
Eine Schleife, die Summe und Risikoindikatoren berechnet, wird in calculateTotal() und collectRiskFlags() geteilt.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal; import java.util.List;
// Refactoring: Split Loop
// Zweck: Zwei unabhängige Fragen getrennt lesbar und veränderbar machen.
public final class OrderAnalysisLoops {
public BigDecimal total(List<BigDecimal> amounts) { BigDecimal total=BigDecimal.ZERO; for (var amount:amounts) total=total.add(amount); return total; }
public long expensiveCount(List<BigDecimal> amounts) { long count=0; for (var amount:amounts) if(amount.compareTo(new BigDecimal("100"))>0) count++; return count; }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
16. Slide Statements
Fachliche Einordnung
Zusammengehörige Anweisungen liegen auseinander und erschweren das Erkennen einer Einheit oder Extraktion.
Anweisungen werden ohne Verhaltensänderung räumlich zusammengeführt.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Datenabhängigkeiten und Seiteneffekte prüfen, Anweisung schrittweise verschieben, nach jedem Schritt kompilieren und Tests ausführen.
Wann sinnvoll?
Sinnvoll als Vorbereitung für Extract Method oder zur besseren lokalen Lesbarkeit. Nicht verschieben, wenn Reihenfolge fachlich oder technisch relevant ist.
Risiken und Fehlanwendungen
Besonders kritisch sind Logging, Transaktionen, Locks und I/O. Ihre Reihenfolge darf nicht nur aufgrund optischer Nähe verändert werden.
Enterprise-Java-Beispiel
Validierung und Aufbau eines PaymentCommand werden direkt vor dem Versand zusammengeführt und anschließend extrahiert.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Slide Statements
// Zweck: Validierung und Normalisierung stehen direkt vor ihrer Verwendung.
public final class InvoicePreparation {
public BigDecimal prepare(BigDecimal net, BigDecimal taxRate) {
if (net.signum() < 0) throw new IllegalArgumentException("net");
BigDecimal normalizedRate = taxRate.stripTrailingZeros();
return net.add(net.multiply(normalizedRate));
}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
17. Replace Loop with Pipeline
Fachliche Einordnung
Eine Schleife enthält mehrere klar trennbare Schritte wie Filtern, Abbilden und Aggregieren.
Der Kontrollfluss wird als deklarative Pipeline aus benannten Operationen formuliert.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Seiteneffekte entfernen, Schritte identifizieren, Filter/Map/Reduce oder eigene Pipeline-Stufen einführen und Ergebnis gegen bestehende Tests prüfen.
Wann sinnvoll?
Sinnvoll bei reinen Transformationen. Nicht sinnvoll, wenn komplexe Abbruchlogik, mutable Zustände oder Debugbarkeit durch eine Pipeline leiden.
Risiken und Fehlanwendungen
Streams sind kein Selbstzweck. Tiefe, verschachtelte Pipelines können schlechter lesbar sein als eine einfache Schleife.
Enterprise-Java-Beispiel
OrderLines werden gefiltert, in Money-Werte abgebildet und mit Money::add aggregiert.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal; import java.util.List;
// Refactoring: Replace Loop with Pipeline
// Zweck: Filter, Abbildung und Aggregation als deklarative Schritte sichtbar machen.
public final class ActiveCustomerRevenue {
public BigDecimal calculate(List<Customer> customers) {
return customers.stream().filter(Customer::active).map(Customer::revenue).reduce(BigDecimal.ZERO,BigDecimal::add);
}
public record Customer(boolean active, BigDecimal revenue) {}
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
18. Remove Dead Code
Fachliche Einordnung
Code ist nicht erreichbar, nicht mehr aufgerufen oder durch alte Featurepfade überholt.
Nicht benötigte Bestandteile werden vollständig entfernt, statt sie aus Angst dauerhaft mitzuschleppen.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Nutzung statisch und dynamisch prüfen, Tests und Telemetrie auswerten, öffentlichen Vertrag beachten, entfernen und Build sowie Regressionstests ausführen.
Wann sinnvoll?
Sinnvoll bei bestätigter Nichtnutzung. Nicht vorschnell bei Reflection, Framework-Konfiguration oder externen API-Aufrufern.
Risiken und Fehlanwendungen
Auskommentierter Code gehört ebenfalls entfernt; Versionsverwaltung ist das Archiv. Bei öffentlichen APIs ist zuerst Deprecation nötig.
Enterprise-Java-Beispiel
Ein alter LegacyPricingAdapter wird nach vollständiger Migration und Nutzungsprüfung entfernt.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Remove Dead Code
// Zweck: Nur die tatsächlich unterstützte Preisregel bleibt Teil des Vertrags.
public final class CurrentPricingRule {
public BigDecimal price(BigDecimal base, boolean premium) { return premium ? base.multiply(new BigDecimal("0.90")) : base; }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
19. Separate Query from Modifier
Fachliche Einordnung
Eine Methode liefert Informationen und verändert gleichzeitig Zustand; Aufrufer können Folgen nicht erkennen.
Lesende Query und verändernder Command werden getrennt. Der Vertrag macht Seiteneffekte explizit.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Beobachtbares Verhalten sichern, Rückgabewert und Mutation analysieren, Query extrahieren, Command separat benennen und Aufrufer in klarer Reihenfolge umstellen.
Wann sinnvoll?
Sinnvoll bei überraschenden Seiteneffekten und schwer testbaren Methoden. Nicht zwingend, wenn eine fachliche Operation natürlich ein Ergebnis ihrer Änderung zurückgibt, etwa reserve() mit ReservationResult.
Risiken und Fehlanwendungen
Eine künstliche Trennung kann Race Conditions erzeugen, wenn Query und Command atomar zusammengehören. Dann ist ein Decision Object als Ergebnis der Operation besser.
Enterprise-Java-Beispiel
getAndMarkInvoicePaid() wird zu paymentStatus() und markPaid(...), sofern beide nicht atomar gekoppelt sein müssen.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.HashSet; import java.util.Set;
// Refactoring: Separate Query from Modifier
// Command-Query Separation: Abfrage und Zustandsänderung sind getrennt.
public final class ReservationLedger {
private final Set<String> reserved = new HashSet<>();
public boolean isReserved(String id) { return reserved.contains(id); }
public void reserve(String id) { if(!reserved.add(id)) throw new IllegalStateException("already reserved"); }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
20. Parameterize Function
Fachliche Einordnung
Mehrere nahezu gleiche Funktionen unterscheiden sich nur durch einen Wert oder eine kleine Strategie.
Die Unterschiede werden als expliziter Parameter modelliert; Duplikate verschwinden und der gemeinsame Ablauf bleibt sichtbar.
Erkennungsmerkmale
Lesefluss oder Änderbarkeit leiden sichtbar.
Mehrere Gründe zur Änderung liegen in derselben Struktur.
Der vorhandene Code erschwert isolierte Tests oder sichere Erweiterungen.
Sicherer Ablauf
Duplikate vergleichen, echte Variationsachse bestimmen, Typ oder Enum statt primitiver Flags wählen, gemeinsame Funktion bilden und Aufrufer migrieren.
Wann sinnvoll?
Sinnvoll bei stabiler gemeinsamer Struktur. Nicht sinnvoll, wenn die Varianten unterschiedliche Verantwortung besitzen oder ein wachsender Flag-Parameter entsteht.
Risiken und Fehlanwendungen
Boolean-Parameter verschlechtern oft die Lesbarkeit. Besser sind fachliche Enums, Value Objects oder Strategy-Objekte.
Enterprise-Java-Beispiel
calculateDomesticFee() und calculateInternationalFee() werden zu calculateFee(ShippingZone zone), solange der Ablauf tatsächlich gemeinsam bleibt.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Parameterize Function
// Strategy-Parameter: Der Grenzwert ist Teil des Aufrufvertrags statt duplizierter Methoden.
public final class ThresholdClassifier {
public boolean reaches(int score, int threshold) { return score >= threshold; }
}
Teststrategie: Vor der Änderung das äußere Verhalten mit Characterization Tests sichern. Nach jedem kleinen Transformationsschritt kompilieren und gezielte Unit-Tests ausführen. Bei öffentlichen Verträgen zusätzlich Kompatibilitäts- oder Contract-Tests verwenden.
Die Erklärung ist eine eigenständige, auf das konkrete Workbench-Beispiel bezogene Zusammenfassung. Die Quellen dienen zur fachlichen Einordnung von Refactoring, Code Smells, Verhaltenssicherung und schrittweisem Vorgehen.
Kapitel 3 Parameter, Abfragen und Rückgabewerte vereinfachen10 Refactorings
API-, Parameter-, Command- und Lebenszyklusrefactorings als kleine, kompilierbare Transformationen.
21. Replace Parameter with Query
Fachliches Ausgangsproblem
Ein Aufrufer übergibt einen Wert, den die aufgerufene Methode aus einem bereits vorhandenen Objekt oder Dienst selbst zuverlässig bestimmen kann. Dadurch müssen mehrere Aufrufer dieselbe Ableitungslogik kennen und können widersprüchliche Werte liefern.
Erkennungsmerkmale
Typische Hinweise sind Parameter, die unmittelbar vor dem Aufruf aus einem anderen Argument berechnet werden, identische Berechnungen an mehreren Aufrufstellen oder Parameter, deren Wert stets aus dem Empfängerobjekt stammt.
Nutzen der Transformation
Der Vertrag wird kürzer, die Wissensverantwortung liegt an einer Stelle und inkonsistente Aufruferberechnungen verschwinden.
Sicherer Refactoring-Ablauf
Verhalten mit Charakterisierungstests absichern.
Die interne Abfrage zunächst zusätzlich zum Parameter einführen und beide Werte vergleichen.
Alle Aufrufer auf den vereinfachten Vertrag umstellen.
Den redundanten Parameter entfernen und die Abfrage hinter einer klar benannten Methode kapseln.
Einsatzgrenze
Nicht anwenden, wenn die Methode bewusst von außen steuerbar bleiben soll, die Abfrage teuer ist oder ein versteckter Zugriff auf Infrastruktur entstehen würde.
Risiken und Fehlanwendungen
Eine häufige Fehlanwendung ersetzt einen sichtbaren Parameter durch globalen Zustand oder eine Datenbankabfrage. Das reduziert die Signatur, erhöht aber die versteckte Kopplung.
Verwandte Techniken
Verwandt mit Preserve Whole Object, Extract Query Object und Introduce Parameter Object.
Enterprise-Java-Einordnung
Eine Versandberechnung liest Expressstatus und Kundensegment aus dem typisierten Auftrag, statt einen extern berechneten Multiplikator entgegenzunehmen.
Teststrategie
Tests sollten Standard-, Express- und Grenzfälle abdecken und beweisen, dass kein Aufrufer mehr einen abweichenden Faktor einschleusen kann.
ShippingFeeQuery.java
ShippingFeeQuery.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Parameter with Query
// Zweck: Der Gebührenfaktor wird aus dem Auftrag statt vom Aufrufer geliefert.
public final class ShippingFeeQuery {
public BigDecimal fee(Order order) {
return order.baseFee().multiply(order.express() ? new BigDecimal("1.50") : BigDecimal.ONE);
}
public record Order(BigDecimal baseFee, boolean express) {}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Methode greift selbst auf veränderlichen Zustand, Zeit, Konfiguration oder Infrastruktur zu. Ihr Ergebnis hängt dadurch von unsichtbaren Bedingungen ab und lässt sich nur schwer reproduzieren.
Erkennungsmerkmale
Warnsignale sind direkte Aufrufe von Uhr, Zufall, Systemkonfiguration, Security Context oder Repository innerhalb einer eigentlich fachlichen Berechnung.
Nutzen der Transformation
Abhängigkeiten werden sichtbar; die Methode wird deterministisch, leichter testbar und unabhängig von Laufzeitumgebung oder Thread-Kontext.
Sicherer Refactoring-Ablauf
Die versteckte Abfrage identifizieren und ihren fachlichen Typ bestimmen.
Den Wert zunächst als zusätzlichen Parameter einführen.
Aufrufer so umbauen, dass sie den Wert an einer klaren Systemgrenze beschaffen.
Den internen Zugriff entfernen und Tests mit expliziten Werten ergänzen.
Einsatzgrenze
Nicht anwenden, wenn der abgefragte Wert Teil der stabilen Identität des Empfängerobjekts ist oder die Übergabe nur eine lange Kette bedeutungsloser Parameter erzeugt.
Risiken und Fehlanwendungen
Zu viele primitive Parameter verschlechtern den Vertrag. In diesem Fall ist ein Context Object oder Value Object meist der bessere nächste Schritt.
Verwandte Techniken
Verwandt mit Extract Clock, Introduce Context Object und Dependency Injection.
Enterprise-Java-Einordnung
Eine Fristberechnung erhält ein Instant oder eine Clock-Schnittstelle, statt LocalDateTime.now() tief im Domain-Code aufzurufen.
Teststrategie
Tests verwenden feste Zeitpunkte, verschiedene Zeitzonen und Grenzwerte rund um Fristwechsel.
TaxCalculationParameter.java
TaxCalculationParameter.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Query with Parameter
// Dependency Injection: Der Steuersatz ist explizite Eingabe statt versteckter Abfrage.
public final class TaxCalculationParameter {
public BigDecimal gross(BigDecimal net, BigDecimal taxRate) {
return net.add(net.multiply(taxRate));
}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein Aufrufer zerlegt ein fachliches Objekt in mehrere Einzelwerte und reicht diese gemeinsam weiter. Die aufgerufene Methode verliert dadurch Kontext und ihre Signatur wächst bei jeder neuen Regel.
Erkennungsmerkmale
Mehrere Parameter werden immer aus demselben Objekt gelesen, ändern sich gemeinsam oder bilden eine bekannte fachliche Einheit.
Nutzen der Transformation
Die Methode erhält den fachlichen Zusammenhang, Signaturen bleiben stabiler und neue Regeln können ohne wiederholte Parameterketten ergänzt werden.
Sicherer Refactoring-Ablauf
Die benötigten Werte und ihre Quelle dokumentieren.
Das vollständige Objekt zusätzlich übergeben und intern zunächst nur lesend verwenden.
Einzelparameter schrittweise durch Objektzugriffe ersetzen.
Redundante Parameter entfernen und die Abhängigkeit auf eine schmale fachliche Sicht begrenzen.
Einsatzgrenze
Nicht anwenden, wenn dadurch eine zu breite Abhängigkeit entsteht, sensible Daten unnötig weitergereicht werden oder nur ein einzelner stabiler Wert benötigt wird.
Risiken und Fehlanwendungen
Das ganze Objekt darf nicht als bequemer Datenbeutel missbraucht werden. Besser ist gegebenenfalls ein fokussiertes Interface oder Value Object.
Verwandte Techniken
Verwandt mit Introduce Parameter Object, Extract Interface und Replace Parameter with Query.
Enterprise-Java-Einordnung
Eine Kreditprüfung erhält den LoanApplication-Kontext statt Einkommen, Laufzeit, Betrag und Kundengruppe als lose Einzelparameter.
Teststrategie
Tests prüfen, dass nur relevante Felder ausgewertet werden und Änderungen an irrelevanten Feldern das Ergebnis nicht beeinflussen.
EligibilityPreserveWholeObject.java
EligibilityPreserveWholeObject.java
package com.aydinsude.workbench.catalog;
// Refactoring: Preserve Whole Object
// Zweck: Die Regel erhält den vollständigen Antrag und bleibt bei neuen Kriterien stabil.
public final class EligibilityPreserveWholeObject {
public boolean eligible(Application application) {
return application.age() >= 18 && application.annualIncome() >= 30000 && !application.blocked();
}
public record Application(int age, int annualIncome, boolean blocked) {}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein boolescher oder codierter Parameter schaltet innerhalb einer Methode zwischen deutlich verschiedenen Abläufen um. Der Aufruf verrät ohne Blick in die Implementierung nicht, welches Verhalten gewählt wird.
Erkennungsmerkmale
Aufrufe wie process(order, true), save(entity, false) oder mehrere Verzweigungen auf demselben Flag sind klare Hinweise.
Nutzen der Transformation
Aufrufe werden selbsterklärend, Varianten können getrennt getestet und später unabhängig entwickelt werden.
Sicherer Refactoring-Ablauf
Alle Flagwerte und Aufrufstellen erfassen.
Für die fachlich unterschiedlichen Absichten sprechende Methoden einführen.
Gemeinsame Teile extrahieren, ohne die Varianten künstlich zusammenzuhalten.
Aufrufer migrieren und das Flag entfernen.
Einsatzgrenze
Nicht anwenden, wenn das Argument echten Datencharakter besitzt und nicht die Auswahl eines Verfahrens darstellt.
Risiken und Fehlanwendungen
Nur zwei Wrapper um dieselbe riesige Flag-Methode zu legen reicht nicht. Die Verzweigung muss tatsächlich in klar benannte Abläufe oder Strategien überführt werden.
Verwandte Techniken
Verwandt mit Replace Conditional with Polymorphism, Strategy und Command.
Enterprise-Java-Einordnung
Aus approve(claim, true) und approve(claim, false) werden approveAutomatically(claim) und requestManualApproval(claim).
Teststrategie
Für jede neue Methode eigene Vertrags- und Negativtests anlegen; gemeinsame Invarianten zusätzlich als Contract Test sichern.
DocumentDeliveryCommands.java
DocumentDeliveryCommands.java
package com.aydinsude.workbench.catalog;
// Refactoring: Remove Flag Argument
// Command-Methoden: Jede Variante besitzt einen sprechenden Vertrag.
public final class DocumentDeliveryCommands {
public String sendImmediately(String documentId) { return "sent:" + documentId; }
public String queueForReview(String documentId) { return "queued:" + documentId; }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein öffentlicher Konstruktor muss technische Details, Variantenwahl oder Validierungsreihenfolge offenlegen. Der Typname allein beschreibt nicht, welche Erzeugungsabsicht vorliegt.
Erkennungsmerkmale
Mehrere Konstruktoren mit ähnlichen Signaturen, optionale Nullwerte, technische Parameter oder nachgelagerte Initialisierung weisen darauf hin.
Nutzen der Transformation
Benannte Erzeugung macht Absicht und Variante sichtbar, kann konkrete Implementierungen verbergen und garantiert vollständige Objekte.
Sicherer Refactoring-Ablauf
Eine benannte Factory Function parallel zum Konstruktor einführen.
Invarianten und Standardwerte in die Factory verlagern.
Aufrufer schrittweise migrieren.
Konstruktorzugriff einschränken, sobald keine externe Nutzung mehr besteht.
Einsatzgrenze
Nicht anwenden, wenn ein einfacher Konstruktor den Vertrag klar ausdrückt und keine Varianten oder Invarianten verborgen werden müssen.
Risiken und Fehlanwendungen
Factories dürfen nicht zu allwissenden Service Locators werden. Externe Abhängigkeiten gehören in einen Composition Root oder eine eigene Factory-Klasse.
Verwandte Techniken
Verwandt mit Factory Method, Static Factory, Builder und Introduce Value Object.
Enterprise-Java-Einordnung
Payment.ofCard(...) und Payment.ofBankTransfer(...) erzeugen typisierte Zahlungsvarianten mit geprüften Pflichtdaten.
Teststrategie
Tests prüfen Invarianten, Standardwerte und die Ablehnung unvollständiger Erzeugungsdaten.
PaymentRequestFactory.java
PaymentRequestFactory.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Constructor with Factory Function
// Pattern: Factory Method
public record PaymentRequestFactory(String reference, BigDecimal amount, String currency) {
public static PaymentRequestFactory euroPayment(String reference, BigDecimal amount) {
if (amount.signum() <= 0) throw new IllegalArgumentException("amount");
return new PaymentRequestFactory(reference, amount, "EUR");
}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Funktion benötigt immer mehr Zustand, optionale Schritte, Wiederholung, Audit oder Rücknahme. Parameter und Kontrollfluss werden unübersichtlich.
Erkennungsmerkmale
Lange Parameterlisten, mehrere Hilfsmethoden, temporärer Zustand und der Wunsch nach Retry, Queueing oder Undo sind typische Hinweise.
Nutzen der Transformation
Der Vorgang wird als Objekt adressierbar, protokollierbar, warteschlangenfähig und isoliert testbar.
Sicherer Refactoring-Ablauf
Den Funktionsaufruf als unveränderliches Command-Objekt modellieren.
Eingaben im Konstruktor oder Record bündeln.
Ausführung in execute() oder handle() verschieben.
Nebenwirkungen über Ports führen und Ergebnis explizit typisieren.
Einsatzgrenze
Nicht anwenden, wenn eine reine, kurze Funktion ohne Lebenszyklus oder Nebenwirkungen ausreicht.
Risiken und Fehlanwendungen
Ein Command darf nicht gleichzeitig Datenmodell, Transaktion, Repository und Event Bus besitzen. Die Ausführung bleibt an klaren Ports ausgerichtet.
Verwandte Techniken
Verwandt mit Command Pattern, Application Service und Process Manager.
Enterprise-Java-Einordnung
CloseAccountCommand enthält AccountId, Grund und Antragsteller; ein Handler koordiniert Validierung, Persistenz und Domain Event.
Teststrategie
Tests prüfen erfolgreiche Ausführung, Ablehnung, Idempotenz und Wiederholung mit identischer Command-ID.
ApproveClaimCommand.java
ApproveClaimCommand.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Function with Command
// Pattern: Command
public final class ApproveClaimCommand {
private final String claimId; private boolean executed;
public ApproveClaimCommand(String claimId) { this.claimId=claimId; }
public Result execute() { if(executed) throw new IllegalStateException("already executed"); executed=true; return new Result(claimId,"APPROVED"); }
public record Result(String claimId, String status) {}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Command-Klasse besitzt keinen eigenen Zustand oder Lebenszyklus und kapselt lediglich eine kurze, reine Berechnung. Die zusätzliche Objektstruktur erschwert das Lesen.
Erkennungsmerkmale
Das Objekt wird unmittelbar erzeugt und einmal ausgeführt, besitzt keine Ports, kein Audit, kein Retry und keine austauschbaren Implementierungen.
Nutzen der Transformation
Weniger Typen, direkterer Kontrollfluss und geringere Navigationskosten.
Sicherer Refactoring-Ablauf
Reinheit und Seiteneffektfreiheit durch Tests belegen.
Berechnung als benannte Funktion oder statische Methode anbieten.
Aufrufer migrieren.
Command-Klasse und unnötige Factory entfernen.
Einsatzgrenze
Nicht anwenden, wenn der Vorgang als Nachricht, Warteschlangenauftrag, Transaktionsgrenze oder Erweiterungspunkt gebraucht wird.
Risiken und Fehlanwendungen
Das Entfernen darf keinen fachlichen Begriff zerstören. Eine gute Funktion braucht weiterhin einen präzisen Namen und typisierte Ein- und Ausgaben.
Verwandte Techniken
Verwandt mit Inline Class, Substitute Algorithm und Pure Function.
Enterprise-Java-Einordnung
Ein VatCalculationCommand ohne Zustand wird zu calculateVat(Money, TaxRate).
Teststrategie
Property- und Grenzwerttests sichern die reine Berechnung; keine Mock-Interaktionen nötig.
NormalizeCustomerName.java
NormalizeCustomerName.java
package com.aydinsude.workbench.catalog;
import java.util.Locale;
// Refactoring: Replace Command with Function
// Zweck: Eine zustandslose Transformation bleibt eine kleine reine Funktion.
public final class NormalizeCustomerName {
public String normalize(String value) { return value.trim().replaceAll("\\s+"," ").toUpperCase(Locale.ROOT); }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Methode verändert einen übergebenen Wert oder ein umgebendes Objekt, ohne dass der Aufruf diese Änderung deutlich macht. Leser müssen Seiteneffekte erraten.
Erkennungsmerkmale
Void-Methoden mit Mutationen, gemeinsam genutzte Akkumulatoren oder veränderte Parameter sind typische Anzeichen.
Nutzen der Transformation
Datenfluss wird sichtbar, Methoden lassen sich leichter kombinieren und unveränderliche Modelle werden möglich.
Sicherer Refactoring-Ablauf
Den tatsächlich erzeugten oder veränderten Wert bestimmen.
Die Methode so umstellen, dass sie den neuen Wert zurückgibt.
Aufrufer auf explizite Zuweisung migrieren.
Verdeckte Mutation reduzieren oder durch unveränderliche Typen ersetzen.
Einsatzgrenze
Nicht anwenden, wenn die Operation bewusst ein zustandsbehaftetes Aggregate verändert und dessen Invarianten nur intern geschützt werden können.
Risiken und Fehlanwendungen
Ein Rückgabewert allein macht eine mutierende Methode noch nicht rein. Dokumentation und Typmodell müssen klar zeigen, ob zusätzlich Zustand verändert wird.
Verwandte Techniken
Verwandt mit Immutable Value Object, Combine Functions into Transform und Command-Query Separation.
Enterprise-Java-Einordnung
normalize(address) liefert eine neue Address zurück, statt das übergebene DTO still zu verändern.
Teststrategie
Tests vergleichen ursprünglichen und neuen Wert und prüfen, dass die Eingabe unverändert bleibt.
RiskScoreUpdate.java
RiskScoreUpdate.java
package com.aydinsude.workbench.catalog;
// Refactoring: Return Modified Value
// Zweck: Der Datenfluss des neuen Risikowerts ist am Aufruf sichtbar.
public final class RiskScoreUpdate {
public Assessment applyDocumentRisk(Assessment current, int documentRisk) {
return new Assessment(current.customerId(), current.score() + documentRisk);
}
public record Assessment(String customerId, int score) {}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein Codeabschnitt implementiert bereits ein bekanntes Konzept, ruft aber nicht die dafür vorhandene Funktion auf. Dadurch entstehen semantische Duplikate.
Erkennungsmerkmale
Mehrere Stellen enthalten fast gleiche Bedingungen, Normalisierungen oder Berechnungen, obwohl ein passender zentraler Vertrag existiert.
Nutzen der Transformation
Ein Begriff besitzt nur noch eine Implementierung; Fehlerkorrekturen und Regeländerungen wirken konsistent.
Sicherer Refactoring-Ablauf
Verhalten von Inline-Code und Zielfunktion mit Tests vergleichen.
Abweichungen sichtbar machen und entscheiden, welche Semantik korrekt ist.
Inline-Code durch Funktionsaufruf ersetzen.
Duplikate und veraltete Hilfswerte entfernen.
Einsatzgrenze
Nicht anwenden, wenn die vorhandene Funktion nur zufällig ähnlich ist oder einen anderen fachlichen Vertrag besitzt.
Risiken und Fehlanwendungen
Das Refactoring darf Unterschiede nicht glattbügeln. Vor der Ersetzung müssen Vorbedingungen, Einheiten und Fehlerfälle identisch sein.
Verwandte Techniken
Verwandt mit Extract Method, Remove Dead Code und Introduce Value Object.
Enterprise-Java-Einordnung
Eine manuelle IBAN-Normalisierung wird durch Iban.normalize(raw) ersetzt.
Teststrategie
Differentialtests vergleichen bisherige Inline-Logik und zentrale Funktion für repräsentative Datensätze.
PremiumCustomerRule.java
PremiumCustomerRule.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Inline Code with Function Call
// Specification-artige Regel: Die fachliche Aussage besitzt einen Namen.
public final class PremiumCustomerRule {
public boolean isPremium(Customer customer) { return customer.orders() >= 10 && customer.revenue().compareTo(new BigDecimal("5000")) >= 0; }
public record Customer(int orders, BigDecimal revenue) {}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein Objekt kann nach seiner Erzeugung über einen Setter in einen ungültigen oder nur teilweise initialisierten Zustand gebracht werden.
Erkennungsmerkmale
Setter für Pflichtwerte, nur einmal gesetzte Felder oder Werte mit komplexen Invarianten sind typische Hinweise.
Nutzen der Transformation
Objekte sind nach Erzeugung gültig, Invarianten werden lokal geschützt und nebenläufige Nutzung wird sicherer.
Sicherer Refactoring-Ablauf
Schreibzugriffe und Lebenszyklus des Feldes analysieren.
Pflichtwert in Konstruktor oder Factory aufnehmen.
Fachliche Änderungen durch benannte Operationen modellieren.
Setter entfernen und Feld möglichst final machen.
Einsatzgrenze
Nicht anwenden, wenn ein Framework zwingend einen Setter benötigt; dann ist ein Persistence DTO oder Adapter statt eines offenen Domain-Setters sinnvoll.
Risiken und Fehlanwendungen
Ein generischer setStatus(...) sollte nicht bloß durch einen privaten Setter ersetzt werden. Zustandswechsel brauchen Methoden wie approve(), cancel() oder suspend().
Verwandte Techniken
Verwandt mit Encapsulate Variable, Introduce Factory und Immutable Object.
Enterprise-Java-Einordnung
Eine Order erhält currency im Konstruktor; Statusänderungen laufen ausschließlich über geprüfte Domain-Operationen.
Teststrategie
Tests prüfen, dass ungültige Zwischenzustände nicht erzeugbar sind und erlaubte Übergänge weiterhin funktionieren.
ImmutableContractNumber.java
ImmutableContractNumber.java
package com.aydinsude.workbench.catalog;
// Refactoring: Remove Setting Method
// Value Object: Vertragsnummer ist nach Erzeugung unveränderlich.
public record ImmutableContractNumber(String value) {
public ImmutableContractNumber { if(value==null || value.isBlank()) throw new IllegalArgumentException("value"); }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Kapitel 4 Funktionen und Verantwortlichkeiten verschieben10 Refactorings
Verantwortungsverschiebung, Typcodes und Hierarchie-Refactorings mit kompilierbarem Java-21-Zielcode.
31. Move Function
Fachliches Ausgangsproblem
Eine Methode verwendet überwiegend Daten und Verhalten einer anderen Klasse. Ihr aktueller Ort erhöht Kopplung und zwingt den Aufrufer, fremdes Wissen zu besitzen.
Erkennungsmerkmale
Feature Envy zeigt sich durch viele Zugriffe auf ein anderes Objekt, lange Navigationsketten oder Parameter, die fast ausschließlich Daten des Zielobjekts transportieren.
Nutzen der Transformation
Verantwortung und Daten liegen näher zusammen; Klassen werden kohärenter und Schnittstellen kleiner.
Sicherer Refactoring-Ablauf
Abhängigkeiten der Methode messen und Zielklasse bestimmen.
Methode zunächst im Ziel duplizieren und durch Tests vergleichen.
Aufrufer delegieren lassen.
Alte Implementierung entfernen und Sichtbarkeiten verkleinern.
Einsatzgrenze
Nicht verschieben, wenn die Methode mehrere Aggregate koordiniert oder bewusst eine Application-Service-Verantwortung besitzt.
Risiken und Fehlanwendungen
Einfaches Verschieben kann zyklische Abhängigkeiten erzeugen. Dann ist eine neue Domain Service-, Policy- oder Mapper-Grenze besser.
Verwandte Techniken
Verwandt mit Move Field, Extract Class und Domain Service.
Enterprise-Java-Einordnung
discountFor(customer) wandert aus OrderService in CustomerPricingPolicy oder Customer, abhängig von der fachlichen Invariante.
Teststrategie
Vorher-/Nachher-Contract-Tests und Architekturtests sichern Verhalten sowie erlaubte Paketabhängigkeiten.
CustomerRiskPolicy.java
CustomerRiskPolicy.java
package com.aydinsude.workbench.catalog;
// Refactoring: Move Function
// Zweck: Die Risikoberechnung liegt beim fachlichen Profil statt in einem fremden Service.
public final class CustomerRiskPolicy {
public Risk assess(CustomerProfile profile) { return profile.risk(); }
public record CustomerProfile(int overdueInvoices, boolean politicallyExposed) {
public Risk risk() { return politicallyExposed || overdueInvoices > 2 ? Risk.HIGH : Risk.NORMAL; }
}
public enum Risk { NORMAL, HIGH }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein Feld befindet sich in einer Klasse, die weder seine Invarianten noch den größten Teil seiner Nutzung besitzt.
Erkennungsmerkmale
Andere Klassen lesen oder ändern den Wert häufiger; der Wert wird ständig weitergereicht oder bildet zusammen mit fremden Daten eine Einheit.
Nutzen der Transformation
Datenhoheit wird klarer und fachliche Regeln können am richtigen Ort geschützt werden.
Sicherer Refactoring-Ablauf
Nutzung und Eigentümerschaft klären.
Feld im Zieltyp einführen und synchron halten.
Leser und Schreiber schrittweise migrieren.
Altes Feld entfernen und Persistenzmapping anpassen.
Einsatzgrenze
Nicht verschieben, wenn der Wert Teil einer anderen Aggregate-Grenze ist und dadurch unerlaubte direkte Kopplung entstehen würde.
Risiken und Fehlanwendungen
Bei persistierten Systemen müssen Migration, Serialisierung und Rückwärtskompatibilität berücksichtigt werden.
Verwandte Techniken
Verwandt mit Move Function, Encapsulate Record und Aggregate Design.
Enterprise-Java-Einordnung
creditLimit wandert aus CustomerProfile in CreditAccount, weil dort Freigaben und Ausschöpfung verwaltet werden.
Teststrategie
Tests prüfen Datenmigration, Serialisierung und alle Invarianten am neuen Eigentümer.
ContractRenewalDate.java
ContractRenewalDate.java
package com.aydinsude.workbench.catalog;
import java.time.LocalDate;
// Refactoring: Move Field
// Zweck: Das Verlängerungsdatum gehört zum Vertragszeitraum.
public record ContractRenewalDate(String contractId, Term term) {
public record Term(LocalDate startsOn, LocalDate renewsOn) {
public boolean renewalDue(LocalDate today) { return !renewsOn.isAfter(today); }
}
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Vor oder nach einem Funktionsaufruf stehen an mehreren Stellen dieselben vorbereitenden oder abschließenden Anweisungen. Der tatsächliche Vertrag der Funktion ist unvollständig.
Erkennungsmerkmale
Wiederkehrende Validierung, Normalisierung, Logging- oder Abschlusslogik direkt an mehreren Aufrufstellen weist darauf hin.
Nutzen der Transformation
Der Funktionsvertrag bildet den vollständigen Vorgang ab und Aufrufer können keinen notwendigen Schritt vergessen.
Sicherer Refactoring-Ablauf
Gemeinsame Statements und ihre Reihenfolge sichern.
Seiteneffekte und Fehlerfälle analysieren.
Statements in die Funktion verschieben.
Aufrufstellen vereinfachen und Duplikate entfernen.
Einsatzgrenze
Nicht anwenden, wenn einzelne Aufrufer bewusst unterschiedliche Transaktions-, Logging- oder Sicherheitsgrenzen besitzen.
Risiken und Fehlanwendungen
Technische Querschnittsbelange sollten eventuell als Decorator oder Interceptor modelliert werden, nicht in jede Fachfunktion wandern.
Verwandte Techniken
Verwandt mit Move Statements to Callers, Extract Function und Decorator.
Enterprise-Java-Einordnung
Eine normalize-and-validate-Sequenz wird Bestandteil von registerCustomer(...), weil jede Registrierung sie zwingend benötigt.
Teststrategie
Tests prüfen, dass alle bisherigen Aufrufvarianten weiterhin normalisiert und validiert werden.
InvoiceDispatchPreparation.java
InvoiceDispatchPreparation.java
package com.aydinsude.workbench.catalog;
// Refactoring: Move Statements into Function
// Template Method: Vorbereitung und Versand bilden einen stabilen Ablauf.
public final class InvoiceDispatchPreparation {
public String dispatch(String invoiceId) { return prepare(invoiceId) + ":sent"; }
private String prepare(String invoiceId) { return "validated:" + invoiceId; }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Funktion enthält Schritte, die nur ein Teil der Aufrufer benötigt. Ihr allgemeiner Vertrag wird dadurch überraschend oder zu spezialisiert.
Erkennungsmerkmale
Bedingungen auf Aufruferart, optionale Seiteneffekte oder spezielle Formatierung am Ende einer allgemein benannten Funktion sind Hinweise.
Nutzen der Transformation
Der gemeinsame Vertrag wird kleiner; Spezialfälle bleiben an der Stelle sichtbar, die sie tatsächlich benötigt.
Sicherer Refactoring-Ablauf
Gemeinsamen Kern und aufruferspezifische Schritte trennen.
Speziallogik an betroffene Aufrufer verschieben.
Gemeinsame Funktion neu benennen oder vereinfachen.
Duplikate nur dort extrahieren, wo ein echter Begriff besteht.
Einsatzgrenze
Nicht anwenden, wenn dadurch ein obligatorischer Schritt leicht vergessen werden kann oder Sicherheitsregeln auf viele Aufrufer verteilt werden.
Risiken und Fehlanwendungen
Das Verschieben darf keine fachliche Invariante aus dem Aggregate herauslösen.
Verwandte Techniken
Verwandt mit Split Phase, Command und Application Service.
Enterprise-Java-Einordnung
generateInvoice() erzeugt nur das Dokument; Versand und Archivierung werden von den jeweiligen Anwendungsfällen orchestriert.
Teststrategie
Contract-Tests sichern den Kern; spezifische Aufrufertests prüfen Versand und Archivierung getrennt.
CustomerNotificationTemplate.java
CustomerNotificationTemplate.java
package com.aydinsude.workbench.catalog;
// Refactoring: Move Statements to Callers
// Strategy am Aufrufer: Der gemeinsame Text bleibt zentral, der Kanal ergänzt seine Variante.
public final class CustomerNotificationTemplate {
public String baseMessage(String customerId) { return "customer:" + customerId; }
public String email(String customerId) { return baseMessage(customerId) + ":email-footer"; }
public String sms(String customerId) { return baseMessage(customerId) + ":sms-short"; }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ein Typcode steuert zahlreiche Verzweigungen und beschreibt Varianten mit unterschiedlichem Verhalten. Neue Varianten erfordern Änderungen an vielen switch- oder if-Stellen.
Erkennungsmerkmale
Wiederkehrende Abfragen desselben Codes, variantenspezifische Felder und ungültige Kombinationen sind typische Hinweise.
Nutzen der Transformation
Varianten besitzen eigene Regeln, ungültige Zustände werden schwerer erzeugbar und Erweiterungen betreffen weniger Stellen.
Sicherer Refactoring-Ablauf
Varianten und gemeinsame Invarianten erfassen.
Eine gemeinsame Abstraktion einführen.
Je Variante Unterklasse oder versiegelten Typ anlegen.
Erzeugung zentralisieren und Verzweigungen schrittweise durch Polymorphie ersetzen.
Einsatzgrenze
Nicht anwenden, wenn der Code nur Daten klassifiziert, Varianten kein eigenes Verhalten besitzen oder sich Kombinationen dynamisch überlagern.
Risiken und Fehlanwendungen
Eine tiefe Vererbungshierarchie kann das Problem verschärfen. In Java sind sealed interfaces, Strategy oder Composition oft geeigneter.
Verwandte Techniken
Verwandt mit Replace Conditional with Polymorphism, State und Strategy.
Enterprise-Java-Einordnung
PaymentMethod wird als versiegelte Hierarchie CardPayment, BankTransfer und InvoicePayment modelliert.
Teststrategie
Contract-Tests gelten für alle Varianten; zusätzliche Tests prüfen variantenspezifische Invarianten.
PaymentMethodHierarchy.java
PaymentMethodHierarchy.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Type Code with Subclasses
// Pattern: Polymorphism
public sealed interface PaymentMethodHierarchy permits PaymentMethodHierarchy.Card, PaymentMethodHierarchy.Transfer {
BigDecimal fee(BigDecimal amount);
record Card() implements PaymentMethodHierarchy { public BigDecimal fee(BigDecimal amount){ return amount.multiply(new BigDecimal("0.02")); } }
record Transfer() implements PaymentMethodHierarchy { public BigDecimal fee(BigDecimal amount){ return new BigDecimal("0.50"); } }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Unterklasse unterscheidet sich kaum noch von ihrer Oberklasse oder trägt nur einen einzelnen Datenwert. Die Hierarchie erzeugt mehr Navigation als Bedeutung.
Erkennungsmerkmale
Leere Überschreibungen, nur ein zusätzliches Feld oder Unterklassen mit identischem Verhalten weisen darauf hin.
Nutzen der Transformation
Die Hierarchie wird flacher und der Typbaum entspricht wieder echten substituierbaren Varianten.
Sicherer Refactoring-Ablauf
Öffentliche Unterschiede und Instanziierungen erfassen.
Unterscheidungsdaten in die Oberklasse oder ein Value Object überführen.
Factory und Aufrufer migrieren.
Unterklasse entfernen und Verträge vereinfachen.
Einsatzgrenze
Nicht anwenden, wenn die Unterklasse einen stabilen Fachbegriff, eigene Invarianten oder zukünftige unabhängige Entwicklung besitzt.
Risiken und Fehlanwendungen
Alle Unterschiede wahllos in Flags zu verwandeln führt zurück zu Typcode-Verzweigungen.
Verwandte Techniken
Verwandt mit Collapse Hierarchy, Replace Subclass with Delegate und Introduce Special Case.
Enterprise-Java-Einordnung
DomesticCustomer und InternationalCustomer werden zu Customer mit einer geprüften MarketRegion, sofern das Verhalten identisch geworden ist.
Teststrategie
Tests vergleichen Serialisierung, Factory-Auswahl und alle bisherigen Varianten.
UnifiedCustomerAccount.java
UnifiedCustomerAccount.java
package com.aydinsude.workbench.catalog;
// Refactoring: Remove Subclass
// Value Object: Der Kontotyp ist Datenwert statt leerer Unterklasse.
public record UnifiedCustomerAccount(String id, AccountType type) {
public boolean business() { return type == AccountType.BUSINESS; }
public enum AccountType { PRIVATE, BUSINESS }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Mehrere Klassen besitzen dieselben Felder, Methoden und Invarianten, ohne dass diese gemeinsame Struktur ausdrücklich modelliert ist.
Erkennungsmerkmale
Duplizierte Implementierungen mit gleicher Bedeutung und substituierbare Objekte deuten auf eine gemeinsame Oberklasse hin.
Nutzen der Transformation
Duplikation sinkt und ein gemeinsamer, typisierter Vertrag wird sichtbar.
Sicherer Refactoring-Ablauf
Gemeinsamen Vertrag und echte Ist-ein-Beziehung prüfen.
Identische Methoden schrittweise hochziehen.
Gemeinsame Felder kapseln und Konstruktion vereinheitlichen.
Substituierbarkeit mit Contract-Tests sichern.
Einsatzgrenze
Nicht anwenden, wenn Gemeinsamkeit nur technisch ist oder Varianten nicht substituierbar sind. Dann sind Interface, Delegation oder Hilfsobjekt besser.
Risiken und Fehlanwendungen
Eine Oberklasse darf keine optionalen Felder und leeren Hooks sammeln. Das verletzt meist das Substitutionsprinzip.
Verwandte Techniken
Verwandt mit Pull Up Method, Extract Interface und Template Method.
Enterprise-Java-Einordnung
EmailNotification und SmsNotification können eine abstrakte Notification mit gemeinsamer Empfänger- und Auditlogik erhalten, sofern ihre Verträge übereinstimmen.
Teststrategie
Contract-Tests prüfen die Oberklasseninvarianten für jede Unterklasse.
AuditedOperation.java
AuditedOperation.java
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Extract Superclass
// Template Method: Gemeinsames Audit-Gerüst, fachliche Aktion bleibt abstrakt.
public abstract class AuditedOperation {
public final Result execute(String actor) { return new Result(actor,Instant.EPOCH,perform()); }
protected abstract String perform();
public record Result(String actor, Instant at, String value) {}
public static final class CloseOrder extends AuditedOperation { protected String perform(){ return "ORDER_CLOSED"; } }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Ober- und Unterklasse unterscheiden sich fachlich nicht mehr. Die Hierarchie erschwert Navigation, Konstruktion und Persistenzmapping ohne Nutzen.
Erkennungsmerkmale
Keine relevante Überschreibung, identische Invarianten und eine Eins-zu-eins-Nutzung der Typen sind Hinweise.
Nutzen der Transformation
Die Modellstruktur wird einfacher und vermeidet künstliche Abstraktionsschichten.
Sicherer Refactoring-Ablauf
Substituierbarkeit und Nutzung beider Typen prüfen.
Felder und Methoden in den verbleibenden Typ verschieben.
Factories, Serialisierung und Persistenz migrieren.
Überflüssigen Typ entfernen und API-Kompatibilität behandeln.
Einsatzgrenze
Nicht anwenden, wenn die Hierarchie eine veröffentlichte API oder bewusst getrennte Fachbegriffe repräsentiert.
Risiken und Fehlanwendungen
Ein vorschnelles Zusammenlegen kann spätere unabhängige Entwicklung erschweren; Entscheidung und Migrationspfad dokumentieren.
Verwandte Techniken
Verwandt mit Remove Subclass, Inline Class und Replace Inheritance with Delegation.
Enterprise-Java-Einordnung
BaseContract und Contract werden zusammengeführt, nachdem keine alternative Unterklasse und kein abweichendes Verhalten mehr existiert.
Teststrategie
Regressionstests sichern JSON-, Datenbank- und API-Kompatibilität.
CollapsedApprovalRule.java
CollapsedApprovalRule.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Collapse Hierarchy
// Zweck: Eine unnötige Vererbungsebene wird entfernt.
public final class CollapsedApprovalRule {
private final BigDecimal limit;
public CollapsedApprovalRule(BigDecimal limit){ this.limit=limit; }
public boolean approves(BigDecimal amount){ return amount.compareTo(limit) <= 0; }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Vererbung wird nur verwendet, um eine optionale Fähigkeit oder einzelne Variante zu erhalten. Kombinationen sind nicht möglich und die Basisklasse wird durch Unterklassenwissen belastet.
Erkennungsmerkmale
Viele kleine Unterklassen, Mehrfachkombinationen oder Überschreibungen, die nur an ein Hilfsobjekt delegieren könnten, sind Hinweise.
Nutzen der Transformation
Verhalten lässt sich kombinieren und zur Laufzeit austauschen; die Hauptklasse bleibt stabil.
Sicherer Refactoring-Ablauf
Variantenspezifisches Verhalten in ein Delegate-Interface extrahieren.
Delegate in der bisherigen Basisklasse halten.
Unterklassen schrittweise durch konfigurierte Instanzen ersetzen.
Hierarchie entfernen und Erzeugung zentralisieren.
Einsatzgrenze
Nicht anwenden, wenn eine echte Ist-ein-Beziehung mit gemeinsamem Vertrag und starken Invarianten besteht.
Risiken und Fehlanwendungen
Ein Delegate ohne klare Verantwortung kann zu beliebiger Strategie-Injektion führen. Seine Schnittstelle muss fachlich eng bleiben.
Verwandte Techniken
Verwandt mit Strategy, State, Decorator und Favor Composition over Inheritance.
Enterprise-Java-Einordnung
PremiumOrder-Unterklasse wird durch PricingPolicy und FulfillmentPolicy ersetzt, die unabhängig kombinierbar sind.
Teststrategie
Contract-Tests prüfen alle Delegate-Implementierungen und ausgewählte Kombinationen.
DeliveryServiceDelegate.java
DeliveryServiceDelegate.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Subclass with Delegate
// Pattern: Strategy/Delegation
public final class DeliveryServiceDelegate {
private final RoutePolicy policy;
public DeliveryServiceDelegate(RoutePolicy policy){ this.policy=policy; }
public String route(String destination){ return policy.route(destination); }
public interface RoutePolicy { String route(String destination); }
public static final class Express implements RoutePolicy { public String route(String d){ return "EXPRESS:"+d; } }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Eine Klasse erbt von einer technischen oder fachfremden Oberklasse nur, um deren Funktionen zu nutzen. Dadurch wird ein falscher Ist-ein-Vertrag veröffentlicht.
Erkennungsmerkmale
Aufrufer sehen geerbte Methoden, die nicht zum Fachtyp gehören; Überschreibungen blockieren Verhalten oder die Oberklasse kann nicht unabhängig gewechselt werden.
Nutzen der Transformation
Der öffentliche Typvertrag entspricht wieder der Fachdomäne; technische Implementierung kann ausgetauscht werden.
Sicherer Refactoring-Ablauf
Tatsächlich benötigte Funktionen der Oberklasse erfassen.
Ein fokussiertes Delegate-Feld und eine schmale Schnittstelle einführen.
Aufrufe auf Delegation umstellen.
Vererbung entfernen und nicht passende API verschwinden lassen.
Einsatzgrenze
Nicht anwenden, wenn die Unterklasse vollständig substituierbar ist und die Oberklasse tatsächlich den stabilen Fachvertrag definiert.
Risiken und Fehlanwendungen
Bloße Eins-zu-eins-Weiterleitung kann eine unnötige Mittelschicht erzeugen. Nur fachlich benötigte Operationen sollten angeboten werden.
Verwandte Techniken
Verwandt mit Encapsulate Collection, Adapter und Replace Inheritance with Delegation.
Enterprise-Java-Einordnung
CustomerReport erbt nicht mehr von ArrayList, sondern besitzt eine List<ReportRow> und bietet nur fachliche Operationen an.
Teststrategie
Tests prüfen, dass nur erlaubte Operationen verfügbar sind und Delegatwechsel dasselbe Fachverhalten liefern.
CustomerScoringDelegate.java
CustomerScoringDelegate.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Superclass with Delegate
// Pattern: Delegation over inheritance
public final class CustomerScoringDelegate {
private final ScoreNormalizer normalizer;
public CustomerScoringDelegate(ScoreNormalizer normalizer){ this.normalizer=normalizer; }
public int score(int raw){ return normalizer.normalize(raw); }
public static final class ScoreNormalizer { public int normalize(int raw){ return Math.max(0,Math.min(100,raw)); } }
}
Deutschsprachige fachliche Grundlagen
Die Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das vorhandene Java-Beispiel übertragen. Die Quellen dienen als fachliche Grundlage; der Text ist keine Übersetzung eines englischen Eintrags.
Kapitel 5 Primitive Werte durch Fachtypen ersetzen10 Refactorings
Datenkapselung, Bedingungen und Kontrollfluss mit kompilierbarem Java-21-Zielcode.
41. Replace Primitive with Object
Fachliches Ausgangsproblem
Ein fachlich bedeutsamer Wert wird nur als String, Zahl oder Boolean weitergereicht. Dadurch liegen Validierung, Formatierung und Regeln an vielen Aufrufstellen verteilt.
Erkennungsmerkmale
Viele Parameter gleichen primitiven Typs, wiederholte Prüfungen, unklare Einheiten und Methoden wie calculate(double amount, String currency).
Nutzen der Transformation
Ein eigener Werttyp bündelt Bedeutung, Gültigkeit und Verhalten. Der Compiler verhindert Verwechslungen und Fachregeln erhalten einen eindeutigen Ort.
Sicherer Refactoring-Ablauf
Money, EmailAddress oder CustomerNumber als unveränderliches Value Object einführen; zunächst Konstruktion absichern, dann Signaturen schrittweise umstellen.
Einsatzgrenze
Nicht für rein technische, lokal begrenzte Werte ohne fachliche Semantik. Ein Typ pro beliebiger Zahl erzeugt unnötige Zerlegung.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Value Object, Encapsulate Variable, Change Function Declaration
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Primitive with Object nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
MoneyValue.java
MoneyValue.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
import java.util.Objects;
// Refactoring: Replace Primitive with Object
// Value Object: Money bündelt Betrag, Währung und fachliche Invarianten.
public record MoneyValue(BigDecimal amount, String currency) {
public MoneyValue {
Objects.requireNonNull(amount); Objects.requireNonNull(currency);
if (currency.isBlank()) throw new IllegalArgumentException("currency");
}
public MoneyValue add(MoneyValue other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
return new MoneyValue(amount.add(other.amount), currency);
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Ein temporäres Ergebnis wird gespeichert und später mehrfach verwendet. Die Variable verlängert den Zustand der Methode und erschwert das Zerlegen in kleinere Funktionen.
Erkennungsmerkmale
Temporäre Variablen werden weit entfernt von ihrer Berechnung gelesen; Änderungen der Berechnungsgrundlage erfordern manuelle Synchronisierung.
Nutzen der Transformation
Eine seiteneffektfreie Abfrage macht die Berechnung an jedem Verwendungspunkt sichtbar und erleichtert Extract Method.
Sicherer Refactoring-Ablauf
Berechnung in eine kleine Query extrahieren, Tests sichern, temporäre Variable ersetzen und erst danach weitere Methoden abtrennen.
Einsatzgrenze
Nicht anwenden, wenn die Berechnung teuer, nicht deterministisch oder seiteneffektbehaftet ist. Dann ist ein bewusstes Snapshot-Objekt besser.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Extract Method, Memoization, Replace Derived Variable with Query
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Temp with Query nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
OrderNetAmountQuery.java
OrderNetAmountQuery.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Temp with Query
// Query Object: Die Berechnung ist benannt, wiederverwendbar und ohne Seiteneffekt.
public final class OrderNetAmountQuery {
public BigDecimal net(BigDecimal gross, BigDecimal discount) { return gross.subtract(discount); }
public boolean requiresApproval(BigDecimal gross, BigDecimal discount) { return net(gross,discount).compareTo(new BigDecimal("1000")) > 0; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine Map, ein Array oder ein frei veränderbarer Datenträger repräsentiert ein Fachobjekt. Schlüssel, Pflichtfelder und Invarianten bleiben implizit.
Erkennungsmerkmale
String-Schlüssel an vielen Stellen, Casts, fehlende Felder erst zur Laufzeit und direkte Mutation durch beliebige Aufrufer.
Nutzen der Transformation
Ein typisierter Record oder eine Klasse macht Struktur und Bedeutung explizit und bietet einen kontrollierten Migrationspunkt.
Sicherer Refactoring-Ablauf
Zunächst lesenden Wrapper einführen, dann Erzeugung zentralisieren, Schreibzugriffe in Methoden überführen und rohe Struktur aus der API entfernen.
Einsatzgrenze
Für echte dynamische Dokumente mit bewusst offenem Schema kann eine Map korrekt sein; dann sollte zumindest eine Validierungsgrenze existieren.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Replace Array with Object, Data Mapper, Value Object
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Encapsulate Record nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
CustomerRecord.java
CustomerRecord.java
package com.aydinsude.workbench.catalog;
import java.util.Map;
// Refactoring: Encapsulate Record
// Facade: Benannte Fachzugriffe verbergen die untypisierte Legacy-Struktur.
public final class CustomerRecord {
private final Map<String,String> data;
public CustomerRecord(Map<String,String> data){ this.data=Map.copyOf(data); }
public String customerId(){ return required("customerId"); }
public String email(){ return required("email"); }
private String required(String key){ var value=data.get(key); if(value==null||value.isBlank()) throw new IllegalStateException(key); return value; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine interne Collection wird direkt herausgegeben. Aufrufer können Elemente hinzufügen, entfernen oder die Reihenfolge verändern und umgehen dabei Fachregeln.
Erkennungsmerkmale
Getter liefern modifizierbare Listen; add/remove geschieht außerhalb des Aggregats; Duplikate und Grenzen werden inkonsistent behandelt.
Nutzen der Transformation
Das besitzende Objekt kontrolliert Mutation und kann Invarianten, Ereignisse und Kopien zuverlässig verwalten.
Sicherer Refactoring-Ablauf
Defensive Sicht zurückgeben, fachliche add/remove-Methoden einführen, Aufrufer migrieren und direkte Collection-Mutation sperren.
Einsatzgrenze
Nicht jede Liste braucht ein reiches Domänenobjekt. Bei reinem Transfer genügt eine unveränderliche Kopie.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Aggregate, Domain Collection, Defensive Copy
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Encapsulate Collection nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
OrderLineCollection.java
OrderLineCollection.java
package com.aydinsude.workbench.catalog;
import java.util.ArrayList; import java.util.List;
// Refactoring: Encapsulate Collection
// Aggregate: Änderungen an Positionen laufen ausschließlich über die Aggregate-Grenze.
public final class OrderLineCollection {
private final List<String> lines=new ArrayList<>();
public void add(String sku){ if(sku==null||sku.isBlank()) throw new IllegalArgumentException("sku"); lines.add(sku); }
public List<String> lines(){ return List.copyOf(lines); }
}
Java-21-Zielcode.java
Java-21-Zielcode.java
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class OrderLineCollectionTest { @Test void collection_is_protected(){ var o=new OrderLineCollection(); o.add("SKU-1"); assertThrows(UnsupportedOperationException.class,()->o.lines().add("SKU-2")); } }
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine benannte Konstante oder ein Value Object dokumentiert die Regel und verhindert uneinheitliche Anpassungen.
Sicherer Refactoring-Ablauf
Bedeutung klären, Vorkommen fachlich unterscheiden, Konstante nahe am Besitzer definieren und Tests für Grenzwerte ergänzen.
Einsatzgrenze
Konstanten sind ungeeignet für konfigurierbare oder zeitabhängige Regeln. Dann Policy oder Configuration Snapshot einsetzen.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Introduce Value Object, Extract Policy, Replace Primitive with Object
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Magic Literal with Symbolic Constant nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
ApprovalThreshold.java
ApprovalThreshold.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Magic Literal with Symbolic Constant
// Policy: Der Schwellenwert besitzt einen fachlichen Namen und eine zentrale Heimat.
public final class ApprovalThreshold {
public static final BigDecimal MANUAL_APPROVAL_LIMIT=new BigDecimal("10000.00");
public boolean manualApprovalRequired(BigDecimal amount){ return amount.compareTo(MANUAL_APPROVAL_LIMIT)>0; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Der Code setzt eine interne Bedingung voraus, dokumentiert sie aber nicht. Fehler zeigen sich später an einer fachlich entfernten Stelle.
Erkennungsmerkmale
Implizite Vorbedingungen, rechnerisch unmögliche Zustände und Kommentare wie 'kann nie null sein'.
Nutzen der Transformation
Eine Assertion macht die interne Annahme ausführbar und lokalisiert Programmierfehler früh.
Sicherer Refactoring-Ablauf
Invariante identifizieren, Assertion an der Entstehungs- oder Nutzungskante platzieren und fachliche Eingabefehler weiterhin regulär behandeln.
Einsatzgrenze
Assertions ersetzen keine Validierung externer Daten und keine fachlichen Ablehnungen. Sie dürfen nicht Teil normaler Kontrollflüsse werden.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Guard Clause, Bean Validation, Design by Contract
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Introduce Assertion nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
SettlementInvariant.java
SettlementInvariant.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Introduce Assertion
// Invariant: Settlement darf nur für positive, bereits abgeglichene Beträge entstehen.
public final class SettlementInvariant {
public String settle(BigDecimal amount, boolean reconciled){
if(amount.signum()<=0) throw new IllegalStateException("amount must be positive");
if(!reconciled) throw new IllegalStateException("payment must be reconciled");
return "SETTLED:"+amount;
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Bei gemeinsamem Abschluss oder notwendiger Ressourcenfreigabe darf die Methode nicht unkontrolliert fragmentiert werden.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Decompose Conditional, Introduce Special Case, Extract Method
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Nested Conditional with Guard Clauses nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
ClaimEligibilityGuards.java
ClaimEligibilityGuards.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Nested Conditional with Guard Clauses
// Specification-artige Guards: Jeder Ablehnungsgrund ist früh und separat sichtbar.
public final class ClaimEligibilityGuards {
public String decide(boolean activePolicy, boolean completeDocuments, int amount){
if(!activePolicy) return "REJECTED_POLICY";
if(!completeDocuments) return "REJECTED_DOCUMENTS";
if(amount<=0) return "REJECTED_AMOUNT";
return "ACCEPTED";
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Ein wachsender switch oder if-Block wählt Verhalten nach Typ, Status oder Kanal. Jede neue Variante verändert dieselbe zentrale Stelle.
Erkennungsmerkmale
Wiederholte Typprüfungen in mehreren Methoden, parallele switch-Blöcke und hohe Änderungsrate bei neuen Varianten.
Nutzen der Transformation
Polymorphe Typen oder Handler kapseln variierendes Verhalten; der Aufrufer arbeitet gegen einen stabilen Vertrag.
Sicherer Refactoring-Ablauf
Variationsachse bestimmen, Interface extrahieren, eine Variante nach der anderen verschieben und Auswahl in Factory oder Registry isolieren.
Einsatzgrenze
Bei wenigen stabilen Fällen kann ein klarer switch verständlicher sein. Polymorphie ist kein Selbstzweck und darf Datenvarianten nicht künstlich aufblasen.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Strategy, State, Factory, Replace Type Code with Subclasses
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Conditional with Polymorphism nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
DeliveryPricingPolymorphism.java
DeliveryPricingPolymorphism.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Conditional with Polymorphism
// Design Pattern: Strategy - Versandpreise sind austauschbare Varianten.
public final class DeliveryPricingPolymorphism {
public BigDecimal price(DeliveryPolicy policy, BigDecimal base){ return policy.price(base); }
public sealed interface DeliveryPolicy permits Standard, Express { BigDecimal price(BigDecimal base); }
public record Standard() implements DeliveryPolicy { public BigDecimal price(BigDecimal base){ return base.add(new BigDecimal("4.90")); } }
public record Express() implements DeliveryPolicy { public BigDecimal price(BigDecimal base){ return base.add(new BigDecimal("14.90")); } }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Ein besonderer Wert wie null, UNKNOWN oder GUEST führt an vielen Stellen zu denselben Prüfungen und Standardreaktionen.
Erkennungsmerkmale
Wiederholte Null- oder Sonderwertprüfungen, Standardtexte und alternative Berechnungen über mehrere Klassen verteilt.
Nutzen der Transformation
Ein Special-Case-Objekt kapselt das abweichende Verhalten und lässt Aufrufer den normalen Vertrag verwenden.
Sicherer Refactoring-Ablauf
Gemeinsame Sonderfalllogik sammeln, passendes Objekt einführen, Erzeugung zentralisieren und Prüfungen schrittweise entfernen.
Einsatzgrenze
Wenn das Fehlen eines Werts fachlich relevant und explizit behandelt werden muss, ist Optional oder Result oft ehrlicher als ein scheinbar normales Objekt.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Null Object, Optional, Result Type
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Introduce Special Case nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
CustomerContactSpecialCase.java
CustomerContactSpecialCase.java
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Special Case
// Design Pattern: Null Object - anonyme Kunden besitzen explizites Standardverhalten.
public sealed interface CustomerContactSpecialCase permits CustomerContactSpecialCase.Known, CustomerContactSpecialCase.Anonymous {
String displayName(); boolean contactable();
record Known(String displayName, String email) implements CustomerContactSpecialCase { public boolean contactable(){ return email!=null&&!email.isBlank(); } }
record Anonymous() implements CustomerContactSpecialCase { public String displayName(){ return "Anonymous"; } public boolean contactable(){ return false; } }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine boolesche Variable steuert, ob eine Schleife oder ein Zweig weiterläuft. Der eigentliche Abbruchgrund wird dadurch indirekt dargestellt.
Erkennungsmerkmale
Flags wie found, done oder continueProcessing werden an mehreren Stellen gesetzt und später geprüft.
Nutzen der Transformation
Direkter break, continue oder return bildet die Kontrollabsicht klarer ab und verkürzt den Gültigkeitsbereich von Zustand.
Sicherer Refactoring-Ablauf
Flag-Verwendungen analysieren, einfachen Abbruch ersetzen, komplexe Schleife gegebenenfalls in eine benannte Suchfunktion extrahieren.
Einsatzgrenze
Nicht anwenden, wenn nach der Schleife der Abbruchgrund benötigt wird; dann ein typisiertes Ergebnis statt eines nackten Flags zurückgeben.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Replace Loop with Pipeline, Extract Method, Decision Object
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Control Flag with Break nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
FraudSignalSearch.java
FraudSignalSearch.java
package com.aydinsude.workbench.catalog;
import java.util.List;
// Refactoring: Replace Control Flag with Break
// Zweck: Der Abbruch ist direkt am fachlichen Treffer sichtbar.
public final class FraudSignalSearch {
public String firstBlocked(List<String> signals){
for(String signal:signals){ if(signal.startsWith("BLOCK:")) return signal.substring(6); }
return "NONE";
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Kapitel 6 Klassen extrahieren und Verantwortungen schneiden10 Refactorings
Klassen-, Delegations-, Algorithmus- und Conditional-Refactorings mit kompilierbarem Java-21-Zielcode.
51. Extract Class
Fachliches Ausgangsproblem
Eine Klasse trägt mehrere voneinander unabhängige Verantwortlichkeiten und ändert sich aus unterschiedlichen Gründen.
Erkennungsmerkmale
Viele Feldergruppen, Methodencluster, häufige Teiländerungen und Namen wie Manager, Processor oder Service mit breitem Auftrag.
Nutzen der Transformation
Eine neue Klasse schafft Kohäsion, begrenzt Abhängigkeiten und macht Verantwortlichkeiten separat testbar.
Sicherer Refactoring-Ablauf
Methoden- und Feldcluster identifizieren, neue Klasse einführen, delegieren, Aufrufer migrieren und Schnittstelle anschließend verengen.
Einsatzgrenze
Zu frühes Extrahieren kann anämische Kleinstklassen und Weiterleitungsketten erzeugen. Verantwortlichkeit muss fachlich oder technisch eigenständig sein.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Single Responsibility, Move Function, Extract Service
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Extract Class nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
CustomerCreditProfile.java
CustomerCreditProfile.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Extract Class
// Design Pattern: Value Object - Kreditdaten und Invarianten bilden eine eigene Fachgrenze.
public record CustomerCreditProfile(BigDecimal limit, BigDecimal exposure) {
public CustomerCreditProfile { if(limit.signum()<0||exposure.signum()<0) throw new IllegalArgumentException("negative credit values"); }
public BigDecimal available(){ return limit.subtract(exposure); }
public boolean canReserve(BigDecimal amount){ return amount.signum()>0 && available().compareTo(amount)>=0; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine Klasse besitzt keine eigenständige Verantwortung mehr und dient nur noch als dünne Hülle oder Datenablage.
Erkennungsmerkmale
Fast alle Methoden delegieren, Lebenszyklus ist identisch mit einer anderen Klasse und Änderungen erfolgen stets gemeinsam.
Nutzen der Transformation
Das Zusammenführen reduziert Indirektion und macht das Modell wieder proportional zur tatsächlichen Komplexität.
Sicherer Refactoring-Ablauf
Öffentliche Nutzung prüfen, Verhalten und Daten in Besitzer verschieben, Aufrufer umstellen und überflüssigen Typ entfernen.
Einsatzgrenze
Nicht inlinen, wenn die Klasse eine stabile Grenze, ein Sicherheitskonzept oder einen zukünftigen unabhängigen Lebenszyklus repräsentiert.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
In einer Enterprise-Anwendung wird Inline Class nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
ShippingLabel.java
ShippingLabel.java
package com.aydinsude.workbench.catalog;
// Refactoring: Inline Class
// Value Object: Die kleine Adresshülle ist direkt Teil des Versandetiketts.
public record ShippingLabel(String recipient, String street, String city) {
public ShippingLabel { if(recipient.isBlank()||street.isBlank()||city.isBlank()) throw new IllegalArgumentException("complete address required"); }
public String printable(){ return recipient+" | "+street+" | "+city; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Aufrufer navigieren durch Objektketten und kennen interne Beziehungen, etwa order.customer().address().country().
Erkennungsmerkmale
Message Chains, wiederholte Navigationspfade und Änderungen der internen Struktur schlagen auf viele Clients durch.
Nutzen der Transformation
Das besitzende Objekt bietet eine fachliche Operation und verbirgt den Delegaten. Dadurch sinkt strukturelle Kopplung.
Sicherer Refactoring-Ablauf
Häufige Navigation identifizieren, fachlich benannte Methode am passenden Besitzer ergänzen und Clients schrittweise umstellen.
Einsatzgrenze
Eine vollständige Abschirmung kann zur überladenen Fassade führen. Nicht jede Navigation ist ein Problem; entscheidend ist die Stabilität der Grenze.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Law of Demeter, Facade, Remove Middle Man
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Hide Delegate nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
OrderCustomerFacade.java
OrderCustomerFacade.java
package com.aydinsude.workbench.catalog;
// Refactoring: Hide Delegate
// Facade: Order verbirgt die interne Customer-Contact-Kette.
public final class OrderCustomerFacade {
private final Customer customer;
public OrderCustomerFacade(Customer customer){ this.customer=customer; }
public String customerEmail(){ return customer.contact().email(); }
public record Customer(Contact contact) {}
public record Contact(String email) { public Contact { if(email==null||email.isBlank()) throw new IllegalArgumentException("email"); } }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine Klasse leitet fast jede Operation unverändert an einen Delegaten weiter. Die zusätzliche Ebene liefert keinen eigenen Vertrag mehr.
Erkennungsmerkmale
Viele triviale Einzeiler, parallele API zum Delegaten und häufige Anpassungen an beiden Klassen.
Nutzen der Transformation
Direkter Zugriff oder eine kleinere gezielte Grenze reduziert Boilerplate und macht Verantwortlichkeiten klarer.
Sicherer Refactoring-Ablauf
Weiterleitungen kategorisieren, fachlich wertvolle behalten, triviale Clients direkt anbinden und alte Methoden deprecaten.
Einsatzgrenze
Nicht entfernen, wenn die Ebene Sicherheit, Transaktion, Caching, Beobachtbarkeit oder eine stabile externe API kapselt.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Hide Delegate, Facade, Adapter
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Remove Middle Man nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
PaymentGatewayAccess.java
PaymentGatewayAccess.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Remove Middle Man
// Port and Adapter: Der Use Case arbeitet direkt gegen den stabilen PaymentPort.
public final class PaymentGatewayAccess {
private final PaymentPort port;
public PaymentGatewayAccess(PaymentPort port){ this.port=port; }
public String charge(String orderId, BigDecimal amount){ return port.charge(orderId,amount); }
public interface PaymentPort { String charge(String orderId, BigDecimal amount); }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Ein Algorithmus ist unnötig komplex, fehleranfällig oder nicht mehr passend, obwohl sein externer Vertrag stabil bleiben soll.
Erkennungsmerkmale
Viele Sonderfälle, schwer beweisbare Schleifen, bekannte Standardlösung vorhanden oder schlechte Performance bei typischen Daten.
Nutzen der Transformation
Ein klarerer Algorithmus verbessert Verständlichkeit und Wartbarkeit, ohne die öffentliche Schnittstelle zu verändern.
Sicherer Refactoring-Ablauf
Verhaltensbaseline und Randfälle sichern, neuen Algorithmus parallel implementieren, Ergebnisse vergleichen und kontrolliert umschalten.
Einsatzgrenze
Nicht ohne ausreichende Tests und Messdaten austauschen. Ein kürzerer Algorithmus ist nicht automatisch korrekter oder schneller.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Strategy, Differential Test, Performance Budget
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Substitute Algorithm nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
PrioritySortAlgorithm.java
PrioritySortAlgorithm.java
package com.aydinsude.workbench.catalog;
import java.util.Comparator; import java.util.List;
// Refactoring: Substitute Algorithm
// Strategy: Comparator beschreibt die Sortierregel explizit.
public final class PrioritySortAlgorithm {
public List<Ticket> sort(List<Ticket> tickets){ return tickets.stream().sorted(Comparator.comparingInt(Ticket::priority).reversed().thenComparing(Ticket::id)).toList(); }
public record Ticket(String id,int priority) {}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine komplexe Bedingung vermischt Entscheidung, Berechnung und Ergebnisbehandlung in einem großen Block.
Erkennungsmerkmale
Lange boolesche Ausdrücke, mehrfach verschachtelte Zweige und fachliche Regeln, die nur aus Operatoren erschlossen werden können.
Nutzen der Transformation
Benannte Methoden für Bedingung und Zweige machen die Fachentscheidung lesbar und separat testbar.
Sicherer Refactoring-Ablauf
Bedingung benennen, then/else-Blöcke extrahieren, gemeinsame Datenübergabe reduzieren und anschließend weitere Pattern prüfen.
Einsatzgrenze
Kleine offensichtliche Bedingungen sollten nicht in bedeutungslose Methoden zerlegt werden.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
In einer Enterprise-Anwendung wird Decompose Conditional nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Mehrere aufeinanderfolgende Bedingungen führen zum gleichen Ergebnis, werden aber getrennt geprüft.
Erkennungsmerkmale
Serien von if-return mit identischem Rückgabewert und schwer sichtbarer gemeinsamer fachlicher Bedeutung.
Nutzen der Transformation
Eine gemeinsame, benannte Bedingung zeigt, dass die Fälle dieselbe Entscheidung repräsentieren.
Sicherer Refactoring-Ablauf
Gleiches Ergebnis verifizieren, Bedingungen kombinieren, Ausdruck benennen und Wahrheitstabelle testen.
Einsatzgrenze
Nicht zusammenführen, wenn Bedingungen unterschiedliche fachliche Ursachen mit eigener Diagnose oder Auditierung darstellen.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Extract Method, Specification, Decision Table
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Consolidate Conditional Expression nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
EligibilityRules.java
EligibilityRules.java
package com.aydinsude.workbench.catalog;
// Refactoring: Consolidate Conditional Expression
// Specification: Die gesamte Zulässigkeit besitzt einen Namen und eine Testgrenze.
public final class EligibilityRules {
public boolean eligible(int age, boolean activeAccount, boolean sanctionsHit){ return hasMinimumAge(age)&&activeAccount&&!sanctionsHit; }
private boolean hasMinimumAge(int age){ return age>=18; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Gleicher Code steht am Anfang oder Ende beider Zweige eines Conditionals und verdeckt den tatsächlichen Unterschied.
Erkennungsmerkmale
Identische Aufrufe in then und else, duplizierte Nachbereitung oder wiederholte Ressourcenfreigabe.
Nutzen der Transformation
Gemeinsame Fragmente werden vor oder nach die Entscheidung gezogen; der Conditional zeigt nur noch die Variation.
Sicherer Refactoring-Ablauf
Duplikat auf Seiteneffekte prüfen, gemeinsame Reihenfolge sichern, Fragment verschieben und beide Pfade testen.
Einsatzgrenze
Nicht verschieben, wenn Ausführungshäufigkeit, Reihenfolge oder Transaktionskontext zwischen den Zweigen verschieden ist.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Slide Statements, Extract Method, Template Method
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Consolidate Duplicate Conditional Fragments nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
NotificationDispatch.java
NotificationDispatch.java
package com.aydinsude.workbench.catalog;
// Refactoring: Consolidate Duplicate Conditional Fragments
// Template Method-artiger Ablauf: Varianten unterscheiden nur den Kanal, Abschluss bleibt gemeinsam.
public final class NotificationDispatch {
public String dispatch(boolean urgent,String message){
String channel=urgent?"SMS":"EMAIL";
String sent=channel+":"+message;
return audit(sent);
}
private String audit(String sent){ return "AUDITED:"+sent; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine Methode meldet Fehler über Zahlen, Sonderwerte oder Booleans. Aufrufer können die Prüfung vergessen oder Bedeutung verwechseln.
Erkennungsmerkmale
Rückgabewerte wie -1, null oder false mit zusätzlichem lastError; Erfolg und Fehler teilen denselben Rückgabetyp.
Nutzen der Transformation
Eine typisierte Exception trennt außergewöhnlichen Fehlerpfad vom normalen Ergebnis und bewahrt Ursache sowie Kontext.
Sicherer Refactoring-Ablauf
Fehlerklassen definieren, Aufrufer einzeln migrieren, Ursache erhalten und API-Vertrag dokumentieren.
Einsatzgrenze
Erwartbare fachliche Ablehnungen sind oft Result oder Decision, nicht Exception. Exceptions dürfen keinen normalen Geschäftsfluss modellieren.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Exception Translation, Result Type, Replace Exception with Precheck
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Error Code with Exception nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
InventoryReservation.java
InventoryReservation.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Error Code with Exception
// Domain Exception: Fehlschlag transportiert fachlichen Kontext statt Zahlencode.
public final class InventoryReservation {
public String reserve(String sku,int requested,int available){
if(requested<=0) throw new IllegalArgumentException("requested");
if(available<requested) throw new InsufficientStockException(sku,requested,available);
return "RESERVED:"+sku+":"+requested;
}
public static final class InsufficientStockException extends RuntimeException {
public InsufficientStockException(String sku,int requested,int available){ super("sku="+sku+", requested="+requested+", available="+available); }
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Eine erwartbare, häufige Situation wird über eine Exception erkannt, obwohl sie vorab günstig und zuverlässig geprüft werden kann.
Erkennungsmerkmale
Exceptions bei normalem Ablauf, etwa fehlender Map-Schlüssel oder nicht vorhandene optionale Datei; auffällige Catch-Blöcke in Schleifen.
Nutzen der Transformation
Eine explizite Vorprüfung macht den normalen Kontrollfluss sichtbar und vermeidet unnötige Ausnahmebehandlung.
Sicherer Refactoring-Ablauf
Atomare oder konsistente Precheck-Möglichkeit prüfen, Bedingung extrahieren und Race Conditions berücksichtigen.
Einsatzgrenze
Nicht anwenden, wenn zwischen Prüfung und Aktion ein Zeitfenster entsteht oder die Operation selbst die einzige verlässliche Prüfung ist.
Risiken und Fehlanwendungen
Das Refactoring muss in kleinen, kompilierbaren Schritten erfolgen. Vor allem öffentliche Verträge, Nebenwirkungen, Transaktionsgrenzen und Nebenläufigkeit dürfen nicht stillschweigend verändert werden.
Verwandte Techniken
Guard Clause, Try-Operation, Result Type
Enterprise-Java-Einordnung
In einer Enterprise-Anwendung wird Replace Exception with Precheck nicht isoliert angewendet, sondern an einer klaren fachlichen oder technischen Grenze. Der vorhandene Java-21-Code zeigt eine kleine, kompilierbare Zielstruktur; in Spring oder Jakarta EE bleibt die Fachlogik dabei unabhängig vom Framework.
Teststrategie
Vor der Änderung sichern Characterization Tests das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfall, Grenzwerte und Fehlerpfade; bei öffentlichen APIs kommen Vertrags- oder Integrationstests hinzu.
DocumentLookup.java
DocumentLookup.java
package com.aydinsude.workbench.catalog;
import java.util.Map; import java.util.Optional;
// Refactoring: Replace Exception with Precheck
// Repository Query: Erwartbare Abwesenheit wird als Optional modelliert.
public final class DocumentLookup {
private final Map<String,String> documents;
public DocumentLookup(Map<String,String> documents){ this.documents=Map.copyOf(documents); }
public Optional<String> find(String id){ if(id==null||id.isBlank()) return Optional.empty(); return Optional.ofNullable(documents.get(id)); }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Die Quellen dienen als Grundlage; der Text ist keine wörtliche Übersetzung.
Kapitel 7 Variablen, Bedingungen und Kontrollfluss klären10 Refactorings
Variablen-, Feld-, Vererbungs- und Delegations-Refactorings mit kompilierbarem Java-21-Zielcode.
61. Split Variable
Fachliches Ausgangsproblem
Eine Variable wird während einer Methode für mehrere unterschiedliche Bedeutungen wiederverwendet. Dadurch ist nicht mehr erkennbar, welcher Wert zu welchem Verarbeitungsschritt gehört.
Erkennungsmerkmale
Mehrfache Zuweisungen mit wechselnder Semantik, Namen wie temp oder value und schwer nachvollziehbare Datenflüsse.
Nutzen der Transformation
Jede Bedeutung erhält eine eigene Variable. Dadurch werden Lebensdauer, Zweck und Abhängigkeiten sichtbar und weitere Extraktionen werden sicherer.
Sicherer Refactoring-Ablauf
Zuweisungen und Verwendungen gruppieren, für jede Bedeutung einen präzisen Namen einführen, schrittweise ersetzen und anschließend unnötige Mutationen entfernen.
Einsatzgrenze
Nicht jede Neuzuweisung ist problematisch. Schleifenzähler oder bewusst akkumulierte Werte brauchen keine künstliche Aufteilung.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Rename Variable, Extract Variable, Replace Temp with Query
Enterprise-Java-Einordnung
Split Variable wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
PricingCalculation.java
PricingCalculation.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Split Variable
// Value Object-orientiert: Jede Rechenstufe besitzt eine eindeutige Bedeutung.
public final class PricingCalculation {
public BigDecimal total(BigDecimal unitPrice,int quantity,BigDecimal discountRate){
BigDecimal gross=unitPrice.multiply(BigDecimal.valueOf(quantity));
BigDecimal discount=gross.multiply(discountRate);
BigDecimal net=gross.subtract(discount);
return net;
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Ein Feldname beschreibt seine fachliche Bedeutung oder Einheit nicht mehr korrekt und führt dadurch zu Fehlinterpretationen.
Erkennungsmerkmale
Abkürzungen, historische Namen, widersprüchliche Begriffe zwischen Code und Fachsprache oder Felder wie statusFlag und data.
Nutzen der Transformation
Ein präziser Name macht Invarianten, Einheit und Verantwortlichkeit sichtbar und reduziert den Bedarf an Kommentaren.
Sicherer Refactoring-Ablauf
Verwendungen und Serialisierungsverträge ermitteln, Feld kontrolliert umbenennen, Mapper und Tests anpassen und externe Kompatibilität bewusst sichern.
Einsatzgrenze
Bei öffentlichen JSON-, Datenbank- oder Messaging-Verträgen darf die technische Feldbezeichnung nicht ohne Migrationsplan geändert werden.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Rename Variable, Encapsulate Field, Data Mapper
Enterprise-Java-Einordnung
Rename Field wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
CustomerAccount.java
CustomerAccount.java
package com.aydinsude.workbench.catalog;
// Refactoring: Rename Field
// Ubiquitous Language: Der Feldname spiegelt den Fachbegriff wider.
public record CustomerAccount(String accountStatus) {
public CustomerAccount { if(accountStatus==null||accountStatus.isBlank()) throw new IllegalArgumentException("accountStatus"); }
public boolean active(){ return "ACTIVE".equals(accountStatus); }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Mehrere Unterklassen enthalten identische oder nahezu identische Methoden, obwohl das Verhalten zur gemeinsamen Abstraktion gehört.
Erkennungsmerkmale
Duplizierte Implementierungen, parallele Fehlerkorrekturen und Methoden mit identischem Vertrag in Geschwisterklassen.
Nutzen der Transformation
Gemeinsames Verhalten erhält einen einzigen Ort; Unterklassen konzentrieren sich auf echte Unterschiede.
Sicherer Refactoring-Ablauf
Vorbedingungen vergleichen, Unterschiede beseitigen oder parametrisieren, Methode in die Oberklasse verschieben und Unterklassen schrittweise vereinfachen.
Einsatzgrenze
Nicht hochziehen, wenn die Ähnlichkeit nur zufällig ist oder die Methode fachlich unterschiedliche Invarianten besitzt.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Extract Superclass, Template Method, Parameterize Function
Enterprise-Java-Einordnung
Pull Up Method wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
NotificationTemplate.java
NotificationTemplate.java
package com.aydinsude.workbench.catalog;
// Refactoring: Pull Up Method
// Template Method: Gemeinsame Normalisierung liegt in der Basisklasse.
public abstract class NotificationTemplate {
public final String render(String message){ return channel()+":"+normalize(message); }
protected abstract String channel();
protected String normalize(String message){ return message.trim().replaceAll("\\s+"," "); }
public static final class Email extends NotificationTemplate { protected String channel(){ return "EMAIL"; } }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Mehrere Unterklassen besitzen dasselbe Feld mit gleicher Bedeutung und gleichem Lebenszyklus.
Erkennungsmerkmale
Identische Felder, doppelte Initialisierung und parallele Getter in Geschwisterklassen.
Nutzen der Transformation
Gemeinsamer Zustand wird zentral verwaltet und kann konsistent geschützt werden.
Sicherer Refactoring-Ablauf
Semantik und Initialisierung vergleichen, Feld in die Oberklasse verschieben, Konstruktoren anpassen und Sichtbarkeit möglichst klein halten.
Einsatzgrenze
Nicht anwenden, wenn gleiche Namen unterschiedliche fachliche Bedeutungen verdecken oder die Oberklasse dadurch unnötigen Zustand erhält.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Pull Up Constructor Body, Extract Superclass, Encapsulate Variable
Enterprise-Java-Einordnung
Pull Up Field wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
AuditedCommand.java
AuditedCommand.java
package com.aydinsude.workbench.catalog;
// Refactoring: Pull Up Field
// Command: Gemeinsamer Korrelationskontext wird zentral verwaltet.
public abstract class AuditedCommand {
private final String correlationId;
protected AuditedCommand(String correlationId){ if(correlationId==null||correlationId.isBlank()) throw new IllegalArgumentException("correlationId"); this.correlationId=correlationId; }
public String correlationId(){ return correlationId; }
public static final class ApproveOrder extends AuditedCommand { public ApproveOrder(String id){ super(id); } }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine Oberklasse darf keine unterklassenspezifischen Annahmen erzwingen. Dann ist Komposition oder Factory oft geeigneter.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Pull Up Field, Extract Superclass, Factory Method
Enterprise-Java-Einordnung
Pull Up Constructor Body wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
BaseImportJob.java
BaseImportJob.java
package com.aydinsude.workbench.catalog;
// Refactoring: Pull Up Constructor Body
// Template Method: Gemeinsame Initialisierung liegt in der Basisklasse.
public abstract class BaseImportJob {
private final String source;
protected BaseImportJob(String source){ if(source==null||source.isBlank()) throw new IllegalArgumentException("source"); this.source=source.trim(); }
public String source(){ return source; }
public static final class CsvImportJob extends BaseImportJob { public CsvImportJob(String source){ super(source); } }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine Methode der Oberklasse wird nur von wenigen Unterklassen sinnvoll verwendet und belastet alle anderen Typen mit einem unpassenden Vertrag.
Erkennungsmerkmale
Leere Implementierungen, UnsupportedOperationException oder Methoden, die nur nach instanceof sinnvoll sind.
Nutzen der Transformation
Der Vertrag wird auf die Typen begrenzt, die ihn tatsächlich erfüllen können.
Sicherer Refactoring-Ablauf
Nutzer der Methode ermitteln, Methode in passende Unterklasse verschieben, Aufrufer auf präzisere Typen umstellen und Oberklassenvertrag verkleinern.
Einsatzgrenze
Wenn Aufrufer weiterhin polymorph auf das Verhalten angewiesen sind, ist eine neue Rolle oder ein Interface besser als bloßes Verschieben.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Push Down Field, Extract Interface, Replace Inheritance with Delegation
Enterprise-Java-Einordnung
Push Down Method wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
ExportFormat.java
ExportFormat.java
package com.aydinsude.workbench.catalog;
// Refactoring: Push Down Method
// Polymorphism: Variantenspezifisches Verhalten bleibt bei der Variante.
public sealed interface ExportFormat permits CsvExport, PdfExport { String mediaType(); }
final class CsvExport implements ExportFormat { public String mediaType(){ return "text/csv"; } public char delimiter(){ return ';'; } }
final class PdfExport implements ExportFormat { public String mediaType(){ return "application/pdf"; } }
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Ein Feld in der Oberklasse ist nur für einzelne Unterklassen relevant und erzeugt bei anderen Typen bedeutungslosen Zustand.
Erkennungsmerkmale
Null- oder Standardwerte in manchen Unterklassen, bedingte Nutzung und Speicherzustand ohne fachliche Bedeutung.
Nutzen der Transformation
Jede Unterklasse trägt nur den Zustand, den ihre Invarianten wirklich benötigen.
Sicherer Refactoring-Ablauf
Verwendungen analysieren, Feld und Initialisierung in die betroffenen Unterklassen verschieben und Oberklassenkonstruktor vereinfachen.
Einsatzgrenze
Wenn das Feld eine gemeinsame zukünftige Fähigkeit ausdrückt, kann ein separates Capability-Objekt geeigneter sein.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Push Down Method, Extract Class, Capability Interface
Enterprise-Java-Einordnung
Push Down Field wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
DeliveryOption.java
DeliveryOption.java
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Push Down Field
// Polymorphism: Express-spezifische SLA liegt nur in ExpressDelivery.
public sealed interface DeliveryOption permits StandardDelivery, ExpressDelivery { String code(); }
final class StandardDelivery implements DeliveryOption { public String code(){ return "STANDARD"; } }
record ExpressDelivery(Duration serviceLevel) implements DeliveryOption { public ExpressDelivery { if(serviceLevel.isNegative()||serviceLevel.isZero()) throw new IllegalArgumentException("serviceLevel"); } public String code(){ return "EXPRESS"; } }
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine Klasse delegiert nahezu jede Operation an einen stabilen Basistyp und soll dessen vollständigen Vertrag bewusst übernehmen.
Erkennungsmerkmale
Viele reine Weiterleitungsmethoden, identischer Lebenszyklus und echte substituierbare Beziehung.
Nutzen der Transformation
Direkte Vererbung kann unnötige Weiterleitung entfernen und Polymorphie vereinfachen.
Sicherer Refactoring-Ablauf
Liskov-Substitution prüfen, Basisklasse kontrollierbar machen, Delegation schrittweise ersetzen und Verhalten mit Vertragstests absichern.
Einsatzgrenze
Dieses Refactoring ist selten. Bei bloßer Wiederverwendung oder instabiler Basisklasse bleibt Delegation meist die bessere Wahl.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Replace Inheritance with Delegation, Liskov Substitution, Adapter
Enterprise-Java-Einordnung
Replace Delegation with Inheritance wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
CsvReportFormatter.java
CsvReportFormatter.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Delegation with Inheritance
// Template Method: CSV-Formatter ist eine echte Spezialisierung des Basisformatters.
public class CsvReportFormatter extends ReportFormatter {
@Override protected String separator(){ return ";"; }
}
class ReportFormatter {
protected String separator(){ return ","; }
public String row(String left,String right){ return left+separator()+right; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine Unterklasse erbt Verhalten und Zustand, obwohl sie fachlich kein echtes Exemplar des Basistyps ist oder nur einen Teil seiner Fähigkeiten benötigt.
Erkennungsmerkmale
Überschriebene Methoden mit leerem Verhalten, verletzte Invarianten, fragile Basisklasse und Änderungen mit unerwarteten Nebenwirkungen.
Nutzen der Transformation
Delegation trennt Wiederverwendung von Typbeziehung und erlaubt gezielte, austauschbare Zusammenarbeit.
Sicherer Refactoring-Ablauf
Benötigte Fähigkeiten identifizieren, Delegat einführen, Aufrufe schrittweise weiterleiten, öffentlichen Vertrag verkleinern und Vererbung entfernen.
Einsatzgrenze
Delegation kann zu bloßen Weiterleitungsketten führen. Der Delegat sollte eine klar benannte Verantwortung besitzen.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Decorator, Strategy, Adapter, Collapse Hierarchy
Enterprise-Java-Einordnung
Replace Inheritance with Delegation wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
DiscountedPrice.java
DiscountedPrice.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Inheritance with Delegation
// Strategy: Preisberechnung wird als kleiner Port delegiert.
public final class DiscountedPrice {
private final PricePolicy policy;
public DiscountedPrice(PricePolicy policy){ this.policy=policy; }
public BigDecimal calculate(BigDecimal base){ return policy.apply(base); }
public interface PricePolicy { BigDecimal apply(BigDecimal base); }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Ein Typ- oder Statuscode steuert wiederholt unterschiedliches Verhalten und erzeugt wachsende switch-Blöcke.
Erkennungsmerkmale
Parallele Bedingungen in mehreren Methoden, neue Varianten ändern viele Stellen und ungültige Kombinationen bleiben darstellbar.
Nutzen der Transformation
State oder Strategy kapseln variierendes Verhalten hinter einem stabilen Vertrag; Auswahl und Verhalten werden getrennt.
Sicherer Refactoring-Ablauf
Variationsachse bestimmen, Interface einführen, Varianten schrittweise extrahieren, Auswahl zentralisieren und alte Typcodes aus Verhalten entfernen.
Einsatzgrenze
Für reine Datenklassifikation oder wenige stabile Fälle kann ein Enum mit klarer Logik ausreichend sein.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Strategy, State, Replace Conditional with Polymorphism
Enterprise-Java-Einordnung
Replace Type Code with State/Strategy wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
PaymentLifecycle.java
PaymentLifecycle.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Type Code with State/Strategy
// State Pattern: Jeder Zustand kontrolliert seine erlaubten Übergänge.
public final class PaymentLifecycle {
private State state=new Pending();
public void authorize(){ state=state.authorize(); }
public String status(){ return state.name(); }
private interface State { State authorize(); String name(); }
private static final class Pending implements State { public State authorize(){ return new Authorized(); } public String name(){ return "PENDING"; } }
private static final class Authorized implements State { public State authorize(){ return this; } public String name(){ return "AUTHORIZED"; } }
}
Java-21-Zielcode.java
Java-21-Zielcode.java
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class PaymentLifecycleTest { @Test void state_replaces_type_code(){ var p=new PaymentLifecycle(); p.authorize(); assertEquals("AUTHORIZED",p.status()); } }
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Kapitel 8 Kapselung, Felder und Zugriffsgrenzen verbessern10 Refactorings
Datenmodell-, Assoziations- und Typisierungs-Refactorings mit kompilierbarem Java-21-Zielcode.
71. Self Encapsulate Field
Fachliches Ausgangsproblem
Eine Klasse greift direkt auf ihr eigenes Feld zu, obwohl Zugriff, Normalisierung oder spätere Ableitung kontrolliert werden sollen.
Erkennungsmerkmale
Viele direkte Lese- und Schreibzugriffe, schwer einführbare Invarianten und spätere Umstellung auf berechnete Werte.
Nutzen der Transformation
Interne Zugriffsmethoden schaffen einen einzigen Kontrollpunkt und erleichtern spätere Änderungen der Repräsentation.
Sicherer Refactoring-Ablauf
Private Getter/Setter oder fachliche Methoden einführen, interne Zugriffe schrittweise ersetzen und Setter möglichst durch intentionale Operationen begrenzen.
Einsatzgrenze
Mechanische Getter und Setter ohne fachlichen Nutzen erhöhen nur die Indirektion. Kapselung braucht einen konkreten Änderungsgrund.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Encapsulate Variable, Replace Primitive with Object, Introduce Assertion
Enterprise-Java-Einordnung
Self Encapsulate Field wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
CustomerBalance.java
CustomerBalance.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Self Encapsulate Field
// Encapsulation: Invarianten bleiben an einem kontrollierten Zugriffspunkt.
public final class CustomerBalance {
private BigDecimal amount = BigDecimal.ZERO;
public BigDecimal amount(){ return amount; }
public void changeTo(BigDecimal value){
if(value==null || value.signum()<0) throw new IllegalArgumentException("amount");
amount=value;
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Ein Array speichert heterogene Fachwerte an positionsabhängigen Indizes. Bedeutung und Gültigkeit bleiben unsichtbar.
Erkennungsmerkmale
Zugriffe wie row[0], row[3], Casts und Kommentare zur Spaltenreihenfolge.
Nutzen der Transformation
Ein typisiertes Objekt macht Felder, Einheiten und Invarianten explizit und verhindert Indexfehler.
Sicherer Refactoring-Ablauf
Bedeutung jeder Position dokumentieren, Record oder Klasse einführen, Konvertierung an der Grenze zentralisieren und Aufrufer schrittweise migrieren.
Einsatzgrenze
Für homogene numerische Vektoren oder bewusst tabellarische technische Daten bleibt ein Array angemessen.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Encapsulate Record, Data Mapper, Replace Stringly-Typed Map with Typed Record
Enterprise-Java-Einordnung
Replace Array with Object wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
ShipmentWindow.java
ShipmentWindow.java
package com.aydinsude.workbench.catalog;
import java.time.LocalDate;
// Refactoring: Replace Array with Object
// Value Object: Benannte Komponenten ersetzen fragile Indexkonventionen.
public record ShipmentWindow(LocalDate from, LocalDate to) {
public ShipmentWindow { if(from==null||to==null||to.isBefore(from)) throw new IllegalArgumentException("window"); }
public long days(){ return java.time.temporal.ChronoUnit.DAYS.between(from,to)+1; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
73. Change Unidirectional Association to Bidirectional
Fachliches Ausgangsproblem
Zwei Objekte müssen konsistent voneinander navigierbar sein, aber nur eine Seite kennt die Beziehung.
Erkennungsmerkmale
Teure Rückwärtssuchen, externe Maps oder wiederholte Queries, um den Besitzer eines Objekts zu finden.
Nutzen der Transformation
Eine kontrollierte bidirektionale Beziehung ermöglicht Navigation in beide Richtungen und kann fachliche Invarianten lokal halten.
Sicherer Refactoring-Ablauf
Verantwortlichen Besitzer festlegen, Änderungsmethoden auf einer Seite zentralisieren, beide Seiten atomar aktualisieren und Konsistenztests ergänzen.
Einsatzgrenze
Bidirektionale Beziehungen erhöhen Kopplung und Fehlergefahr. Ohne echten Navigationsbedarf sollte die Beziehung einseitig bleiben.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Aggregate, Encapsulate Collection, Association Object
Enterprise-Java-Einordnung
Change Unidirectional Association to Bidirectional wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
OrderCustomerRelation.java
OrderCustomerRelation.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Change Unidirectional Association to Bidirectional
// Aggregate consistency: Eine Operation hält beide Seiten synchron.
public final class OrderCustomerRelation {
public static final class Customer { private final List<Order> orders=new ArrayList<>(); public List<Order> orders(){ return List.copyOf(orders); } }
public static final class Order { private Customer customer; public Customer customer(){ return customer; } }
public static void assign(Order order, Customer customer){
if(order.customer!=null) order.customer.orders.remove(order);
order.customer=Objects.requireNonNull(customer); if(!customer.orders.contains(order)) customer.orders.add(order);
}
}
Java-21-Zielcode.java
Java-21-Zielcode.java
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class OrderCustomerRelationTest { @Test void both_sides_stay_consistent(){ var o=new OrderCustomerRelation.Order(); var c=new OrderCustomerRelation.Customer(); OrderCustomerRelation.assign(o,c); assertSame(c,o.customer()); assertEquals(1,c.orders().size()); } }
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
74. Change Bidirectional Association to Unidirectional
Fachliches Ausgangsproblem
Beide Objekte kennen einander, obwohl nur eine Navigationsrichtung benötigt wird. Dadurch entstehen Kopplung, Synchronisationsaufwand und Serialisierungszyklen.
Erkennungsmerkmale
Gegenseitige Setter, Endlosschleifen bei JSON-Ausgabe und schwer kontrollierbare Lebenszyklen.
Nutzen der Transformation
Eine gerichtete Beziehung vereinfacht Eigentum, Laden, Persistenz und Konsistenz.
Sicherer Refactoring-Ablauf
Tatsächliche Navigationsnutzung messen, überflüssige Rückreferenz entfernen, Queries oder Repository-Zugriff an der richtigen Grenze ergänzen und Tests anpassen.
Einsatzgrenze
Wenn die Rücknavigation eine echte Aggregate-Invariante trägt, darf sie nicht nur zur technischen Vereinfachung entfernt werden.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Data Mapper, Repository, Aggregate Boundary
Enterprise-Java-Einordnung
Change Bidirectional Association to Unidirectional wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
InvoiceAccountLink.java
InvoiceAccountLink.java
package com.aydinsude.workbench.catalog;
// Refactoring: Change Bidirectional Association to Unidirectional
// Simplicity: Nur die fachlich benötigte Richtung bleibt bestehen.
public final class InvoiceAccountLink {
public record Account(String id) { public Account { if(id==null||id.isBlank()) throw new IllegalArgumentException("id"); } }
public record Invoice(String number, Account account) { public Invoice { if(number==null||number.isBlank()||account==null) throw new IllegalArgumentException(); } }
}
Java-21-Zielcode.java
Java-21-Zielcode.java
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class InvoiceAccountLinkTest { @Test void only_required_direction_remains(){ var a=new InvoiceAccountLink.Account("A1"); var i=new InvoiceAccountLink.Invoice("I1",a); assertEquals("A1",i.account().id()); } }
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine fremde Klasse benötigt eine kleine zusätzliche Operation, kann aber nicht verändert oder erweitert werden.
Erkennungsmerkmale
Wiederholte Hilfslogik direkt neben Aufrufen einer Bibliotheksklasse oder eines unveränderbaren Legacy-Typs.
Nutzen der Transformation
Eine klar benannte Hilfsmethode lokalisiert die fehlende Operation und dokumentiert die fachliche Absicht.
Sicherer Refactoring-Ablauf
Operation nahe am nutzenden Code als statische oder private Methode einführen, Parameter explizit halten und Duplikate entfernen.
Einsatzgrenze
Bei mehreren Operationen oder dauerhaftem Erweiterungsbedarf ist Local Extension, Adapter oder Wrapper geeigneter.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Introduce Local Extension, Adapter, Extension Method
Enterprise-Java-Einordnung
Introduce Foreign Method wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
LegacyDateSupport.java
LegacyDateSupport.java
package com.aydinsude.workbench.catalog;
import java.time.LocalDate;
// Refactoring: Introduce Foreign Method
// Adapter-artige Erweiterung: Fremdtyp bleibt unverändert.
public final class LegacyDateSupport {
private LegacyDateSupport(){}
public static LocalDate nextBusinessDay(LocalDate date){
var next=date.plusDays(1); while(next.getDayOfWeek().getValue()>=6) next=next.plusDays(1); return next;
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine fremde Klasse benötigt mehrere zusätzliche Operationen oder eine stabilere lokale Schnittstelle, kann aber nicht verändert werden.
Erkennungsmerkmale
Viele Foreign Methods, wiederholte Konvertierung und direkte Abhängigkeit von Bibliotheksdetails.
Nutzen der Transformation
Wrapper oder Unterklasse bietet eine lokale, fachlich passende Schnittstelle und begrenzt Fremdabhängigkeiten.
Sicherer Refactoring-Ablauf
Benötigten Vertrag definieren, Wrapper einführen, Konvertierung zentralisieren und Aufrufer schrittweise auf die lokale Erweiterung umstellen.
Einsatzgrenze
Vererbung ist nur bei wirklich erweiterbarer Basisklasse sicher. Häufig ist ein Adapter mit Komposition robuster.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Introduce Local Extension wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
MoneyExtension.java
MoneyExtension.java
package com.aydinsude.workbench.catalog;
import java.math.*;
// Refactoring: Introduce Local Extension
// Wrapper/Value Object: Lokale Erweiterungen erhalten eine kohärente API.
public record MoneyExtension(BigDecimal value) {
public MoneyExtension { value=value.setScale(2,RoundingMode.HALF_UP); }
public MoneyExtension add(MoneyExtension other){ return new MoneyExtension(value.add(other.value)); }
public boolean positive(){ return value.signum()>0; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Ein Fachobjekt enthält nur Felder und Getter; Regeln und Zustandsänderungen liegen in Services verteilt.
Erkennungsmerkmale
Anämische Modelle, Setter für jeden Zustand und Services, die fremde Daten manipulieren.
Nutzen der Transformation
Das Domänenobjekt schützt seine Invarianten und bietet intentionale Operationen statt beliebiger Mutation.
Sicherer Refactoring-Ablauf
Invarianten und zustandsnahe Regeln identifizieren, fachliche Methoden einführen, Setter entfernen und orchestrierende Services auf Koordination reduzieren.
Einsatzgrenze
Nicht jede DTO- oder Persistenzklasse soll reich werden. Transfermodelle und Read Models dürfen bewusst datenorientiert bleiben.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Domain Model, Aggregate, Move Function
Enterprise-Java-Einordnung
Replace Data Class with Rich Domain Object wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
ServiceAgreement.java
ServiceAgreement.java
package com.aydinsude.workbench.catalog;
import java.time.LocalDate;
// Refactoring: Replace Data Class with Rich Domain Object
// Aggregate: Invarianten und Verhalten liegen beim Fachobjekt.
public final class ServiceAgreement {
private LocalDate validUntil;
public ServiceAgreement(LocalDate validUntil){ this.validUntil=java.util.Objects.requireNonNull(validUntil); }
public void extendTo(LocalDate newDate){ if(!newDate.isAfter(validUntil)) throw new IllegalArgumentException("newDate"); validUntil=newDate; }
public boolean activeOn(LocalDate date){ return !date.isAfter(validUntil); }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Ein Boolean steht für mehr als eine fachliche Bedeutung oder wird mit unklaren true/false-Aufrufen verwendet.
Erkennungsmerkmale
Methodenaufrufe wie send(true), mehrere kombinierte Flags oder Kommentare zur Bedeutung von false.
Nutzen der Transformation
Ein Enum benennt erlaubte Zustände, verhindert Verwechslungen und kann später Verhalten aufnehmen.
Sicherer Refactoring-Ablauf
Bedeutungen sammeln, Enum einführen, Signaturen und Persistenzmapping migrieren und ungültige Kombinationen mit Tests ausschließen.
Einsatzgrenze
Für eine wirklich binäre, lokal klare Aussage kann Boolean die einfachere Darstellung bleiben.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Remove Flag Argument, Replace Type Code with State/Strategy, Value Object
Enterprise-Java-Einordnung
Replace Boolean with Enum wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
DeliveryConfirmation.java
DeliveryConfirmation.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Boolean with Enum
// Type-safe domain model: Benannte Varianten ersetzen mehrdeutige Flags.
public record DeliveryConfirmation(ConfirmationMode mode) {
public enum ConfirmationMode { NONE, SIGNATURE, ONE_TIME_CODE }
public boolean requiresCustomerAction(){ return mode!=ConfirmationMode.NONE; }
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Eine Abfrage signalisiert ein reguläres Nichtvorhandensein durch null; Aufrufer vergessen Prüfungen oder verwechseln Fehler mit Abwesenheit.
Erkennungsmerkmale
NullPointerException, verstreute Nullprüfungen und unklare Verträge von Finder-Methoden.
Nutzen der Transformation
Optional macht Abwesenheit im Rückgabetyp sichtbar und zwingt zu einer bewussten Behandlung.
Sicherer Refactoring-Ablauf
Nur regulär fehlende Ergebnisse umstellen, Rückgabetyp ändern, Aufrufer migrieren und Fehlerfälle weiterhin als Result oder Exception modellieren.
Einsatzgrenze
Optional ist ungeeignet für Felder, Parameter oder komplexe Fehlerdiagnosen. Dort sind Null Object oder Result Type oft besser.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Introduce Special Case, Result Type, Null Object
Enterprise-Java-Einordnung
Replace Null with Optional Result wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
CustomerLookupResult.java
CustomerLookupResult.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Replace Null with Optional Result
// Explicit result contract: Abwesenheit ist Teil der Signatur.
public final class CustomerLookupResult {
private final Map<String,String> names=Map.of("C1","Ada");
public Optional<String> findName(String id){ return Optional.ofNullable(names.get(id)); }
}
Java-21-Zielcode.java
Java-21-Zielcode.java
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class CustomerLookupResultTest { @Test void optional_replaces_null(){ var l=new CustomerLookupResult(); assertEquals("Ada",l.findName("C1").orElseThrow()); assertTrue(l.findName("missing").isEmpty()); } }
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Fachwerte werden in einer Map mit String-Schlüsseln transportiert. Strukturfehler werden erst zur Laufzeit erkannt.
Erkennungsmerkmale
Tippfehler in Schlüsseln, Casts, uneinheitliche Pflichtfelder und fehlende IDE-Unterstützung.
Nutzen der Transformation
Ein Record oder typisiertes DTO macht den Vertrag kompilierbar, dokumentierbar und validierbar.
Sicherer Refactoring-Ablauf
Schema und Pflichtfelder bestimmen, Record einführen, Mapper an der Systemgrenze erstellen und interne Aufrufer schrittweise migrieren.
Einsatzgrenze
Bei bewusst offenen Metadaten oder Plugin-Daten kann eine Map bleiben; der stabile Kern sollte dennoch typisiert sein.
Risiken und Fehlanwendungen
Die Änderung muss in kleinen, kompilierbaren Schritten erfolgen. Öffentliche Verträge, Persistenzmapping, Serialisierung, Nebenwirkungen und Nebenläufigkeit werden ausdrücklich mitgeprüft.
Verwandte Techniken
Encapsulate Record, Data Mapper, Anti-Corruption Layer
Enterprise-Java-Einordnung
Replace Stringly-Typed Map with Typed Record wird in Enterprise-Java-Anwendungen an einer klaren Fach-, Persistenz- oder Integrationsgrenze eingesetzt. Der vorhandene Java-21-Code zeigt die Zielstruktur ohne Frameworkkopplung; Spring und Jakarta EE bleiben Adapter um die Fachlogik.
Teststrategie
Characterization Tests sichern zunächst das beobachtbare Verhalten. Danach prüfen fokussierte Unit-Tests Normalfälle, Grenzwerte und Fehlerpfade; bei öffentlichen Verträgen kommen Mapper-, Serialisierungs- oder Integrationstests hinzu.
ClaimSubmission.java
ClaimSubmission.java
package com.aydinsude.workbench.catalog;
import java.math.BigDecimal;
// Refactoring: Replace Stringly-Typed Map with Typed Record
// Value Object: Compiler und Konstruktor sichern das Datenmodell.
public record ClaimSubmission(String claimId, String customerId, BigDecimal amount) {
public ClaimSubmission {
if(claimId==null||claimId.isBlank()||customerId==null||customerId.isBlank()||amount==null||amount.signum()<=0) throw new IllegalArgumentException("claim");
}
}
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul- und Fachquellen redaktionell zusammengeführt und auf das konkrete Java-Beispiel übertragen. Der Text ist keine wörtliche Übersetzung.
Kapitel 9 Interfaces, Hierarchien und Polymorphie ordnen10 Refactorings
Abhängigkeits-, Integrations- und Persistenzgrenzen mit kompilierbarem Java-21-Zielcode.
81. Extract Interface
Fachliches Ausgangsproblem
Eine Fach- oder Anwendungsklasse hängt von einer umfangreichen konkreten Providerklasse ab, obwohl sie nur einen kleinen Ausschnitt ihres Verhaltens benötigt. Dadurch werden technische Details, Lebenszyklusentscheidungen und zusätzliche Methoden ungewollt Teil der fachlichen Kopplung.
Erkennungsmerkmale
Konstruktoren erwarten konkrete SDK-, Client- oder Frameworktypen; Tests müssen große Providerobjekte aufbauen; Änderungen am Provider erzwingen Anpassungen in fachlich unabhängigen Klassen; der Aufrufer nutzt nur ein bis drei Methoden des Typs.
Nutzen der Transformation
Eine fachlich benannte, schmale Schnittstelle macht den tatsächlich benötigten Vertrag sichtbar. Die Abhängigkeit zeigt danach auf eine stabile Fähigkeit statt auf die gesamte technische Implementierung. Das erleichtert Adapter, Fakes und getrennte Weiterentwicklung.
Sicherer Refactoring-Ablauf
Verhalten des bestehenden Aufrufers mit Characterization Tests sichern.
Alle tatsächlich verwendeten Provideroperationen und benötigten Datentypen erfassen.
Eine kleine, aus Sicht des Verbrauchers benannte Schnittstelle einführen.
Die konkrete Providerklasse oder einen Adapter diese Schnittstelle implementieren lassen.
Den Aufrufer auf die Schnittstelle umstellen und überflüssige Providerkenntnisse entfernen.
Einsatzgrenze
Nicht jede Klasse braucht ein Interface. Wenn exakt eine stabile Implementierung existiert, keine Austausch- oder Testgrenze benötigt wird und der konkrete Typ selbst der fachliche Vertrag ist, erzeugt eine zusätzliche Schnittstelle nur Navigation ohne Nutzen.
Risiken und Fehlanwendungen
Ein zu breites Interface konserviert die alte Kopplung. Ein Interface pro Klasse ohne fachliche Bedeutung führt zu sogenannter Interface-Verschmutzung. Die Schnittstelle sollte vom Verbraucher und nicht vom Providerdesign bestimmt werden.
Verwandte Techniken
Dependency Inversion, Interface Segregation, Extract Adapter und Introduce Port.
Abgrenzung und Entscheidungshilfe
Extract Interface trennt einen benötigten Vertrag aus einem vorhandenen Typ. Extract Adapter übersetzt zusätzlich zwischen zwei nicht passenden Modellen. Beides kann aufeinander folgen.
Enterprise-Java-Einordnung
Ein Zahlungsanwendungsfall kennt nur PaymentAuthorizationPort. Ein Stripe- oder Bankadapter übersetzt technische Requests und Responses. Die Fachlogik bleibt unabhängig von SDK-Versionen, HTTP-Fehlern und Provider-Metadaten.
Teststrategie
Contract-Tests prüfen den Portvertrag für jede Adapterimplementierung. Unit-Tests des Anwendungsfalls verwenden einen kleinen Fake und prüfen Fachentscheidungen ohne Netzwerkzugriff.
PaymentAuthorizationPort.java
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Interface
// Port Pattern: Der Use Case hängt nur vom benötigten Vertrag ab.
public interface PaymentAuthorizationPort {
Authorization authorize(String paymentId, long cents);
record Authorization(String providerReference, boolean approved) {}
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine statische Utility-Klasse enthält veränderliche fachliche oder technische Entscheidungen. Aufrufer können die Abhängigkeit weder im Konstruktor erkennen noch im Test kontrollieren. Globale Zustände, Zeit, Zufall oder Konfiguration werden häufig indirekt gelesen.
Erkennungsmerkmale
Statische Methoden greifen auf Systemzeit, Umgebungsvariablen, globale Konfiguration oder externe Ressourcen zu; Tests benötigen statische Mocking-Werkzeuge; unterschiedliche Mandanten oder Regionen brauchen Varianten; Aufrufer lassen ihre Abhängigkeit nicht erkennen.
Nutzen der Transformation
Ein injizierter Service macht die Abhängigkeit explizit, ermöglicht unterschiedliche Implementierungen und begrenzt Seiteneffekte. Fachlich reine, unveränderliche Hilfsfunktionen dürfen dagegen weiterhin statisch bleiben.
Sicherer Refactoring-Ablauf
Statische Aufrufe inventarisieren und reine von zustands- oder umgebungsabhängigen Funktionen trennen.
Einen kleinen Service-Vertrag für die veränderliche Fähigkeit definieren.
Die vorhandene Logik zunächst unverändert in eine Implementierung verschieben.
Den Service über den Konstruktor an die Verbraucher übergeben.
Statische Zugriffspunkte nach schrittweiser Migration entfernen.
Einsatzgrenze
Reine Funktionen wie mathematische Umrechnungen oder nullsichere Stringoperationen benötigen nicht automatisch einen Service. Die Transformation ist sinnvoll, wenn Austauschbarkeit, Lebenszyklus oder Seiteneffekte relevant sind.
Risiken und Fehlanwendungen
Ein UniversalService als Ersatz für eine UniversalUtility verbessert nichts. Ebenso problematisch ist ein Service, der nur statische Methoden umhüllt, aber weiterhin global bezogen wird.
Verwandte Techniken
Introduce Dependency Injection, Extract Clock, Extract ID Generator und Replace Global State with Context Object.
Abgrenzung und Entscheidungshilfe
Der Kern ist nicht Objektorientierung um jeden Preis, sondern explizite Kontrolle über eine relevante Abhängigkeit.
Enterprise-Java-Einordnung
Steuerberechnung wird als TaxCalculationService injiziert. Regionale Regeln, Stichtage und Rundungsverfahren können separat implementiert und getestet werden, ohne den Bestellservice neu zu verdrahten.
Teststrategie
Unit-Tests injizieren kontrollierte Implementierungen. Contract-Tests sichern gemeinsame Regeln. Ein Integrationstest prüft die produktive Verdrahtung im Composition Root.
TaxCalculationService.java
package com.aydinsude.workbench.catalog;
import java.math.*;
// Refactoring: Replace Static Utility with Injected Service
// Strategy + Dependency Injection: Steuerregeln sind austauschbar und testbar.
public final class TaxCalculationService {
public interface TaxPolicy { BigDecimal taxFor(BigDecimal net); }
private final TaxPolicy policy;
public TaxCalculationService(TaxPolicy policy){ this.policy=java.util.Objects.requireNonNull(policy); }
public BigDecimal gross(BigDecimal net){ return net.add(policy.taxFor(net)).setScale(2,RoundingMode.HALF_UP); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Mehrere Klassen lesen und verändern globale statische Variablen. Das Verhalten hängt dadurch von einer unsichtbaren Reihenfolge vorheriger Aufrufe ab und kann zwischen Tests, Threads oder Mandanten auslaufen.
Erkennungsmerkmale
Statische mutable Maps, globale CurrentUser- oder Tenant-Felder, Tests mit reset()-Aufrufen, sporadische Parallelitätsfehler und Methoden, deren Ergebnis ohne sichtbare Parameter variiert.
Nutzen der Transformation
Ein explizites Context Object bündelt zusammengehörige Laufzeitinformationen und wird bewusst übergeben. Abhängigkeiten, Gültigkeitsdauer und Besitz werden sichtbar. Unveränderliche Snapshots vermeiden unkontrollierte Änderungen.
Sicherer Refactoring-Ablauf
Globale Felder und alle Lese-/Schreibstellen erfassen.
Zusammengehörige Werte in einem unveränderlichen Kontexttyp modellieren.
Den Kontext zunächst zusätzlich an eine äußere Anwendungsgrenze übergeben.
Aufrufer schrittweise von globalem Zugriff auf den Parameter umstellen.
Globale Schreiboperationen entfernen und Kontextwechsel explizit modellieren.
Einsatzgrenze
Ein Kontext darf nicht zum beliebigen Datenbeutel werden. Werte, die nur eine einzelne Methode benötigt, bleiben direkte Parameter. Technische Thread-Kontexte benötigen klare Propagationsregeln.
Risiken und Fehlanwendungen
Ein riesiges Context Object versteckt Abhängigkeiten erneut. Mutable Kontexte erzeugen weiterhin Reihenfolgeprobleme. Besonders kritisch ist die Speicherung in ThreadLocal bei asynchronen oder virtuellen Threads.
Ein Context Object macht Laufzeitkontext sichtbar. Ein Service Locator liefert dagegen versteckt beliebige Dienste und ist kein Ersatz.
Enterprise-Java-Einordnung
RequestContext enthält TenantId, CorrelationId, Locale und Actor in unveränderlicher Form. Anwendungskommandos erhalten diesen Kontext explizit; Adapter erzeugen ihn an der Systemgrenze.
Teststrategie
Tests erzeugen kleine Kontextinstanzen und prüfen Mandantenisolation sowie unveränderte Weitergabe. Parallelitätstests stellen sicher, dass keine Daten zwischen Requests geteilt werden.
RequestExecutionContext.java
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Replace Global State with Context Object
// Context Object: Laufzeitdaten sind explizit, unveränderlich und requestgebunden.
public record RequestExecutionContext(String tenantId, String correlationId, Instant startedAt) {
public RequestExecutionContext {
if(tenantId==null||tenantId.isBlank()||correlationId==null||correlationId.isBlank()||startedAt==null) throw new IllegalArgumentException("context");
}
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine Klasse erzeugt benötigte Dienste selbst und entscheidet damit gleichzeitig über Fachlogik, technische Implementierung und Objektlebenszyklus. Änderungen an Infrastruktur führen zu Änderungen im Anwendungsfall.
Erkennungsmerkmale
new-Aufrufe für Repository, HTTP-Client oder Clock innerhalb fachlicher Methoden; Konfigurationszugriffe im Konstruktor; Tests müssen reale Infrastruktur starten; Varianten werden mit if-Abfragen ausgewählt.
Nutzen der Transformation
Dependency Injection verlagert Erzeugung und Verdrahtung an eine äußere Stelle. Die Klasse beschreibt nur noch, welche Fähigkeiten sie benötigt. Constructor Injection sichert vollständige, unveränderliche Objekte.
Sicherer Refactoring-Ablauf
Direkte Objekterzeugung und versteckte Dienstzugriffe identifizieren.
Für jede relevante Fähigkeit einen konkreten oder abstrakten Vertrag bestimmen.
Abhängigkeiten als Konstruktorparameter einführen und Felder final machen.
Erzeugung in einen Composition Root oder Framework-Konfiguration verschieben.
Service-Locator- und Fallback-Pfade entfernen, sobald alle Aufrufer migriert sind.
Einsatzgrenze
Value Objects und interne Implementierungsdetails müssen nicht injiziert werden. Eine Dependency ist typischerweise ein externer Dienst, eine Policy oder eine austauschbare Kollaboration.
Risiken und Fehlanwendungen
Zu viele Konstruktorparameter weisen häufig auf zu viele Verantwortlichkeiten hin. Field Injection versteckt Unvollständigkeit und erschwert reine Unit-Tests. Ein DI-Container darf nicht in Fachcode hineinreichen.
Verwandte Techniken
Replace Service Locator with Constructor Injection, Introduce Composition Root und Extract Interface.
Abgrenzung und Entscheidungshilfe
Dependency Injection ist die Übergabe von Abhängigkeiten; ein DI-Framework ist nur eine mögliche technische Umsetzung.
Enterprise-Java-Einordnung
OrderApplicationService erhält OrderRepository, PaymentPort, Clock und EventPublisher im Konstruktor. Spring oder CDI verdrahtet die produktiven Adapter außerhalb des Fachmoduls.
Teststrategie
Unit-Tests erstellen den Service direkt mit Fakes. Ein schlanker Container-Smoke-Test prüft, dass die produktive Verdrahtung vollständig und eindeutig ist.
ShipmentUseCase.java
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Dependency Injection
// Constructor Injection: Abhängigkeiten sind vollständig und unveränderlich.
public final class ShipmentUseCase {
public interface ShipmentRepository { void save(String shipmentId); }
public interface NotificationPort { void sent(String shipmentId); }
private final ShipmentRepository repository; private final NotificationPort notifications;
public ShipmentUseCase(ShipmentRepository repository, NotificationPort notifications){ this.repository=repository; this.notifications=notifications; }
public void ship(String id){ repository.save(id); notifications.sent(id); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
85. Replace Service Locator with Constructor Injection
Fachliches Ausgangsproblem
Klassen rufen einen globalen Service Locator auf, um Abhängigkeiten bei Bedarf zu suchen. Die Signatur behauptet Selbstständigkeit, obwohl das Objekt von mehreren externen Diensten abhängt.
Erkennungsmerkmale
ServiceLocator.get(), ApplicationContext.getBean() oder CDI.current() im Anwendungs- und Fachcode; Laufzeitfehler wegen fehlender Registrierung; Tests müssen globale Containerzustände vorbereiten.
Nutzen der Transformation
Constructor Injection macht alle Pflichtabhängigkeiten beim Erzeugen sichtbar. Ungültige Objekte können nicht entstehen, IDE und Compiler unterstützen Umbenennung und Navigation, Tests benötigen keinen globalen Container.
Sicherer Refactoring-Ablauf
Alle Locator-Aufrufe pro Klasse erfassen.
Pflicht- und optionale Abhängigkeiten unterscheiden.
Pflichtabhängigkeiten als Konstruktorparameter einführen.
Composition Root oder Framework-Konfiguration auf die neue Signatur umstellen.
Locator-Aufrufe entfernen und globale Registrierungen zurückbauen.
Einsatzgrenze
Dynamische Pluginverzeichnisse können eine Registry als fachlichen Mechanismus benötigen. Auch dann sollte die Registry explizit injiziert werden und nicht global erreichbar sein.
Risiken und Fehlanwendungen
Setter Injection für Pflichtabhängigkeiten erzeugt zeitweise unvollständige Objekte. Ein einziger injizierter Locator mit neuem Namen behebt die versteckte Kopplung nicht.
Verwandte Techniken
Introduce Dependency Injection, Introduce Composition Root und Strategy Registry.
Abgrenzung und Entscheidungshilfe
Eine explizit injizierte, fachlich eng begrenzte Registry ist nicht dasselbe wie ein universeller Service Locator.
Enterprise-Java-Einordnung
Ein ExportUseCase erhält DocumentStore und ExporterRegistry direkt. Die Auswahl eines Exporters bleibt dynamisch, aber die Registry ist eine sichtbare Abhängigkeit.
Teststrategie
Konstruktionstests zeigen fehlende Abhängigkeiten sofort. Unit-Tests übergeben Fakes; ein Integrationstest prüft die Containerkonfiguration.
CustomerRegistration.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Service Locator with Constructor Injection
// Explicit Dependencies: Der Vertrag zeigt, was der Use Case wirklich benötigt.
public final class CustomerRegistration {
public interface CustomerStore { void add(String customerId); }
public interface WelcomeSender { void send(String customerId); }
private final CustomerStore store; private final WelcomeSender sender;
public CustomerRegistration(CustomerStore store, WelcomeSender sender){ this.store=store; this.sender=sender; }
public void register(String id){ store.add(id); sender.send(id); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Fachcode verwendet Datentypen, Fehlercodes und Aufrufreihenfolgen eines externen Systems direkt. Änderungen des Fremdsystems breiten sich über viele Klassen aus.
Erkennungsmerkmale
SDK-Klassen in Domänenpaketen, wiederholte Mappinglogik, Providerfehler in Use-Case-Signaturen, technische Retry-Details in Fachmethoden und schwer testbare externe Aufrufe.
Nutzen der Transformation
Ein Adapter übersetzt zwischen internem Port und fremder Schnittstelle. Er kapselt Mapping, Fehlerübersetzung, Protokolldetails und Anbieterbesonderheiten an einer klaren Systemgrenze.
Sicherer Refactoring-Ablauf
Intern benötigte Fähigkeit aus Sicht des Anwendungsfalls als Port formulieren.
Fremde Typen und Fehler auf eine Adapterklasse begrenzen.
Mapping und Fehlerübersetzung aus den Verbrauchern in den Adapter verschieben.
Verbraucher auf den Port umstellen.
Adapter mit Contract- und Integrationstests absichern.
Einsatzgrenze
Kein Adapter ist nötig, wenn die externe Schnittstelle bereits exakt dem stabilen internen Vertrag entspricht und keine Übersetzung stattfindet. Eine bloße Weiterleitungsfassade ohne Grenze erzeugt wenig Nutzen.
Risiken und Fehlanwendungen
Ein undichter Adapter gibt Providerklassen oder Fehlercodes weiter. Ein übermächtiger Adapter kann mehrere unabhängige Fähigkeiten vermischen.
Verwandte Techniken
Extract Interface, Anti-Corruption Layer, Gateway und Exception Translation.
Abgrenzung und Entscheidungshilfe
Ein Adapter passt eine Schnittstelle an. Eine Facade vereinfacht ein Subsystem, ohne zwingend inkompatible Modelle zu übersetzen.
Enterprise-Java-Einordnung
BankPaymentAdapter implementiert PaymentPort, übersetzt Money und PaymentRequest in SDK-Objekte und bildet Timeout, Ablehnung und Providerstörung auf typisierte interne Ergebnisse ab.
Teststrategie
Contract-Tests sichern den Port. Mappingtests prüfen Grenzwerte und Fehlerübersetzung. Ein Integrationstest gegen Sandbox oder Stub prüft das echte Protokoll.
LegacyCustomerAdapter.java
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Adapter
// Adapter Pattern: Legacy-Sprache endet an der Integrationsgrenze.
public final class LegacyCustomerAdapter {
public interface LegacyClient { LegacyCustomer load(String id); }
public record LegacyCustomer(String number, String activeFlag) {}
public record Customer(String id, boolean active) {}
private final LegacyClient client;
public LegacyCustomerAdapter(LegacyClient client){ this.client=client; }
public Customer find(String id){ var raw=client.load(id); return new Customer(raw.number(),"Y".equals(raw.activeFlag())); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Mehrere Anwendungsfälle enthalten wiederholte technische Logik für den Zugriff auf ein entferntes System, eine Datenbankfunktion oder einen fremden Dienst. Endpunkte, Authentisierung, Timeouts und Mapping sind verteilt.
Erkennungsmerkmale
HTTP-URLs, SQL oder SDK-Aufrufe in Use Cases; wiederholte Header- und Fehlerbehandlung; technische DTOs in Fachsignaturen; uneinheitliche Timeout- oder Retryregeln.
Nutzen der Transformation
Ein Gateway bildet einen klaren Zugang zu einem externen System. Es bündelt Protokollwissen, technische Policies und Datentransformationen, während der Anwendungsfall mit fachlich benannten Operationen arbeitet.
Sicherer Refactoring-Ablauf
Verteilte Zugriffslogik und gemeinsame Operationen sammeln.
Eine fachlich benannte Gateway-Schnittstelle definieren.
Protokoll- und Mappinglogik in eine Implementierung verschieben.
Aufrufer schrittweise umstellen und Duplikate entfernen.
Timeout-, Retry- und Observability-Regeln am Gateway zentral testen.
Einsatzgrenze
Ein Gateway sollte keine komplette fremde API spiegeln. Es modelliert nur die vom eigenen System benötigten Fähigkeiten. Für reine Persistenz fachlicher Aggregate ist meist Repository passender.
Risiken und Fehlanwendungen
Ein God Gateway mit dutzenden unabhängigen Operationen wird zum neuen Kopplungsknoten. Werden fachliche Entscheidungen hineingeschoben, verschwimmt die Schichtgrenze.
Verwandte Techniken
Adapter, Repository, Facade, Circuit Breaker und Anti-Corruption Layer.
Abgrenzung und Entscheidungshilfe
Gateway bezeichnet den Zugang zu einem externen System; Adapter betont die Übersetzung inkompatibler Schnittstellen. Häufig ist eine Klasse beides.
Enterprise-Java-Einordnung
CustomerRegistryGateway kapselt Abfrage und Aktualisierung eines behördlichen Registers. Der Use Case sieht CustomerStatus statt HTTP-Statuscodes und JSON-Strukturen.
Teststrategie
Gateway-Contract-Tests, Protokolltests mit Stubserver sowie Fehlerklassifikation für Timeout, Nichtgefunden und fachliche Ablehnung.
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Anwendungs- oder Domänenlogik enthält SQL, ORM-Abfragen oder technische Persistenzoperationen. Fachliche Regeln hängen dadurch von Tabellen, Sessions und Ladeverhalten ab.
Erkennungsmerkmale
EntityManager oder JdbcTemplate im Use Case, Querystrings neben Fachlogik, technische Persistence Entities außerhalb des Adapters, wiederholte Abfragen für dieselben Aggregate.
Nutzen der Transformation
Ein Repository stellt aus Sicht der Domäne eine Sammlung fachlicher Aggregate dar. Es kapselt Persistenzzugriff und erlaubt fachlich benannte Lade- und Speicheroperationen.
Sicherer Refactoring-Ablauf
Aggregate und deren Identitätsgrenzen bestimmen.
Nur benötigte fachliche Repositoryoperationen definieren.
Bestehende SQL-/ORM-Logik in einen Adapter verschieben.
Mapping zwischen Persistenz- und Domänenmodell an der Grenze etablieren.
Use Cases auf den Repositoryvertrag umstellen.
Einsatzgrenze
Nicht jede Tabelle benötigt ein Repository. Read-only-Auswertungen können über Query Objects oder spezialisierte Read Models besser modelliert werden. Ein Repository sollte Aggregate, nicht beliebige Tabellenzeilen, verwalten.
Risiken und Fehlanwendungen
Generische CRUD-Repositories geben technische Operationen als Fachvertrag aus. Leaky Repositories liefern Streams, Sessions oder JPA Entities und machen die Domäne wieder abhängig.
Verwandte Techniken
Data Mapper, Unit of Work, Query Object, Aggregate und Gateway.
Abgrenzung und Entscheidungshilfe
DAO ist oft datenquellen- und tabellenorientiert. Repository ist domänenorientiert und spricht in Aggregaten und fachlichen Suchbegriffen.
Enterprise-Java-Einordnung
OrderRepository lädt und speichert Order-Aggregate. Ein JpaOrderRepository übernimmt EntityGraph, Mapping und optimistisches Locking, ohne diese Details an den Use Case weiterzugeben.
Teststrategie
Repository-Contract-Tests laufen gegen In-Memory-Fake und echte Datenbankimplementierung. Integrationstests prüfen Mapping, Nebenläufigkeit und Transaktionsverhalten.
OrderRepositoryBoundary.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Repository
// Repository Pattern: Persistenz wird als Aggregat-Sammlung modelliert.
public interface OrderRepositoryBoundary {
Optional<Order> findById(String id); void save(Order order);
record Order(String id, String status) { public Order { Objects.requireNonNull(id); Objects.requireNonNull(status); } }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Ein Geschäftsfall verändert mehrere Objekte und Persistenzoperationen, ohne dass klar ist, welche Änderungen gemeinsam erfolgreich sein müssen. Einzelne Saves können Teilzustände hinterlassen.
Erkennungsmerkmale
Mehrere Repository.save-Aufrufe in einem Use Case, manuelle Transaktionssteuerung an vielen Stellen, unklare Reihenfolge von Inserts/Updates, doppelte Speicherung desselben Objekts.
Nutzen der Transformation
Eine Unit of Work verfolgt Änderungen innerhalb eines Geschäftsfalls und koordiniert deren atomaren Abschluss. Die Transaktionsgrenze wird explizit und technisch gebündelt.
Sicherer Refactoring-Ablauf
Geschäftliche Konsistenzgrenze und beteiligte Aggregate bestimmen.
Aktuelle Schreiboperationen und Commitpunkte erfassen.
Eine Unit-of-Work-Abstraktion oder klaren Transaction Runner einführen.
Schreiboperationen innerhalb dieser Grenze ausführen.
Commit, Rollback und Fehlerübersetzung zentralisieren.
Einsatzgrenze
Eine Unit of Work darf nicht über lange Benutzerinteraktionen oder entfernte Systemgrenzen offen bleiben. Für verteilte Abläufe sind Saga, Outbox oder Process Manager geeigneter.
Risiken und Fehlanwendungen
Eine globale Unit of Work für den gesamten Request kann unnötig viele Objekte binden. Versteckte automatische Flushes erschweren Vorhersagbarkeit. Transaktionen dürfen keine langsamen Remote-Aufrufe umschließen.
Verwandte Techniken
Repository, Transaction Boundary, Transactional Outbox und Aggregate.
Abgrenzung und Entscheidungshilfe
Repository kapselt Zugriff auf Aggregate. Unit of Work koordiniert mehrere Änderungen und ihren gemeinsamen Commit.
Enterprise-Java-Einordnung
ApproveClaimUseCase ändert Claim und PaymentInstruction in einer lokalen Transaktion. Ein Outbox-Ereignis wird im selben Commit geschrieben; die externe Auszahlung erfolgt danach.
Teststrategie
Integrationstests erzwingen Fehler zwischen Schreibschritten und prüfen vollständigen Rollback. Nebenläufigkeitstests prüfen Konflikte und Wiederholung.
SettlementUnitOfWork.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Unit of Work
// Unit of Work Pattern: Zusammengehörige Änderungen werden atomar gesammelt.
public final class SettlementUnitOfWork {
private final List<Runnable> changes=new ArrayList<>();
public void register(Runnable change){ changes.add(Objects.requireNonNull(change)); }
public void commit(){ var snapshot=List.copyOf(changes); snapshot.forEach(Runnable::run); changes.clear(); }
public int pending(){ return changes.size(); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Objekte werden an vielen Stellen erzeugt und verdrahtet. Konfiguration, konkrete Adapter und Fachlogik vermischen sich, wodurch der tatsächliche Objektgraph schwer nachvollziehbar ist.
Erkennungsmerkmale
new-Aufrufe für Services in Controllern und Use Cases, Containerzugriff aus Fachcode, mehrere konkurrierende Konfigurationsorte, Laufzeitfehler durch uneindeutige Beans.
Nutzen der Transformation
Ein Composition Root ist der zentrale äußere Ort, an dem konkrete Implementierungen gewählt und der Objektgraph aufgebaut wird. Fachmodule bleiben frei von Container- und Umgebungswissen.
Sicherer Refactoring-Ablauf
Alle Erzeugungs- und Verdrahtungsstellen erfassen.
Abhängigkeiten in Fach- und Anwendungsklassen explizit machen.
Produktive Adapter in einer Bootstrap- oder Konfigurationskomponente zusammenführen.
Umgebungs- und Variantenentscheidungen ausschließlich dort treffen.
Direkte Containerzugriffe und verstreute Fabriken entfernen.
Einsatzgrenze
Bei großen modularen Systemen kann jedes Feature einen lokalen Composition Root besitzen, der von einem übergeordneten Bootstrap zusammengesetzt wird. Zentral bedeutet nicht zwingend eine einzige riesige Datei.
Risiken und Fehlanwendungen
Ein Composition Root mit Fachlogik wird unübersichtlich. Reflektive Magie und automatische Scans können die sichtbare Verdrahtung wieder verbergen. Zyklen sollten nicht durch Lazy-Auflösung kaschiert werden.
Verwandte Techniken
Dependency Injection, Factory, Module und Feature Composition.
Abgrenzung und Entscheidungshilfe
Composition Root ist ein Ort im Systemdesign. Ein DI-Container ist ein Werkzeug, das dort verwendet werden kann.
Enterprise-Java-Einordnung
Eine Spring-Konfiguration verbindet OrderApplicationService mit JpaOrderRepository, BankPaymentAdapter, SystemClock und OutboxPublisher. Das Domänenmodul kennt Spring nicht.
Teststrategie
Ein Context-Smoke-Test prüft den vollständigen Objektgraphen. Architekturrules verhindern Frameworkabhängigkeiten in Kernmodulen.
BillingCompositionRoot.java
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Composition Root
// Composition Root: Nur hier werden konkrete Adapter mit dem Use Case verdrahtet.
public final class BillingCompositionRoot {
public interface InvoiceStore { void save(String invoiceId); }
public static final class BillingUseCase { private final InvoiceStore store; BillingUseCase(InvoiceStore store){ this.store=store; } public void create(String id){ store.save(id); } }
private BillingCompositionRoot(){}
public static BillingUseCase create(InvoiceStore store){ return new BillingUseCase(store); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Kapitel 10 Value Objects und fachliche Modelle einführen10 Refactorings
Fachtypen, Regeln und Variantenachsen mit kompilierbarem Java-21-Zielcode.
91. Introduce Value Object
Fachliches Ausgangsproblem
Fachlich bedeutsame Werte werden als String, int oder BigDecimal weitergegeben. Einheit, Format, Gültigkeit und Vergleichsregeln sind nicht am Typ erkennbar.
Erkennungsmerkmale
Parameter wie String email, String country, BigDecimal amount; wiederholte Validierung und Normalisierung; vertauschte Parameter gleichen Typs; primitive Obsession.
Nutzen der Transformation
Ein Value Object bündelt Wert, Invarianten und fachliche Operationen. Gleichheit basiert auf dem Inhalt, der Typ verhindert Verwechslungen und ungültige Zustände werden früh abgewiesen.
Sicherer Refactoring-Ablauf
Einen stabilen fachlichen Begriff mit klaren Invarianten auswählen.
Einen unveränderlichen Typ mit validierender Fabrik oder Konstruktor einführen.
Zunächst eine Signatur auf den neuen Typ umstellen.
Verteilte Validierung und Formatierung in den Typ verschieben.
Persistenz- und Transportmapping an den Systemgrenzen ergänzen.
Einsatzgrenze
Ein Value Object sollte klein, unveränderlich und wertbasiert sein. Objekte mit eigener Identität und Lebenszyklus sind Entities. Nicht jeder String benötigt einen Wrapper.
Risiken und Fehlanwendungen
Ein anämisches Wrapperobjekt ohne Regeln bringt wenig. Zu aggressive Validierung kann Import- und Migrationsfälle blockieren; dann sind Parse-Ergebnis und validierter Wert getrennt zu modellieren.
Verwandte Techniken
Replace Primitive with Object, Embedded Value, Record und Domain Model.
Abgrenzung und Entscheidungshilfe
Value Objects sind nicht bloß DTOs: Sie schützen Invarianten und besitzen fachlich sinnvolles Verhalten.
Enterprise-Java-Einordnung
Money enthält Betrag und Währung, verhindert währungsübergreifende Addition und kapselt Rundungsregeln. EmailAddress normalisiert und validiert Adressen an der Eingangsgrenze.
Teststrategie
Äquivalenz, Hashcode, Grenzwerte, ungültige Konstruktion und fachliche Operationen werden direkt am Typ getestet.
EmailAddress.java
package com.aydinsude.workbench.catalog;
import java.util.Locale;
// Refactoring: Introduce Value Object
// Value Object Pattern: Identität entsteht ausschließlich aus dem fachlichen Wert.
public record EmailAddress(String value) {
public EmailAddress {
if (value == null) throw new IllegalArgumentException("email");
value = value.trim().toLowerCase(Locale.ROOT);
if (!value.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) throw new IllegalArgumentException("invalid email");
}
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine rohe List oder Set enthält fachlich zusammengehörige Elemente, während Regeln über Anzahl, Duplikate, Reihenfolge und Aggregation in verschiedenen Services verteilt sind.
Erkennungsmerkmale
Wiederholte Schleifen über dieselbe Sammlung, size()-Prüfungen an vielen Stellen, Entfernen oder Hinzufügen ohne Invarianten, Methoden mit List<DomainType> in zahlreichen Signaturen.
Nutzen der Transformation
Eine Domain Collection gibt der Menge einen fachlichen Namen und kapselt Invarianten sowie häufige Operationen. Aufrufer sprechen über OrderLines oder Approvers statt über technische Listen.
Sicherer Refactoring-Ablauf
Wiederkehrende Regeln und Operationen der Sammlung erfassen.
Einen unveränderlichen oder kontrolliert mutierbaren Sammlungstyp einführen.
Erzeugung und Änderungsoperationen mit Invarianten versehen.
Signaturen schrittweise auf den Domänentyp umstellen.
Direkte Freigabe der internen Collection durch defensive Sichten ersetzen.
Einsatzgrenze
Bei rein technischen Zwischenlisten ohne eigene Regeln genügt die Standard-Collection. Eine Domain Collection ist sinnvoll, wenn die Menge einen eigenen Begriff und Verhalten besitzt.
Risiken und Fehlanwendungen
Das bloße Delegieren sämtlicher List-Methoden erzeugt einen unnötigen Wrapper. Mutierbare interne Listen dürfen nicht nach außen leaken.
Verwandte Techniken
Encapsulate Collection, Value Object, Aggregate und Composite.
Abgrenzung und Entscheidungshilfe
Eine Domain Collection ist ein Fachtyp; ein Repository repräsentiert dagegen eine persistierte Sammlung von Aggregaten.
Enterprise-Java-Einordnung
OrderLines verhindert doppelte Positionsnummern, berechnet Total und liefert nur fachliche Operationen addProduct, removeLine und taxableTotal.
Teststrategie
Invarianten bei Erzeugung und Änderung, defensive Kopien, Aggregationen und Gleichheit der Collection testen.
OrderLines.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Replace Collection with Domain Collection
// Domain Collection: Invarianten und Aggregatoperationen liegen bei der Sammlung.
public final class OrderLines {
public record Line(String productId,int quantity,long unitCents){ public Line { if(productId==null||productId.isBlank()||quantity<1||unitCents<0) throw new IllegalArgumentException("line"); } }
private final List<Line> lines;
public OrderLines(List<Line> lines){ if(lines==null||lines.isEmpty()) throw new IllegalArgumentException("empty"); var ids=new HashSet<String>(); for(var l:lines) if(!ids.add(l.productId())) throw new IllegalArgumentException("duplicate"); this.lines=List.copyOf(lines); }
public long totalCents(){ return lines.stream().mapToLong(l->l.unitCents()*l.quantity()).sum(); }
public List<Line> asList(){ return lines; }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine fachliche Operation gehört zu keinem einzelnen Entity oder Value Object, weil sie mehrere Aggregate oder externe Fachinformationen kombiniert. Sie landet deshalb in einem technischen Application Service oder Utility.
Erkennungsmerkmale
Fachformeln in Controllern, statische DomainUtils, Methoden mit mehreren fachlichen Objekten ohne Infrastruktur, Logik die künstlich in eine Entity gedrückt wurde.
Nutzen der Transformation
Ein Domain Service erhält einen fachlichen Namen und kapselt domänenreine Logik, die nicht natürlich einem Objekt gehört. Das Modell bleibt ausdrucksstark, ohne Entities zu überladen.
Sicherer Refactoring-Ablauf
Prüfen, ob das Verhalten wirklich in Entity oder Value Object gehört.
Benötigte fachliche Eingaben und Ergebnis als klare Typen bestimmen.
Eine zustandslose, domänensprachlich benannte Serviceklasse einführen.
Logik aus technischen Schichten verschieben.
Infrastrukturzugriffe über vorbereitete Werte oder Ports außerhalb halten.
Einsatzgrenze
Orchestrierung, Transaktionen und technische Aufrufe gehören in Application Services. Ein Domain Service ist für fachliche Regeln, nicht als Sammelplatz für beliebige Logik.
Risiken und Fehlanwendungen
Zu viele Domain Services führen zu anämischen Entities. Ein Service mit Repository-, HTTP- und Messagingzugriff ist meist Application Service oder Use Case.
Verwandte Techniken
Domain Model, Policy Object, Specification und Application Service.
Abgrenzung und Entscheidungshilfe
Domain Service entscheidet fachlich. Application Service koordiniert Ablauf, Transaktion und Ports.
Enterprise-Java-Einordnung
CreditLimitService berechnet aus Kundenrisiko, offenen Forderungen und beantragtem Betrag eine fachliche Kreditentscheidung, ohne Datenbank oder HTTP zu kennen.
Teststrategie
Reine Unit-Tests decken Entscheidungstabellen, Grenzwerte und Symmetrieeigenschaften ab. Keine Frameworkumgebung erforderlich.
TransferDomainService.java
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Domain Service
// Domain Service Pattern: Eine fachliche Operation koordiniert mehrere Entities.
public final class TransferDomainService {
public static final class Account { private long cents; public Account(long cents){ if(cents<0) throw new IllegalArgumentException(); this.cents=cents; } public long balance(){ return cents; } void debit(long amount){ if(amount<=0||amount>cents) throw new IllegalArgumentException("funds"); cents-=amount; } void credit(long amount){ if(amount<=0) throw new IllegalArgumentException("amount"); cents+=amount; } }
public void transfer(Account source,Account target,long cents){ if(source==target) throw new IllegalArgumentException("same account"); source.debit(cents); target.credit(cents); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine veränderliche Geschäftsregel ist als verschachtelte Bedingung in einem großen Service eingebettet. Varianten nach Land, Produkt, Risiko oder Kundengruppe führen zu wachsenden if-Ketten.
Erkennungsmerkmale
Bedingungen mit vielen fachlichen Parametern, häufige Regeländerungen, mehrere Versionen derselben Entscheidung, schwer benennbare Booleans.
Nutzen der Transformation
Ein Policy Object macht eine Regel als eigenständiges, testbares Konzept sichtbar. Eingaben und Entscheidung werden typisiert, Varianten können gezielt ausgetauscht oder versioniert werden.
Sicherer Refactoring-Ablauf
Die fachliche Entscheidung und ihre Einflussgrößen isolieren.
Einen benannten Eingabekontext und typisiertes Ergebnis definieren.
Die bestehende Regel unverändert in ein Policy Object verschieben.
Aufrufer über einen klaren Vertrag anbinden.
Varianten nur bei echtem Änderungsdruck als Strategien ergänzen.
Einsatzgrenze
Eine triviale, stabile Bedingung benötigt kein eigenes Objekt. Policy Objects sind besonders wertvoll bei eigenständigem Vokabular, Versionierung oder mehreren Varianten.
Risiken und Fehlanwendungen
Eine Policy darf keine Orchestrierung oder Persistenz übernehmen. Dutzende winzige Policies ohne erkennbare Fachgrenze erschweren den Ablauf.
Verwandte Techniken
Strategy, Specification, Decision Object und Domain Service.
Abgrenzung und Entscheidungshilfe
Eine Specification beantwortet typischerweise, ob ein Objekt ein Kriterium erfüllt. Eine Policy kann eine reichere Entscheidung mit Begründung und Folgeschritten liefern.
Enterprise-Java-Einordnung
RefundPolicy entscheidet anhand Betrag, Lieferstatus, Kundensegment und Frist über automatische Erstattung, manuelle Prüfung oder Ablehnung.
Teststrategie
Entscheidungstabellen decken alle Regelkombinationen ab. Versions- und Mandantenvarianten erhalten gemeinsame Contract-Tests.
RefundPolicy.java
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Policy Object
// Policy Pattern: Veränderliche Fachregeln werden als explizite Entscheidung gekapselt.
public final class RefundPolicy {
public record Decision(boolean allowed,String reason) {}
private final Duration window;
public RefundPolicy(Duration window){ if(window==null||window.isNegative()||window.isZero()) throw new IllegalArgumentException("window"); this.window=window; }
public Decision evaluate(Instant paidAt,Instant requestedAt,boolean consumed){ if(consumed) return new Decision(false,"already-consumed"); if(requestedAt.isAfter(paidAt.plus(window))) return new Decision(false,"window-expired"); return new Decision(true,"eligible"); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine Methode wählt anhand eines Typs oder Merkmals zwischen mehreren vollständigen Algorithmen. Jede neue Variante verändert dieselbe zentrale if-else-Kette.
Erkennungsmerkmale
Lange Kette auf paymentType, channel, country oder customerType; Varianten haben eigene Berechnungsschritte; Tests benötigen viele Verzweigungskombinationen; häufige Merge-Konflikte.
Nutzen der Transformation
Strategy trennt austauschbare Algorithmen hinter einem gemeinsamen Vertrag. Jede Variante kann unabhängig entwickelt und getestet werden; die Auswahl wird von der Ausführung getrennt.
Sicherer Refactoring-Ablauf
Gemeinsamen Eingabe- und Ergebnisvertrag der Varianten bestimmen.
Eine Strategy-Schnittstelle einführen.
Eine bestehende Verzweigung in eine konkrete Strategy verschieben.
Schrittweise weitere Varianten extrahieren.
Auswahl über Resolver, Registry oder Composition Root zentralisieren.
Einsatzgrenze
Bei zwei sehr kleinen, stabilen Ausdrücken ist eine Strategiehierarchie übertrieben. Wenn Verhalten vom internen Lebenszykluszustand abhängt, ist State oft passender.
Risiken und Fehlanwendungen
Ein großer Context mit vielen optionalen Feldern zeigt einen zu breiten Vertrag. Eine Registry darf unbekannte Varianten nicht stillschweigend auf einen Default abbilden.
Verwandte Techniken
Replace Conditional with Polymorphism, State, Factory und Registry.
Abgrenzung und Entscheidungshilfe
Strategy wird vom Aufrufer oder Resolver ausgewählt. State wechselt typischerweise aufgrund interner Zustandsübergänge des Contexts.
Enterprise-Java-Einordnung
ShippingCostStrategy berechnet Kosten für Standard, Express und Abholung. Ein Resolver wählt anhand des Lieferprodukts, während der Checkout nur den Strategy-Vertrag kennt.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Replace If-Else Chain with Strategy
// Strategy Pattern: Varianten werden ohne wachsende Verzweigung ergänzt.
public final class DeliveryPricing {
public enum Mode { STANDARD, EXPRESS, PICKUP }
public interface Strategy { long price(long basketCents); }
private final Map<Mode,Strategy> strategies;
public DeliveryPricing(Map<Mode,Strategy> strategies){ this.strategies=Map.copyOf(strategies); }
public long price(Mode mode,long basketCents){ var strategy=strategies.get(mode); if(strategy==null) throw new IllegalArgumentException("mode"); return strategy.price(basketCents); }
public static DeliveryPricing defaults(){ return new DeliveryPricing(Map.of(Mode.STANDARD,b->b>=5000?0:490,Mode.EXPRESS,b->990,Mode.PICKUP,b->0)); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Ein switch über einen Befehlstyp, Ereignisnamen oder Dokumenttyp enthält pro Fall Validierung, Ausführung und Fehlerbehandlung. Neue Typen verändern eine zentrale Dispatch-Methode.
Erkennungsmerkmale
switch auf enum oder String in einem Dispatcher, wiederholte Casts, pro Fall deutlich unterschiedliche Abhängigkeiten, zentraler Code mit hoher Änderungsfrequenz.
Nutzen der Transformation
Polymorphe Handler kapseln typbezogenes Verhalten und ihre Abhängigkeiten. Ein Dispatcher ordnet Typ zu Handler, ohne die Implementierung jedes Falls zu kennen.
Sicherer Refactoring-Ablauf
Die Fälle und ihren gemeinsamen Dispatch-Vertrag erfassen.
Einen typisierten Handlervertrag definieren.
Einen Fall vollständig in einen Handler verschieben.
Handler über eine explizite Registry registrieren.
Switch entfernen, sobald alle Fälle migriert und unbekannte Typen definiert behandelt sind.
Einsatzgrenze
Ein switch zur einfachen Wertabbildung kann lesbarer sein. Handler lohnen sich, wenn Fälle eigenes Verhalten, Abhängigkeiten oder Tests besitzen.
Risiken und Fehlanwendungen
Stringbasierte Handlerkeys bleiben fehleranfällig. Eine automatische Classpath-Suche kann Mehrdeutigkeiten verstecken. Handler dürfen keine gemeinsame Transaktionsregel widersprüchlich implementieren.
Verwandte Techniken
Command, Strategy Registry, Factory und Message Router.
Abgrenzung und Entscheidungshilfe
Der polymorphe Handler ist besonders für typisierte Nachrichten oder Befehle geeignet; Strategy beschreibt eher austauschbare Algorithmen für denselben Kontext.
Enterprise-Java-Einordnung
DocumentHandler verarbeitet Invoice, Contract und ClaimDocument. Jeder Handler validiert und transformiert seinen Typ; die Registry lehnt unbekannte Typen explizit ab.
Teststrategie
Contract-Tests prüfen Handlerverträge. Registrytests prüfen doppelte und fehlende Registrierung. Jeder Handler erhält eigene Fachtests.
DocumentExportHandlers.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Replace Switch with Polymorphic Handler
// Polymorphism + Registry: Jeder Handler besitzt genau ein Formatverhalten.
public final class DocumentExportHandlers {
public interface Handler { String format(); byte[] export(List<String> rows); }
private final Map<String,Handler> handlers;
public DocumentExportHandlers(List<Handler> handlers){ var map=new HashMap<String,Handler>(); for(var h:handlers) if(map.put(h.format(),h)!=null) throw new IllegalArgumentException("duplicate"); this.handlers=Map.copyOf(map); }
public byte[] export(String format,List<String> rows){ var h=handlers.get(format); if(h==null) throw new IllegalArgumentException("format"); return h.export(List.copyOf(rows)); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Zustandsabhängiges Verhalten ist über Statusfelder, Booleans und verstreute Bedingungen verteilt. Ungültige Übergänge sind möglich und erlaubte Folgeaktionen nicht zentral erkennbar.
Erkennungsmerkmale
Mehrere Felder wie approved, paid, closed; switch(status) in vielen Methoden; direkte setStatus-Aufrufe; Produktionsfehler durch übersprungene Zustände.
Nutzen der Transformation
Eine explizite State Machine modelliert Zustände, Ereignisse, erlaubte Übergänge und Aktionen als überprüfbaren Vertrag. Ungültige Übergänge werden an einer Stelle abgewiesen.
Sicherer Refactoring-Ablauf
Bestehende Zustände und tatsächlich beobachtete Übergänge dokumentieren.
Characterization Tests für gültige und ungültige Abläufe erstellen.
Einen zentralen Übergangsvertrag mit Ereignis und Ergebnis einführen.
Direkte Statusänderungen schrittweise durch transition(event) ersetzen.
Nebenwirkungen von Zustandsänderung trennen und nach erfolgreichem Übergang auslösen.
Einsatzgrenze
Für einen linearen Ablauf mit zwei stabilen Zuständen genügt oft eine validierende Methode. Eine State Machine ist sinnvoll bei mehreren Übergängen, Rollen oder zeitlichen Regeln.
Risiken und Fehlanwendungen
Diagramm und Code dürfen nicht auseinanderlaufen. Versteckte Seiteneffekte in Transitionen erschweren Wiederholung. Persistierte Zustände benötigen Migrationsstrategie.
Verwandte Techniken
State Pattern, Workflow Object, Process Manager und Decision Object.
Abgrenzung und Entscheidungshilfe
State Pattern verteilt Verhalten auf Zustandsobjekte. Eine explizite State Machine kann auch tabellen- oder regelbasiert implementiert sein.
Enterprise-Java-Einordnung
ClaimStateMachine erlaubt SUBMIT, APPROVE, REJECT und PAY abhängig vom aktuellen ClaimStatus. Jede Ablehnung enthält einen fachlichen Grund.
Teststrategie
Eine Übergangsmatrix prüft jede Kombination aus Zustand und Ereignis. Property-Tests sichern, dass geschlossene Zustände keine Änderungen mehr erlauben.
TicketStateMachine.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Explicit State Machine
// State Machine Pattern: Erlaubte Übergänge sind zentral und ausführbar dokumentiert.
public final class TicketStateMachine {
public enum State { NEW, ASSIGNED, RESOLVED, CLOSED }
private static final Map<State,Set<State>> ALLOWED=Map.of(State.NEW,Set.of(State.ASSIGNED),State.ASSIGNED,Set.of(State.RESOLVED),State.RESOLVED,Set.of(State.CLOSED),State.CLOSED,Set.of());
public State transition(State from,State to){ if(!ALLOWED.getOrDefault(from,Set.of()).contains(to)) throw new IllegalStateException(from+" -> "+to); return to; }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Ein optionaler Kollaborator führt zu wiederholten null-Prüfungen. Der fehlende Fall besitzt dabei ein wohldefiniertes neutrales Verhalten, etwa keine Benachrichtigung oder keinen Rabatt.
Erkennungsmerkmale
if (service != null) an vielen Stellen, optionale No-op-Konfiguration, Tests für identische Nullpfade, NullPointer-Risiken nach neuen Aufrufen.
Nutzen der Transformation
Ein Null Object implementiert denselben Vertrag mit bewusst neutralem Verhalten. Aufrufer können den Vertrag ohne Sonderprüfung verwenden und bleiben frei von Nullzweigen.
Sicherer Refactoring-Ablauf
Prüfen, ob für das Fehlen wirklich ein eindeutiges neutrales Verhalten existiert.
Einen kleinen Vertrag und eine No-op-Implementierung definieren.
Erzeugungs- oder Verdrahtungsstelle so ändern, dass niemals null geliefert wird.
Nullprüfungen schrittweise entfernen.
Observability prüfen, damit wichtige Fehlkonfigurationen nicht unsichtbar werden.
Einsatzgrenze
Wenn das Fehlen fachlich relevant ist oder vom Aufrufer behandelt werden muss, sind Optional, Result Type oder Special Case besser. Null Object darf Fehler nicht verschlucken.
Risiken und Fehlanwendungen
Ein stilles Null Object kann fehlende Konfiguration verbergen. Neutrales Verhalten muss tatsächlich alle Invarianten wahren und darf keine falschen Erfolgsannahmen erzeugen.
Verwandte Techniken
Introduce Special Case, Optional Result, Result Type und No-op Adapter.
Abgrenzung und Entscheidungshilfe
Optional macht Abwesenheit sichtbar. Null Object macht sie für Aufrufer transparent. Die Wahl hängt davon ab, ob der fehlende Fall behandelt werden muss.
Enterprise-Java-Einordnung
NoOpAuditPublisher ist nur in lokalen Tests zulässig; produktive Profile verlangen einen echten AuditPublisher. Ein NoDiscountPolicy liefert Money.zero statt null.
Teststrategie
Contract-Tests prüfen, dass das Null Object den Vertrag erfüllt, keine Seiteneffekte auslöst und definierte neutrale Ergebnisse liefert.
CustomerDiscount.java
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Null Object
// Null Object Pattern: Fehlende Variante besitzt neutrales Verhalten statt null.
public final class CustomerDiscount {
public interface Discount { long apply(long cents); }
public static final Discount NONE=cents->cents;
private final Discount discount;
public CustomerDiscount(Discount discount){ this.discount=discount==null?NONE:discount; }
public long finalPrice(long cents){ if(cents<0) throw new IllegalArgumentException("price"); return discount.apply(cents); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Fachliche Auswahl- oder Zulassungsregeln sind als lange Prädikate in Services, Repositories und Controllern dupliziert. Kombinationen aus UND, ODER und NICHT sind schwer lesbar.
Erkennungsmerkmale
Wiederholte Bedingungen über dasselbe Fachobjekt, Methoden wie isEligible mit vielen Klauseln, inkonsistente Filter zwischen UI und Backend, Regeln mit eigenem Fachnamen.
Nutzen der Transformation
Eine Specification kapselt ein benanntes Kriterium und kann mit anderen Spezifikationen kombiniert werden. Fachsprache und Wiederverwendung werden sichtbar, ohne die Entity mit jeder Abfrage zu belasten.
Sicherer Refactoring-Ablauf
Eine wiederkehrende fachliche Bedingung mit klarem Namen auswählen.
Einen Specification-Vertrag über den relevanten Typ einführen.
Die Bedingung unverändert in eine konkrete Specification verschieben.
Komposition für and, or und not kontrolliert ergänzen.
Duplikate in Services und Queries schrittweise ersetzen.
Einsatzgrenze
Ein einmaliges triviales Predicate benötigt keine Klasse. Für Entscheidungen mit mehreren Ergebnissen und Begründungen ist Policy oder Decision Object geeigneter.
Risiken und Fehlanwendungen
Eine generische Specification-Bibliothek kann Fachsprache verdrängen. Datenbankübersetzung und In-Memory-Auswertung dürfen nicht unbemerkt unterschiedliche Semantik haben.
Verwandte Techniken
Predicate, Policy Object, Query Object und Decision Table.
Abgrenzung und Entscheidungshilfe
Specification beschreibt ein Kriterium. Strategy führt einen Algorithmus aus. Policy kann eine reichere Entscheidung treffen.
Enterprise-Java-Einordnung
EligibleForAutomaticRefund kombiniert WithinReturnWindow, NotFraudSuspected und AmountBelowLimit. Die Regel kann im Use Case erklärt und separat getestet werden.
Teststrategie
Beispiel- und Grenzwerttests je Specification, Wahrheitstabellen für Kompositionen und Konsistenztests zwischen In-Memory- und Persistenzabfrage.
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Eine Methode liefert true oder false, obwohl der Aufrufer zusätzlich Grund, Regelversion, Grenzwert oder nächsten Schritt benötigt. Daraufhin wird die Entscheidung an anderer Stelle erneut berechnet oder mit Zusatzabfragen ergänzt.
Erkennungsmerkmale
Methoden wie canApprove oder isValid mit anschließenden if-Blöcken, Logmeldungen ohne konkreten Grund, parallele Methoden reasonForRejection, mehrere Booleans für denselben Entscheidungsprozess.
Nutzen der Transformation
Ein Decision Object macht Ergebnis und Begründung atomar. Es kann Status, Reason Code, erklärende Fakten und empfohlene Aktion typisiert transportieren, ohne dass Aufrufer Regeln duplizieren.
Sicherer Refactoring-Ablauf
Alle Informationen erfassen, die Aufrufer nach der Boolean-Entscheidung benötigen.
Einen kleinen unveränderlichen Decision-Typ mit Status und fachlichem Grund definieren.
Bestehende Methode intern auf Decision umstellen und vorübergehend einen Boolean-Adapter anbieten.
Aufrufer schrittweise auf die reichere Entscheidung migrieren.
Alte Boolean-Methode entfernen, sobald keine Doppelberechnung mehr existiert.
Einsatzgrenze
Wenn die Antwort wirklich vollständig binär ist und kein Grund oder Folgeschritt benötigt wird, bleibt boolean angemessen. Decision Objects dürfen nicht zu beliebigen Diagnosecontainern anwachsen.
Risiken und Fehlanwendungen
Freitextgründe erschweren Automatisierung und Übersetzung; besser sind typisierte Reason Codes plus optionale Parameter. Das Decision Object darf keine spätere mutable Prozessakte werden.
Verwandte Techniken
Result Type, Policy Object, Validation Result und State Transition Result.
Abgrenzung und Entscheidungshilfe
Ein Result Type modelliert häufig Erfolg oder technischen/fachlichen Fehler einer Operation. Ein Decision Object beschreibt bewusst das Ergebnis einer Regelbewertung.
Enterprise-Java-Einordnung
CreditDecision enthält APPROVED, MANUAL_REVIEW oder REJECTED, einen ReasonCode, verwendete PolicyVersion und Limit. API, Audit und Workflow nutzen dasselbe Ergebnis.
Teststrategie
Entscheidungstabellen prüfen Status und Reason Code gemeinsam. Serialisierungs- und Kompatibilitätstests sichern öffentliche Decision-Verträge.
CreditDecision.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Boolean Return with Decision Object
// Decision Object Pattern: Ergebnis, Begründung und nächster Schritt bleiben zusammen.
public final class CreditDecision {
public enum Status { APPROVED, REVIEW, REJECTED }
public record Decision(Status status,String reason,String nextAction) {}
public Decision decide(int score,long exposureCents){ if(score<500) return new Decision(Status.REJECTED,"score-too-low","notify-customer"); if(exposureCents>500000) return new Decision(Status.REVIEW,"high-exposure","manual-review"); return new Decision(Status.APPROVED,"policy-satisfied","create-contract"); }
}
Deutschsprachige fachliche Grundlagen
Die fachliche Erklärung wurde aus deutschsprachigen Hochschul-, Hersteller- und Überblicksquellen redaktionell zusammengeführt und auf das vorhandene Java-21-Beispiel übertragen. Sie ist keine wortwörtliche Übersetzung oder Kopie einer einzelnen Quelle.
Wirkung: Eventdaten werden zu einem stabilen, typisierten Vertrag.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein fachlich bedeutsames Geschehen wird nur durch einen Methodenaufruf, mehrere lose Parameter oder eine technische Nachricht dargestellt. Zeitpunkt, Verursacher, fachliche Identität und relevante Fakten sind nicht gemeinsam modelliert. Dadurch können nachgelagerte Reaktionen das Geschehen weder zuverlässig protokollieren noch unabhängig verarbeiten.
Erkennungsmerkmale
Mehrere Empfänger erhalten dieselben losen Parameter; Methoden heißen notifySomething oder handleChange, ohne das Ereignis als Typ auszudrücken; Audit, Messaging und Fachreaktionen bauen jeweils eigene Datenstrukturen; spätere Erweiterungen verändern viele Signaturen.
Ziel und Nutzen
Ein unveränderliches Event Object hält die fachlich relevanten Tatsachen eines bereits eingetretenen Ereignisses zusammen. Es gibt dem Geschehen einen Namen, stabilisiert den Vertrag und trennt die Entstehung des Ereignisses von seinen Reaktionen.
Sicherer Refactoring-Ablauf
Das fachlich eingetretene Geschehen in der Vergangenheitsform benennen.
Nur Fakten aufnehmen, die zum Ereigniszeitpunkt feststanden; keine Services oder veränderlichen Entitäten einbetten.
Eindeutige Ereigniskennung, Zeitpunkt und fachliche Referenzen typisieren.
Bisherige lose Parameter schrittweise durch das Event Object ersetzen.
Verbraucher über eigene Handler anbinden und Versionierungsregeln festlegen.
Wann sinnvoll - und wann nicht
Ein Event Object ist sinnvoll, wenn ein Geschehen mehrere Reaktionen, Auditierbarkeit oder zeitliche Entkopplung benötigt. Für einen rein lokalen Funktionsaufruf ohne fachliche Bedeutung wäre ein eigener Ereignistyp unnötig.
Risiken und typische Fehlanwendungen
Ein Event darf nicht zum vollständigen Objekt-Snapshot anwachsen. Werden Befehle als Ereignisse benannt oder zukünftige Absichten aufgenommen, verschwimmt die Semantik. Öffentliche Ereignisse brauchen besonders stabile Verträge.
Abgrenzung
Ein Command fordert eine Handlung an und kann abgelehnt werden. Ein Event beschreibt eine bereits eingetretene Tatsache.
Verwandte Refactorings und Muster
Domain Event, Integration Event, Event Envelope, Observer und Transactional Outbox.
Enterprise-Java-Einordnung
Nach erfolgreicher Policierung erzeugt das Aggregat PolicyIssued mit PolicyId, CustomerId, Tarifversion und Ausstellungszeitpunkt. Rechnung, Dokumenterzeugung und Audit reagieren getrennt darauf.
Teststrategie
Tests prüfen Ereignisinhalt und Entstehungsbedingung. Contract-Tests sichern Serialisierung und Versionierung; Handler-Tests prüfen jede Reaktion unabhängig.
Mappingcode zwischen API-DTO und Domänenobjekt ist über Controller und Service verteilt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere eine reine Mapper-Komponente ohne I/O; halte Mappingregeln zentral und testbar.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CustomerMapper.java
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Mapper
// Mapper Pattern: Übersetzung zwischen externem Vertrag und Domänenmodell ist isoliert.
public final class CustomerMapper {
public record CustomerRequest(String firstName,String lastName,String email) {}
public record Customer(String fullName,EmailAddress email) {}
public Customer toDomain(CustomerRequest request){
if(request==null) throw new IllegalArgumentException("request");
return new Customer((request.firstName()+" "+request.lastName()).trim(),new EmailAddress(request.email()));
}
}
Wirkung: Eine reine Übersetzungsgrenze schützt das Domänenmodell.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Konvertierungen zwischen API-, Persistenz-, Fremdsystem- und Domänenmodellen sind in Controllern, Repositories oder Use Cases verteilt. Feldzuordnungen, Standardwerte und Typkonvertierungen werden wiederholt und vermischen sich mit Fachlogik.
Erkennungsmerkmale
Viele Konstruktoraufrufe mit Feld-für-Feld-Kopien; technische DTOs gelangen in die Domäne; dieselbe Zuordnung existiert in mehreren Richtungen; Mappingfehler werden erst in End-to-End-Tests sichtbar.
Ziel und Nutzen
Ein expliziter Mapper konzentriert die Übersetzung an einer Systemgrenze. Fachlogik bleibt frei von Transport- und Persistenzformaten; Änderungen am äußeren Modell lassen sich an einer Stelle behandeln.
Sicherer Refactoring-Ablauf
Die Grenze und beide beteiligten Modelle eindeutig bestimmen.
Vorhandene Feldzuordnungen und Sonderregeln mit Tests sichern.
Einen richtungsbezogenen Mapper mit klar benannten Methoden einführen.
Konvertierungen aus Controller, Repository oder Use Case in den Mapper verschieben.
Technische DTOs aus inneren Signaturen entfernen.
Wann sinnvoll - und wann nicht
Bei einer einmaligen, trivialen Zuordnung von zwei oder drei Feldern kann eine lokale Factory lesbarer sein. Ein Mapper ist besonders wertvoll, wenn Modelle bewusst unabhängig bleiben oder mehrere Konvertierungsregeln existieren.
Risiken und typische Fehlanwendungen
Bidirektionale Universalmapper verschleiern unterschiedliche Regeln je Richtung. Reflexionsbasierte Automatik kann fachlich relevante Konvertierungen unsichtbar machen. Mapper dürfen keine Fachentscheidungen treffen.
Abgrenzung
Ein Mapper übersetzt Datenrepräsentationen. Ein Adapter übersetzt einen ganzen Verhaltensvertrag und kann Mapper intern verwenden.
Verwandte Refactorings und Muster
Data Mapper, Adapter, Anti-Corruption Layer und Factory Method.
Enterprise-Java-Einordnung
CustomerApiMapper übersetzt REST-Anfragen in RegisterCustomerCommand und CustomerView in Response-DTOs. Ein JpaCustomerMapper trennt zusätzlich Persistenzobjekte vom Aggregat.
Teststrategie
Beispiel- und Grenzwerttests prüfen jede Richtung. Tests müssen Null-, Enum-, Zeit- und Geldkonvertierungen sowie unbekannte Werte abdecken.
Ein veränderliches DTO kann nach Validierung unbemerkt geändert werden.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Ersetze Getter/Setter-Datencontainer durch ein validierendes Record mit defensiven Kopien.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
ShipmentRequest.java
package com.aydinsude.workbench.catalog;
import java.util.List;
// Refactoring: Replace Mutable DTO with Record
// Immutable Data Transfer Object: Der Transportvertrag kann nach Erzeugung nicht driften.
public record ShipmentRequest(String orderId, List<String> packageIds) {
public ShipmentRequest {
if(orderId==null||orderId.isBlank()) throw new IllegalArgumentException("orderId");
packageIds=List.copyOf(packageIds);
if(packageIds.isEmpty()||packageIds.stream().anyMatch(String::isBlank)) throw new IllegalArgumentException("packages");
}
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.*; import org.junit.jupiter.api.Test;
class ShipmentRequestTest { @Test void record_is_immutable(){ var source=new ArrayList<>(List.of("P1")); var r=new ShipmentRequest("O1",source); source.add("P2"); assertEquals(List.of("P1"),r.packageIds()); } }
Wirkung: Ein Record reduziert veränderlichen Zustand und stärkt den Vertrag.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein reines Transportobjekt besitzt Setter, einen leeren Konstruktor und veränderliche Felder, obwohl es nach der Erzeugung nur Daten transportieren soll. Unvollständige Zwischenzustände und spätere Änderungen sind dadurch möglich.
Erkennungsmerkmale
DTOs mit vielen Settern; Werte werden nach Übergabe verändert; equals/hashCode fehlen oder sind inkonsistent; Tests bauen Objekte in mehreren Schritten auf; parallele Verarbeitung benötigt defensive Kopien.
Ziel und Nutzen
Ein Java Record beschreibt einen unveränderlichen, transparenten Datenträger mit kompakter Syntax und konsistenter Wertsemantik. Pflichtfelder werden beim Erzeugen übergeben; der gültige Zustand ist leichter erkennbar.
Sicherer Refactoring-Ablauf
Prüfen, ob der Typ tatsächlich Daten transportiert und keine Identität oder veränderliches Verhalten besitzt.
Alle Erzeugungsstellen und Frameworkanforderungen erfassen.
Felder in Record-Komponenten überführen und Validierung in einen kompakten Konstruktor legen.
Setter-basierte Erzeugung durch Konstruktor, Factory oder Builder ersetzen.
Serialisierungs- und Kompatibilitätstests ausführen.
Wann sinnvoll - und wann nicht
Records eignen sich nicht automatisch für JPA-Entities, langlebige Aggregate oder Objekte mit bewusst veränderlichem Lebenszyklus. Für sehr viele optionale Felder kann ein Builder weiterhin sinnvoll sein.
Risiken und typische Fehlanwendungen
Unveränderlichkeit ist nur flach: mutable Collections müssen defensiv kopiert werden. Ein Record mit Fachidentität wird nicht allein durch die Syntax zum Value Object. Framework-Kompatibilität muss geprüft werden.
Abgrenzung
Ein Record ist ein Java-Sprachmittel. Ein Value Object ist ein fachliches Modellkonzept und kann als Record umgesetzt werden, benötigt aber fachliche Invarianten.
Verwandte Refactorings und Muster
Immutable DTO, Value Object, Defensive Copy und Builder.
Enterprise-Java-Einordnung
PaymentRequest wird als Record mit PaymentId, Money, CustomerReference und RequestedAt modelliert. Der Adapter validiert die Eingabe einmal und übergibt einen vollständigen Wert an den Use Case.
Teststrategie
Konstruktorvalidierung, Wertgleichheit, defensive Kopien und JSON-Kompatibilität prüfen. Mutationstests zeigen, ob Invarianten wirklich abgesichert sind.
Erwartbare fachliche Ablehnungen werden als Exceptions oder Boolean ohne Begründung transportiert.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Führe einen abgeschlossenen Result-Typ mit Success und Rejected ein; zwinge Aufrufer zur expliziten Behandlung.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
RegistrationResult.java
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Result Type
// Result Pattern: Erwartbare Ergebnisse sind explizit und exhaustiv behandelbar.
public sealed interface RegistrationResult permits RegistrationResult.Accepted,RegistrationResult.Rejected {
record Accepted(String customerId) implements RegistrationResult { public Accepted { if(customerId==null||customerId.isBlank()) throw new IllegalArgumentException(); } }
record Rejected(String code,String message) implements RegistrationResult { public Rejected { if(code==null||code.isBlank()||message==null||message.isBlank()) throw new IllegalArgumentException(); } }
static RegistrationResult evaluate(boolean duplicate){ return duplicate?new Rejected("DUPLICATE","E-Mail bereits registriert"):new Accepted("C-100"); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class RegistrationResultTest { @Test void result_explains_rejection(){ var r=RegistrationResult.evaluate(true); assertInstanceOf(RegistrationResult.Rejected.class,r); assertEquals("DUPLICATE",((RegistrationResult.Rejected)r).code()); } }
Wirkung: Ein algebraischer Result-Typ macht erwartbare Ablehnungen sichtbar.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Erwartbare fachliche Ablehnungen werden als Exceptions, null oder mehrdeutige Booleans signalisiert. Aufrufer können Erfolg, Ablehnungsgrund und Folgeaktion nicht typsicher unterscheiden.
Erkennungsmerkmale
catch-Blöcke für normale Geschäftsentscheidungen; null bedeutet verschiedene Dinge; parallele Methoden liefern reason oder status; Fehlertexte werden ausgewertet; jeder Aufrufer interpretiert Rückgaben anders.
Ziel und Nutzen
Ein Result Type modelliert die endlichen möglichen Ausgänge einer Operation ausdrücklich. Erfolg und erwartbare Ablehnung tragen jeweils passende Daten; Compiler und Pattern Matching unterstützen eine vollständige Behandlung.
Sicherer Refactoring-Ablauf
Mögliche fachliche und technische Ausgänge der Operation erfassen.
Erwartbare Ergebnisse von unerwarteten Programm- oder Infrastrukturfehlern trennen.
Einen geschlossenen Result-Vertrag mit typisierten Varianten definieren.
Bestehende Rückgabe intern adaptieren und Aufrufer schrittweise umstellen.
Textbasierte Statusauswertung und normale Kontrollfluss-Exceptions entfernen.
Wann sinnvoll - und wann nicht
Unerwartete technische Defekte oder verletzte interne Invarianten müssen nicht in jeden Result Type aufgenommen werden. Ein Result ist sinnvoll, wenn der Aufrufer fachlich auf mehrere erwartbare Ausgänge reagieren soll.
Risiken und typische Fehlanwendungen
Ein allumfassendes Result mit beliebigem Fehlerobjekt verliert Typsicherheit. Tief verschachtelte Result-Typen erschweren APIs. Technische Details dürfen nicht unkontrolliert bis zur Fachgrenze durchgereicht werden.
Abgrenzung
Ein Decision Object beschreibt das Ergebnis einer Regelbewertung. Ein Result Type beschreibt den Ausgang einer Operation und kann Erfolg oder erwartbare Fehlervarianten enthalten.
Verwandte Refactorings und Muster
Decision Object, Either, Validation Result und Exception Translation.
Enterprise-Java-Einordnung
ReserveInventory liefert Reserved, InsufficientStock oder ProductUnknown. Der Orchestrator entscheidet je Variante über Fortsetzung, Kompensation oder manuelle Prüfung.
Teststrategie
Tests prüfen jede Variante einschließlich Nutzdaten. Exhaustive Switches und Contract-Tests sichern, dass neue Varianten bewusst behandelt werden.
SQL- oder HTTP-Ausnahmen entweichen bis in fachliche Schichten und koppeln Aufrufer an Infrastrukturdetails.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Fange technische Fehler am Adapter ab und übersetze sie in eine stabile fachliche/Anwendungs-Ausnahme mit erhaltener Ursache.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CustomerLoadGateway.java
package com.aydinsude.workbench.catalog;
// Refactoring: Translate Infrastructure Exception
// Exception Translation Pattern: Technische Fehler werden an der Adaptergrenze stabilisiert.
public final class CustomerLoadGateway {
public interface Client { String fetch(String id) throws Exception; }
public static final class CustomerUnavailable extends RuntimeException { public CustomerUnavailable(String id,Throwable cause){ super("customer unavailable: "+id,cause); } }
private final Client client;
public CustomerLoadGateway(Client client){ this.client=client; }
public String load(String id){ try { return client.fetch(id); } catch(Exception e){ throw new CustomerUnavailable(id,e); } }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class CustomerLoadGatewayTest { @Test void gateway_translates_and_preserves_cause(){ var cause=new Exception("down"); var g=new CustomerLoadGateway(id->{throw cause;}); var e=assertThrows(CustomerLoadGateway.CustomerUnavailable.class,()->g.load("C1")); assertSame(cause,e.getCause()); } }
Wirkung: Exception Translation hält technische Details hinter der Adaptergrenze.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
SQL-, HTTP-, Messaging- oder SDK-Ausnahmen gelangen aus Adaptern in Anwendung und Domäne. Innere Schichten müssen technische Fehlerklassen kennen und können nicht stabil zwischen fachlich relevanten Fehlerkategorien unterscheiden.
Erkennungsmerkmale
Use Cases fangen SQLException, WebClientException oder SDKExceptions; Controller kennen Datenbankcodes; Retryentscheidungen basieren auf Herstellermeldungen; technische Bibliotheksupdates verändern Fachcode.
Ziel und Nutzen
Exception Translation übersetzt technische Fehler an der Adaptergrenze in einen kleinen, stabilen Fehlervertrag. Innere Schichten reagieren auf Bedeutung wie vorübergehend nicht verfügbar, Konflikt oder nicht gefunden statt auf Herstellerdetails.
Sicherer Refactoring-Ablauf
Tatsächlich auftretende technische Fehler und ihre Semantik inventarisieren.
Stabile Fehlerkategorien aus Sicht des inneren Vertrags definieren.
Im Adapter Ursache und relevante Metadaten erhalten und übersetzen.
Aufrufer auf die neuen Kategorien umstellen.
Logging, Retry und API-Mapping getrennt nach Kategorie testen.
Wann sinnvoll - und wann nicht
Nicht jede Exception muss übersetzt werden. Programmierfehler und verletzte Invarianten sollten sichtbar bleiben. Die Übersetzung gehört an eine echte Abhängigkeitsgrenze, nicht in beliebige Zwischenklassen.
Risiken und typische Fehlanwendungen
Eine einzige generische InfrastructureException vernichtet Information. Das Weglassen der Ursache erschwert Diagnose. Fehlercodes aus Fremdsystemen dürfen nicht ungefiltert zum öffentlichen Fachvertrag werden.
Abgrenzung
Exception Translation stabilisiert Fehlersemantik. Exception Mapping an einer REST-Grenze übersetzt anschließend interne Fehler in HTTP-Antworten.
Verwandte Refactorings und Muster
Adapter, Gateway, Anti-Corruption Layer, Result Type und Retry Policy.
Enterprise-Java-Einordnung
Ein PaymentProviderAdapter übersetzt ProviderTimeout in PaymentTemporarilyUnavailable und DuplicateRequest in PaymentAlreadyProcessed. Der Use Case kennt weder HTTP-Status noch SDK-Typen.
Teststrategie
Adaptertests provozieren typische Providerfehler und prüfen Kategorie, Ursache und Metadaten. Höhere Tests prüfen Retry- oder API-Reaktionen ohne technische Exceptionklassen.
Validierungsregeln sind in Controller, Entity und Persistenzcode dupliziert.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere einen kompositionsfähigen Validator mit strukturierten Verletzungen statt verstreuter Guard-Logik.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
OrderValidator.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Validator
// Validator + Composite Pattern: Regeln werden einzeln testbar und gemeinsam ausführbar.
public final class OrderValidator {
public record Order(String customerId,long totalCents) {}
public record Violation(String code,String message) {}
public interface Rule { Optional<Violation> validate(Order order); }
private final List<Rule> rules;
public OrderValidator(List<Rule> rules){ this.rules=List.copyOf(rules); }
public List<Violation> validate(Order order){ return rules.stream().map(r->r.validate(order)).flatMap(Optional::stream).toList(); }
public static OrderValidator defaults(){ return new OrderValidator(List.of(o->o.customerId()==null||o.customerId().isBlank()?Optional.of(new Violation("CUSTOMER","Kunde fehlt")):Optional.empty(),o->o.totalCents()<0?Optional.of(new Violation("TOTAL","Summe negativ")):Optional.empty())); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class OrderValidatorTest { @Test void validator_collects_structured_violations(){ var v=OrderValidator.defaults().validate(new OrderValidator.Order("",-1)); assertEquals(2,v.size()); assertEquals("CUSTOMER",v.getFirst().code()); } }
Validierungsregeln sind über Controller, Services, Entitäten und Persistenzcode verteilt. Dieselbe Eingabe wird mehrfach oder in unterschiedlicher Reihenfolge geprüft; Fehlermeldungen und Schweregrade sind inkonsistent.
Erkennungsmerkmale
Lange if-Ketten vor einem Use Case; wiederholte Null-, Format- und Fachprüfungen; frühe Exceptions verhindern das Sammeln mehrerer Befunde; Regeln lassen sich nicht isoliert testen.
Ziel und Nutzen
Ein Validator bündelt zusammengehörige Prüfungen an einer klaren Grenze und liefert ein typisiertes Ergebnis. Die eigentliche Verarbeitung kann von gültigen Voraussetzungen ausgehen.
Sicherer Refactoring-Ablauf
Syntaktische, strukturelle und fachliche Regeln voneinander unterscheiden.
Die Regeln einer konkreten Eingabe oder Operation zuordnen.
Einen Validator mit typisierten Violations und stabilen Codes einführen.
Bestehende Prüfungen schrittweise verschieben und Duplikate entfernen.
Reihenfolge, Sammelstrategie und Abbruchregeln ausdrücklich festlegen.
Wann sinnvoll - und wann nicht
Invarianten eines Aggregats bleiben im Aggregat; ein externer Validator darf sie nicht ersetzen. Bean Validation eignet sich gut für strukturelle Grenzen, komplexe Fachentscheidungen benötigen häufig Policies oder Specifications.
Risiken und typische Fehlanwendungen
Ein zentraler Universalvalidator wird zum neuen God Object. Freitextfehler erschweren APIs und Übersetzung. Validierung darf keine Daten ändern oder Seiteneffekte auslösen.
Abgrenzung
Ein Validator meldet Verstöße für eine Eingabe oder Operation. Eine Specification beschreibt meist ein wiederverwendbares fachliches Kriterium und ist kombinierbar.
Verwandte Refactorings und Muster
Specification, Bean Validation Boundary, Policy Object und Result Type.
Enterprise-Java-Einordnung
RegisterSupplierValidator prüft Pflichtangaben, IBAN-Format und erlaubte Länderkombinationen. Das Supplier-Aggregat schützt weiterhin seine eigenen Invarianten.
Teststrategie
Entscheidungstabellen prüfen Regeln, Kombinationen und stabile Violation Codes. Tests sichern, dass der Validator rein ist und keine Eingabe verändert.
107. Replace Temporal Coupling with Workflow Object Refactoring
Ausgangsproblem
Mehrere Methoden müssen in einer unsichtbaren Reihenfolge aufgerufen werden, sonst entsteht ein inkonsistenter Zustand.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Kapsle den Ablauf in einem Workflow-Objekt, das Reihenfolge und Zwischenzustand explizit kontrolliert.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
InvoiceWorkflow.java
package com.aydinsude.workbench.catalog;
// Refactoring: Replace Temporal Coupling with Workflow Object
// Workflow Pattern: Reihenfolge und erlaubte Übergänge werden explizit modelliert.
public final class InvoiceWorkflow {
public enum Stage { CREATED, CALCULATED, SENT }
private Stage stage=Stage.CREATED; private long total;
public Stage stage(){ return stage; } public long total(){ return total; }
public void calculate(long net,long tax){ require(Stage.CREATED); if(net<0||tax<0) throw new IllegalArgumentException(); total=net+tax; stage=Stage.CALCULATED; }
public String send(){ require(Stage.CALCULATED); stage=Stage.SENT; return "sent:"+total; }
private void require(Stage expected){ if(stage!=expected) throw new IllegalStateException("expected "+expected+", was "+stage); }
}
Wirkung: Ein Workflow-Objekt macht zeitliche Kopplung sichtbar und prüfbar.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Mehrere Methoden müssen in einer bestimmten, nicht typisierten Reihenfolge aufgerufen werden. Ein Objekt ist zwischen den Aufrufen teilweise initialisiert; falsche Reihenfolge oder ausgelassene Schritte führen erst zur Laufzeit zu Fehlern.
Erkennungsmerkmale
Kommentare wie zuerst validate, dann reserve, dann commit; Boolean-Felder steuern erlaubte Methoden; öffentliche Setter bauen Zwischenzustände auf; Aufrufer duplizieren dieselbe Sequenz.
Ziel und Nutzen
Ein Workflow Object macht den Prozess und seine Übergänge explizit. Es hält benötigten Zustand kontrolliert, benennt Schritte fachlich und verhindert oder typisiert ungültige Reihenfolgen.
Sicherer Refactoring-Ablauf
Die tatsächliche Sequenz, Zustände und Abbruchpfade dokumentieren.
Einen Workflow mit klarer Eingabe, Ergebnis und expliziten Zuständen einführen.
Die bestehende Reihenfolge zunächst unverändert kapseln.
Aufrufer auf eine einzige Startoperation oder typisierte Schritte umstellen.
Versteckte Statusfelder und öffentliche Zwischenmethoden entfernen.
Wann sinnvoll - und wann nicht
Ein einfacher synchroner Ablauf mit wenigen lokalen Anweisungen benötigt kein Workflow Object. Bei langlebigen oder verteilten Prozessen können Process Manager oder Saga geeigneter sein.
Risiken und typische Fehlanwendungen
Ein Workflow darf nicht jede beteiligte Fachlogik übernehmen. Persistierter Workflowzustand erfordert Versionierung und Wiederanlauf. Zu generische Workflow-Engines können Fachsprache verdrängen.
Abgrenzung
Eine State Machine fokussiert erlaubte Zustandsübergänge eines Objekts. Ein Workflow Object orchestriert eine Folge von Arbeitsschritten und deren Ergebnisse.
Verwandte Refactorings und Muster
Process Manager, Saga, State Machine, Command und Template Method.
Enterprise-Java-Einordnung
CustomerOnboardingWorkflow koordiniert Identitätsprüfung, Kontoanlage und Begrüßung. Jeder Schritt liefert ein typisiertes Ergebnis; Wiederaufnahme und Kompensation sind sichtbar.
Teststrategie
Szenariotests prüfen Happy Path, Abbruch, Wiederholung und ungültige Übergänge. Ein deterministischer Clock- und ID-Port hält Tests reproduzierbar.
Wiederholte Requests führen zu doppelten Zahlungen oder Aufträgen.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Führe einen typisierten Idempotency-Key und einen atomaren Store ein; gib bei Wiederholung dasselbe Ergebnis zurück.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
IdempotentPaymentService.java
package com.aydinsude.workbench.catalog;
import java.util.concurrent.ConcurrentHashMap;
// Refactoring: Introduce Idempotency Key
// Idempotent Receiver Pattern: Gleicher Schlüssel liefert dieselbe fachliche Wirkung.
public final class IdempotentPaymentService {
public record IdempotencyKey(String value){ public IdempotencyKey { if(value==null||value.isBlank()) throw new IllegalArgumentException(); } }
private final ConcurrentHashMap<IdempotencyKey,String> results=new ConcurrentHashMap<>();
public String charge(IdempotencyKey key,long cents){ if(cents<=0) throw new IllegalArgumentException("cents"); return results.computeIfAbsent(key,k->"PAY-"+k.value()+"-"+cents); }
public int executionCount(){ return results.size(); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class IdempotentPaymentServiceTest { @Test void duplicate_key_has_single_effect(){ var s=new IdempotentPaymentService(); var k=new IdempotentPaymentService.IdempotencyKey("K1"); assertEquals(s.charge(k,500),s.charge(k,500)); assertEquals(1,s.executionCount()); } }
Wirkung: Ein Idempotency-Key stabilisiert wiederholte oder parallele Aufrufe.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Befehl oder externer Request kann durch Retry, Timeout oder doppelte Zustellung mehrfach eintreffen. Ohne stabile Identität werden Zahlungen, Buchungen oder Nachrichten doppelt ausgeführt.
Erkennungsmerkmale
Clients wiederholen Requests nach Timeout; Consumer erhält At-least-once-Zustellung; Duplikate werden nur über zufällige Zeitfenster erkannt; Seiteneffekte besitzen keine Wiederholungssemantik.
Ziel und Nutzen
Ein Idempotency Key identifiziert die fachlich gleiche Ausführungsabsicht über Wiederholungen hinweg. Die Verarbeitung kann ein gespeichertes Ergebnis zurückgeben oder konkurrierende Ausführungen kontrolliert ablehnen.
Sicherer Refactoring-Ablauf
Die fachliche Einheit bestimmen, die genau einmal wirksam werden soll.
Den Schlüssel am Ursprung erzeugen und über alle Grenzen unverändert propagieren.
Speicherung von Status und Ergebnis atomar mit der Ausführung koordinieren.
Wiederholungen mit gleichem Schlüssel und anderem Payload als Konflikt behandeln.
Aufbewahrung, Ablauf und Parallelitätsverhalten festlegen.
Wann sinnvoll - und wann nicht
Ein zufälliger Schlüssel pro Wiederholungsversuch löst das Problem nicht. Für reine, ohnehin idempotente Leseoperationen ist zusätzliche Speicherung oft unnötig.
Risiken und typische Fehlanwendungen
Check-then-act ohne atomare Reservierung bleibt rennanfällig. Zu kurze Aufbewahrung erlaubt späte Duplikate; unbegrenzte Speicherung wächst dauerhaft. Schlüssel dürfen nicht unkontrolliert Mandantengrenzen überschreiten.
Abgrenzung
Ein Correlation Identifier verbindet zusammengehörige Nachrichten zur Beobachtung. Ein Idempotency Key verhindert die doppelte Wirkung derselben Absicht.
Verwandte Refactorings und Muster
Inbox Pattern, Idempotent Receiver, Transactional Outbox und Deduplication Store.
Enterprise-Java-Einordnung
Der Payment-API-Client sendet eine stabile Idempotency-Key-ID. Der Orchestrator reserviert sie atomar, speichert PaymentResult und liefert bei Wiederholung exakt dieses Ergebnis.
Teststrategie
Parallelitätstests senden denselben Schlüssel gleichzeitig. Contract-Tests prüfen identischen und abweichenden Payload, Wiederholung nach Fehler sowie Ablaufregeln.
Retry-Schleifen, Backoff und Fehlerklassifizierung sind über mehrere Integrationen kopiert.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere eine Policy, die Versuchszahl und Wiederholbarkeit entscheidet; halte I/O im Adapter.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
RetryPolicy.java
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Extract Retry Policy
// Policy Pattern: Wiederholungsentscheidung ist von der Ausführung getrennt.
public record RetryPolicy(int maxAttempts, Duration delay) {
public RetryPolicy { if(maxAttempts<1||delay==null||delay.isNegative()) throw new IllegalArgumentException(); }
public boolean shouldRetry(int attempt,Throwable failure){ return attempt<maxAttempts && failure instanceof TransientFailure; }
public static final class TransientFailure extends RuntimeException { public TransientFailure(String message){ super(message); } }
}
Wirkung: Eine Policy trennt Retry-Entscheidung von Transport und Warte-Mechanik.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Wiederholungslogik ist als Schleife mit festen Pausen in mehreren Adaptern verteilt. Fehlerklassifikation, maximale Dauer und Abbruchbedingungen unterscheiden sich oder verursachen Wiederholungsstürme.
Erkennungsmerkmale
for-Schleifen um Remoteaufrufe; catch(Exception) mit sleep; identische Retries auf allen Fehlern; verschachtelte Wiederholungen in SDK, Client und Use Case; fehlende Deadline.
Ziel und Nutzen
Eine Retry Policy macht ausdrücklich, welche Fehler vorübergehend sind, wie oft und mit welchem Backoff wiederholt wird und wann sofort abgebrochen werden muss. Sie lässt sich testen und mit Deadline sowie Circuit Breaker kombinieren.
Sicherer Refactoring-Ablauf
Fehler nach vorübergehend, dauerhaft und unbekannt klassifizieren.
Maximale Versuche oder besser ein Zeitbudget definieren.
Backoff, Jitter und serverseitige Retry-After-Hinweise modellieren.
Policy am äußeren Adapter platzieren und doppelte Retryschichten entfernen.
Metriken für Versuche, Erfolge und Erschöpfung ergänzen.
Wann sinnvoll - und wann nicht
Nicht-idempotente Operationen dürfen nur mit geeigneter Idempotenzsicherung wiederholt werden. Validierungsfehler, Berechtigungsfehler und dauerhafte Ablehnungen sind keine Retryfälle.
Risiken und typische Fehlanwendungen
Unbegrenzte oder synchronisierte Retries verstärken Ausfälle. Retry innerhalb einer langen Transaktion bindet Ressourcen. Mehrere unsichtbare Retryebenen vervielfachen Versuche.
Abgrenzung
Retry behandelt erwartbar vorübergehende Fehler durch erneute Versuche. Circuit Breaker stoppt Aufrufe, wenn ein Ziel wahrscheinlich weiterhin ausfällt.
Verwandte Refactorings und Muster
Circuit Breaker, Deadline, Idempotency Key, Backoff und Bulkhead.
Enterprise-Java-Einordnung
InventoryGateway wiederholt nur Timeouts und 503-Antworten innerhalb einer Gesamtdeadline. 400- und 409-Antworten werden sofort fachlich übersetzt.
Teststrategie
Deterministische Tests verwenden Fake Clock und berechnen den Retryplan ohne reale Wartezeiten. Fehlerklassifikation, Jittergrenzen und Deadlineabbruch werden separat geprüft.
Audit-Logging wird direkt mit Logger- oder Datenbankaufrufen in Geschäftslogik vermischt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere einen typisierten Audit-Port und injiziere ihn; Fachlogik beschreibt nur das Ereignis.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
AuditedApprovalService.java
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Extract Audit Port
// Ports and Adapters: Audit-I/O liegt hinter einem typisierten Ausgangsport.
public final class AuditedApprovalService {
public record AuditEntry(String action,String subject,Instant at) {}
public interface AuditPort { void append(AuditEntry entry); }
private final AuditPort audit; private final java.time.Clock clock;
public AuditedApprovalService(AuditPort audit,java.time.Clock clock){ this.audit=audit; this.clock=clock; }
public boolean approve(String claimId,long cents){ if(claimId==null||claimId.isBlank()||cents<0) throw new IllegalArgumentException(); boolean approved=cents<=10_000; audit.append(new AuditEntry(approved?"APPROVED":"MANUAL_REVIEW",claimId,clock.instant())); return approved; }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import java.util.*; import org.junit.jupiter.api.Test;
class AuditedApprovalServiceTest { @Test void service_emits_typed_audit_entry(){ var entries=new ArrayList<AuditedApprovalService.AuditEntry>(); var s=new AuditedApprovalService(entries::add,Clock.fixed(Instant.EPOCH,ZoneOffset.UTC)); assertTrue(s.approve("CL-1",500)); assertEquals("APPROVED",entries.getFirst().action()); assertEquals(Instant.EPOCH,entries.getFirst().at()); } }
Wirkung: Ein Audit-Port entkoppelt Fachentscheidung von Logging- und Speichertechnik.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fach- und Anwendungscode schreibt direkt in Logdateien, Datenbanktabellen oder Audit-SDKs. Dadurch vermischen sich Nachweispflichten mit Speicherformat, Transport und technischer Fehlerbehandlung.
Erkennungsmerkmale
Logger- oder JDBC-Aufrufe mitten in Fachmethoden; Audittexte werden frei zusammengesetzt; unterschiedliche Module protokollieren gleiche Vorgänge anders; Tests müssen Logs durchsuchen.
Ziel und Nutzen
Ein Audit Port beschreibt fachlich relevante Nachweise als typisierten Vertrag. Adapter entscheiden über unveränderliche Speicherung, Signierung oder Weiterleitung; die Fachlogik bleibt unabhängig vom Auditprodukt.
Sicherer Refactoring-Ablauf
Pflichtige Auditereignisse und benötigte Fakten mit Fach- und Compliance-Seite klären.
Einen schmalen Port mit typisierten Audit Records definieren.
Direkte Log- und Persistenzaufrufe in einen Adapter verschieben.
Aufrufer schrittweise auf den Port umstellen.
Fehlerstrategie, Datenschutz, Aufbewahrung und Unveränderlichkeit festlegen.
Wann sinnvoll - und wann nicht
Technisches Debug-Logging ist nicht automatisch Audit. Ein Audit Port ist für nachweispflichtige, fachlich benannte Vorgänge gedacht. Kritische Audits benötigen eine explizite Zustell- und Fehlerstrategie.
Risiken und typische Fehlanwendungen
Freitext-Audits sind schwer auswertbar. Sensible Daten dürfen nicht ungefiltert aufgenommen werden. Fire-and-forget kann Nachweise verlieren; synchrone Kopplung kann dagegen den Fachablauf blockieren.
Abgrenzung
Observability dient Betrieb und Diagnose. Audit dient nachvollziehbaren fachlichen oder regulatorischen Nachweisen mit strengeren Anforderungen.
Verwandte Refactorings und Muster
Security Audit Event, Event Object, Outbox und Observability Context.
Enterprise-Java-Einordnung
ApprovalService veröffentlicht ApprovalAuditRecord mit Vorgang, Akteur, Entscheidung, Grundcode und Policyversion. Ein Adapter schreibt manipulationsgeschützt in das zentrale Audit-System.
Teststrategie
Unit-Tests prüfen, welches Audit Record bei welcher Fachentscheidung entsteht. Adaptertests sichern Persistenz, Reihenfolge, Maskierung und Fehlerverhalten.
Kapitel 12 Publisher-Ports und ausgehende Nachrichten entkoppeln10 Refactorings
Technische Grenzen, kontrollierbare Infrastruktur und Resilienz mit kompilierbarem Java-21-Zielcode.
111. Extract Publisher Port Port / Publisher
Ausgangsproblem
Geschäftslogik ruft Kafka-, JMS- oder HTTP-Publisher direkt auf und kennt technische Zustellparameter.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere einen typisierten Ausgangsport. Die Anwendung publiziert ein Fachereignis; der technische Adapter übernimmt Serialisierung und Transport.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
OrderEventPublisher.java
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Extract Publisher Port
// Ports and Adapters: Fachcode publiziert über einen typisierten Ausgangsport.
public final class OrderEventPublisher {
public record OrderConfirmed(String orderId, Instant occurredAt) {
public OrderConfirmed { if(orderId==null||orderId.isBlank()||occurredAt==null) throw new IllegalArgumentException(); }
}
public interface EventPublisher { void publish(OrderConfirmed event); }
private final EventPublisher publisher; private final java.time.Clock clock;
public OrderEventPublisher(EventPublisher publisher, java.time.Clock clock){ this.publisher=publisher; this.clock=clock; }
public void confirm(String orderId){ publisher.publish(new OrderConfirmed(orderId,clock.instant())); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import java.util.*; import org.junit.jupiter.api.Test;
class OrderEventPublisherTest { @Test void publishes_typed_event(){ var events=new ArrayList<OrderEventPublisher.OrderConfirmed>(); var s=new OrderEventPublisher(events::add,Clock.fixed(Instant.EPOCH,ZoneOffset.UTC)); s.confirm("O-1"); assertEquals(new OrderEventPublisher.OrderConfirmed("O-1",Instant.EPOCH),events.getFirst()); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Anwendungsfälle verwenden direkt Brokerclients, Topics, Header und Serialisierung. Fachliche Verarbeitung ist dadurch an ein Messagingprodukt und dessen Zustellsemantik gekoppelt.
Erkennungsmerkmale
KafkaTemplate, JMSProducer oder SDK-Client im Use Case; Topicnamen im Fachcode; technische Header werden manuell gesetzt; Tests benötigen Broker oder umfangreiche Mocks.
Ziel und Nutzen
Ein Publisher Port beschreibt die fachliche Veröffentlichungsabsicht unabhängig vom Transport. Ein Adapter übernimmt Event Envelope, Serialisierung, Routing und Brokerfehler.
Sicherer Refactoring-Ablauf
Fachliche Veröffentlichungsfälle und benötigte Garantien erfassen.
Einen Port mit fachlich typisierten Nachrichten oder Events definieren.
Broker- und Serialisierungslogik in einen Adapter verschieben.
Direkte Clientaufrufe schrittweise ersetzen.
Zustellstrategie, Outbox und Fehlerbehandlung bewusst entscheiden.
Wann sinnvoll - und wann nicht
Ein Publisher Port garantiert allein keine atomare Veröffentlichung zusammen mit Datenänderungen. Dafür ist häufig Transactional Outbox nötig. Rein interne synchrone Reaktionen können über Domain Events erfolgen.
Risiken und typische Fehlanwendungen
Ein generischer publish(Object) Port versteckt Verträge. Topic- und Schemaentscheidungen dürfen nicht unkontrolliert in Aufrufer zurückwandern. Falsche Erfolgsannahmen bei asynchroner Zustellung sind gefährlich.
Abgrenzung
Ein Publisher Port abstrahiert die Fähigkeit zu veröffentlichen. Event Object oder Integration Event definiert den Inhalt der Nachricht.
Verwandte Refactorings und Muster
Transactional Outbox, Domain Event, Integration Event, Event Envelope und Adapter.
Enterprise-Java-Einordnung
OrderApplicationService ruft OrderEventPublisher.publish(OrderConfirmed) auf. Der Kafka-Adapter erzeugt Envelope, Partition Key und Schema-Version.
Korrelation wird als optionaler String durch Logger, Header und Methoden verteilt und geht an asynchronen Grenzen verloren.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Führe einen validierten Correlation-Identifier und einen expliziten Request-Kontext ein. Übergib ihn bewusst über jede Prozessgrenze.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CorrelatedRequest.java
package com.aydinsude.workbench.catalog;
import java.util.UUID;
// Refactoring: Introduce Correlation Identifier
// Correlation Identifier Pattern: Ein stabiler Fachtyp verbindet zusammengehörige Aufrufe.
public record CorrelatedRequest(CorrelationId correlationId, String payload) {
public CorrelatedRequest { if(correlationId==null||payload==null) throw new IllegalArgumentException(); }
public record CorrelationId(String value) {
public CorrelationId { if(value==null||value.isBlank()) throw new IllegalArgumentException("correlationId"); }
public static CorrelationId newId(){ return new CorrelationId(UUID.randomUUID().toString()); }
}
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class CorrelatedRequestTest { @Test void correlation_is_explicit(){ var id=new CorrelatedRequest.CorrelationId("trace-42"); var r=new CorrelatedRequest(id,"payload"); assertEquals("trace-42",r.correlationId().value()); assertThrows(IllegalArgumentException.class,()->new CorrelatedRequest.CorrelationId("")); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein fachlicher Vorgang durchläuft mehrere Threads, Dienste und Nachrichten, ohne eine gemeinsame Kennung. Logs, Traces und Antworten lassen sich nicht zuverlässig demselben Ablauf zuordnen.
Erkennungsmerkmale
Support sucht über Zeitstempel und Kundennummern; neue IDs entstehen an jeder Grenze; Message-Header fehlen; asynchrone Antworten können nicht dem Request zugeordnet werden.
Ziel und Nutzen
Ein Correlation Identifier verbindet technisch oder fachlich zusammengehörige Schritte über Prozessgrenzen. Er verbessert Diagnose, Request-Reply-Zuordnung und Auditbezug, ohne Fachidentitäten zu ersetzen.
Sicherer Refactoring-Ablauf
Lebensdauer und Startpunkt des zu korrelierenden Vorgangs bestimmen.
Einen typisierten, unveränderlichen Identifier einführen.
An der Eingangsgrenze vorhandene Kennung validieren oder neu erzeugen.
Über HTTP, Messaging und asynchrone Tasks bewusst propagieren.
Logging, Tracing und Antworten auf dieselbe Kennung ausrichten.
Wann sinnvoll - und wann nicht
CorrelationId ist weder Entity-ID noch Idempotency Key. Eine einzige globale Kennung für alle Unterprozesse kann zu grob sein; bei Verzweigungen können Causation IDs ergänzen.
Risiken und typische Fehlanwendungen
ThreadLocal-basierte Propagation bricht bei asynchroner Verarbeitung. Ungeprüfte externe Kennungen können Log-Injection oder Kollisionen verursachen. Personenbezogene Daten gehören nicht in die ID.
Abgrenzung
Correlation Identifier verbindet einen Ablauf. Idempotency Key identifiziert eine wiederholbare Absicht; Trace IDs werden häufig vom Observability-System verwaltet.
Verwandte Refactorings und Muster
Observability Context, Request-Reply, Causation Identifier und Event Envelope.
Enterprise-Java-Einordnung
Ein Schadenfall-Upload erhält CorrelationId am API-Gateway. Dokumentprüfung, Virenscan, Extraktion und Ergebnisnachricht tragen dieselbe Kennung.
Teststrategie
Boundary-Tests prüfen Erzeugung, Validierung und Weitergabe. Asynchrone Tests stellen sicher, dass Kontext nicht zwischen Vorgängen ausläuft.
113. Replace Direct Call with Command Bus Command Bus
Ausgangsproblem
Ein Use Case ruft mehrere konkrete Handler direkt auf; Routing, Audit und Erweiterung sind über den Aufrufer verteilt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Modelliere Commands und Handler als typisierte Verträge. Ein kleiner Bus übernimmt ausschließlich Auflösung und Dispatch.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CommandBus.java
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Replace Direct Call with Command Bus
// Command + Registry Pattern: Dispatch wird vom fachlichen Aufrufer getrennt.
public final class CommandBus {
public interface Command<R> {}
public interface Handler<C extends Command<R>,R> { Class<C> type(); R handle(C command); }
private final Map<Class<?>,Handler<?,?>> handlers;
public CommandBus(List<Handler<?,?>> handlers){ var map=new HashMap<Class<?>,Handler<?,?>>(); for(var h:handlers) if(map.put(h.type(),h)!=null) throw new IllegalArgumentException("duplicate handler"); this.handlers=Map.copyOf(map); }
@SuppressWarnings("unchecked") public <C extends Command<R>,R> R dispatch(C command){ var h=(Handler<C,R>)handlers.get(command.getClass()); if(h==null) throw new IllegalArgumentException("no handler"); return h.handle(command); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.*; import org.junit.jupiter.api.Test;
class CommandBusTest { record Ping(String value) implements CommandBus.Command<String>{} @Test void bus_dispatches_by_type(){ CommandBus.Handler<Ping,String> h=new CommandBus.Handler<>(){ public Class<Ping> type(){return Ping.class;} public String handle(Ping p){return "pong:"+p.value();} }; assertEquals("pong:x",new CommandBus(List.of(h)).dispatch(new Ping("x"))); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Aufrufer kennt konkrete Handler und deren Abhängigkeiten. Querschnittliche Regeln wie Transaktion, Autorisierung, Metrik oder Idempotenz werden an vielen Aufrufstellen wiederholt.
Erkennungsmerkmale
Controller instanziieren oder injizieren zahlreiche Use Cases; große Dispatcher mit instanceof oder switch; Decoratorlogik wird pro Handler manuell verdrahtet; neue Befehle verändern zentrale Aufrufer.
Ziel und Nutzen
Ein Command Bus trennt das Senden eines typisierten Befehls von dessen Handler. Eine kontrollierte Pipeline kann Querschnittsregeln einheitlich anwenden, während jeder Handler einen einzelnen Anwendungsfall umsetzt.
Sicherer Refactoring-Ablauf
Befehle und jeweils genau einen verantwortlichen Handler identifizieren.
Typisierte Command- und Handlerverträge definieren.
Eine explizite Registry oder Composition-Root-Zuordnung aufbauen.
Direkte Handleraufrufe schrittweise über den Bus führen.
Pipeline-Verhalten wie Transaktion und Autorisierung klein und sichtbar halten.
Wann sinnvoll - und wann nicht
Für wenige direkte Aufrufe kann ein Command Bus unnötige Indirektion erzeugen. Innerhalb eines Aggregats ersetzt er keine normalen Methoden. Query und Command sollten nicht unklar vermischt werden.
Risiken und typische Fehlanwendungen
Ein magischer Reflection-Bus erschwert Navigation und Fehleranalyse. Ein untypisiertes Object-API verliert Compilerhilfe. Zu viele Pipelinebehaviors können tatsächlichen Ablauf verstecken.
Abgrenzung
Ein Command Bus dispatcht Befehle an Handler. Ein Event Bus verteilt ein eingetretenes Ereignis typischerweise an mehrere unabhängige Empfänger.
Verwandte Refactorings und Muster
Command, Mediator, Chain of Responsibility, CQRS und Composition Root.
Enterprise-Java-Einordnung
REST-Adapter sendet ApproveClaimCommand. Der Bus findet ApproveClaimHandler und führt davor Authorization-, Transaction- und Audit-Behavior aus.
Teststrategie
Handler werden direkt unit-getestet. Bustests prüfen eindeutige Registrierung, Pipeline-Reihenfolge und unbekannte Commands; Architekturtests verhindern direkte Umgehung.
Fachlogik liest die Systemzeit direkt. Tests werden zeitabhängig und Grenzfälle sind schwer reproduzierbar.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Injiziere java.time.Clock und leite alle Zeitentscheidungen aus genau dieser Quelle ab.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
ExpiryService.java
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Clock
// Dependency Injection: Zeit wird als kontrollierbare Abhängigkeit behandelt.
public final class ExpiryService {
private final Clock clock;
public ExpiryService(Clock clock){ this.clock=clock; }
public boolean expired(Instant validUntil){ if(validUntil==null) throw new IllegalArgumentException(); return !clock.instant().isBefore(validUntil); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import org.junit.jupiter.api.Test;
class ExpiryServiceTest { @Test void boundary_is_deterministic(){ var now=Instant.parse("2026-07-11T10:00:00Z"); var s=new ExpiryService(Clock.fixed(now,ZoneOffset.UTC)); assertTrue(s.expired(now)); assertFalse(s.expired(now.plusSeconds(1))); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fachlogik liest System.currentTimeMillis, Instant.now oder LocalDate.now direkt. Tests hängen von realer Zeit ab; Zeitzonen, Stichtage und Ablaufregeln sind schwer reproduzierbar.
Erkennungsmerkmale
Flaky Tests um Mitternacht; Thread.sleep in Tests; Zeit wird mehrfach innerhalb einer Operation gelesen; Fachentscheidungen ändern sich je Testlauf.
Ziel und Nutzen
Eine injizierte Clock macht Zeit zu einer expliziten Abhängigkeit. Produktiv liefert sie die Systemzeit, Tests verwenden feste oder steuerbare Zeitpunkte. Ein Vorgang kann einen konsistenten Jetzt-Snapshot verwenden.
Sicherer Refactoring-Ablauf
Direkte Zeitabfragen inventarisieren und fachliche Zeiteinheit bestimmen.
java.time.Clock oder einen fachlich benannten Zeitport einführen.
Clock per Konstruktor injizieren und innerhalb einer Operation einmal lesen.
Tests auf feste Zeitpunkte und Zeitzonen umstellen.
Sleep-basierte Tests durch kontrolliertes Fortschalten ersetzen.
Wann sinnvoll - und wann nicht
Zeitwerte, die bereits als Ereignis- oder Befehlsdaten vorliegen, sollten nicht erneut über Clock gelesen werden. Eine globale veränderliche Fake-Zeit kann wiederum Tests koppeln.
Risiken und typische Fehlanwendungen
Vermischung von Instant und lokaler Zeit erzeugt Zeitzonenfehler. Mehrfaches now() innerhalb derselben Entscheidung kann inkonsistente Grenzfälle schaffen. Clock darf nicht als beliebiger Scheduler missbraucht werden.
Abgrenzung
Clock liefert Zeit. Ein Scheduler plant Ausführung; eine Deadline beschreibt das verbleibende Zeitbudget eines Vorgangs.
Verwandte Refactorings und Muster
Deterministic Clock Boundary, Deadline, Scheduler und Immutable Snapshot.
Enterprise-Java-Einordnung
SubscriptionRenewalPolicy erhält Clock und entscheidet reproduzierbar über Kulanzfrist, Fälligkeit und Kündigungsdatum.
Teststrategie
Feste Clock prüft Grenzwerte vor, auf und nach dem Stichtag. Zeitzonentests decken Sommerzeit und lokale Geschäftstage ab.
UUID-Erzeugung ist im Use Case versteckt. Tests können konkrete Ergebnisse nicht stabil prüfen.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere einen ID-Generator-Port. Produktion verwendet UUID, Tests eine deterministische Sequenz.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CustomerRegistration.java
package com.aydinsude.workbench.catalog;
// Refactoring: Extract ID Generator
// Factory Port: Identitätserzeugung wird als Abhängigkeit sichtbar.
public final class CustomerRegistration {
public interface IdGenerator { String nextId(); }
public record Customer(String id,String name) {}
private final IdGenerator ids;
public CustomerRegistration(IdGenerator ids){ this.ids=ids; }
public Customer register(String name){ if(name==null||name.isBlank()) throw new IllegalArgumentException("name"); return new Customer(ids.nextId(),name.trim()); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class CustomerRegistrationTest { @Test void generated_identity_is_testable(){ var s=new CustomerRegistration(()->"C-100"); assertEquals(new CustomerRegistration.Customer("C-100","Ada"),s.register(" Ada ")); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fach- oder Anwendungscode erzeugt IDs direkt über UUID.randomUUID, Datenbanksequenzen oder statische Utilities. Tests können erwartete Referenzen nicht kontrollieren und unterschiedliche ID-Regeln sind verteilt.
Erkennungsmerkmale
Zufällige IDs erschweren Assertions; Aggregate kennen Datenbanksequenzen; Formate werden per Stringkonkatenation gebaut; Mandanten oder Regionen benötigen unterschiedliche Präfixe.
Ziel und Nutzen
Ein ID Generator Port macht Identitätserzeugung explizit und austauschbar. Produktive Adapter können UUID, ULID, Sequenz oder externe Nummernkreise verwenden; Tests liefern deterministische Werte.
Sicherer Refactoring-Ablauf
Arten und Gültigkeitsbereiche der IDs erfassen.
Für jede fachlich verschiedene Identität einen schmalen Generatorvertrag definieren.
Direkte statische Erzeugung in einen Adapter verschieben.
Generator in den erzeugenden Use Case oder die Factory injizieren.
Kollision, Format und Sortierbarkeit als technische Verträge testen.
Wann sinnvoll - und wann nicht
Bereits vorhandene externe Identitäten werden nicht neu generiert. Value Objects ohne Identität benötigen keinen Generator. Ein generischer StringIdGenerator kann fachlich unterschiedliche IDs vermischen.
Risiken und typische Fehlanwendungen
Vorhersagbare IDs können Sicherheitsrisiken erzeugen. Datenbank- und Anwendungssequenzen dürfen nicht doppelt konkurrieren. ID-Format sollte nicht unnötig Fachlogik steuern.
Abgrenzung
Ein Generator erzeugt neue Identitäten. Ein Mapper oder Parser wandelt bestehende externe Identifikatoren in interne Value Objects um.
Verwandte Refactorings und Muster
Factory, Composition Root, Deterministic ID Generator und Entity Identity.
Enterprise-Java-Einordnung
ClaimIdGenerator liefert eine typisierte ClaimId. Der produktive ULID-Adapter unterstützt zeitliche Sortierung; Tests verwenden eine Sequenz aus vorbereiteten IDs.
Teststrategie
Unit-Tests injizieren feste IDs. Contract-Tests prüfen Eindeutigkeit, Format, Parallelität und gegebenenfalls monotone Sortierung.
116. Introduce Transaction Boundary Unit of Work / Transaction Boundary
Ausgangsproblem
Repository-Aufrufe committen unabhängig; Teilfehler können einen fachlich inkonsistenten Zwischenstand hinterlassen.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Führe eine explizite Transaction-Boundary ein, die den gesamten Use Case atomar ausführt.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
TransferApplicationService.java
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Transaction Boundary
// Unit of Work Pattern: Ein fachlicher Use Case besitzt genau eine Commit-Grenze.
public final class TransferApplicationService {
public interface TransactionBoundary { <T> T required(java.util.function.Supplier<T> work); }
public interface AccountPort { long balance(String id); void save(String id,long cents); }
private final TransactionBoundary tx; private final AccountPort accounts;
public TransferApplicationService(TransactionBoundary tx,AccountPort accounts){ this.tx=tx; this.accounts=accounts; }
public void transfer(String from,String to,long cents){ tx.required(()->{ long source=accounts.balance(from); if(cents<=0||source<cents) throw new IllegalArgumentException("funds"); accounts.save(from,source-cents); accounts.save(to,accounts.balance(to)+cents); return null; }); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.*; import org.junit.jupiter.api.Test;
class TransferApplicationServiceTest { @Test void whole_use_case_runs_in_one_boundary(){ var data=new HashMap<String,Long>(Map.of("A",1000L,"B",100L)); var calls=new int[1]; TransferApplicationService.TransactionBoundary tx=w->{calls[0]++;return w.get();}; TransferApplicationService.AccountPort p=new TransferApplicationService.AccountPort(){public long balance(String id){return data.get(id);} public void save(String id,long c){data.put(id,c);}}; new TransferApplicationService(tx,p).transfer("A","B",400); assertEquals(1,calls[0]); assertEquals(600L,data.get("A")); assertEquals(500L,data.get("B")); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Transaktionen beginnen implizit in Controllern, Repositories oder über breit gesetzte Annotationen. Mehrere fachliche Schritte laufen entweder ungewollt getrennt oder halten eine Transaktion während langsamer Remoteaufrufe offen.
Erkennungsmerkmale
@Transactional auf ganzen Klassen; Repositories committen selbst; Lazy Loading bestimmt fachlichen Ablauf; Netzwerkaufrufe innerhalb einer Datenbanktransaktion; unklare Rollbackregeln.
Ziel und Nutzen
Eine explizite Transaction Boundary umfasst genau die atomare lokale Zustandsänderung eines Anwendungsfalls. Besitz, Dauer und Rollback werden sichtbar; Remoteintegration wird über geeignete Muster entkoppelt.
Sicherer Refactoring-Ablauf
Die fachlich atomare lokale Änderung bestimmen.
Aktuelle Start-, Commit- und Rollbackpunkte dokumentieren.
Transaktionssteuerung an die Application-Service- oder Unit-of-Work-Grenze verschieben.
Langsame Remoteaufrufe aus der lokalen Transaktion herauslösen.
Nebenläufigkeit, Events und Outbox bewusst integrieren.
Wann sinnvoll - und wann nicht
Eine lokale Datenbanktransaktion kann keine verteilte Atomarität über mehrere Dienste garantieren. Für verteilte Abläufe sind Saga, Outbox oder Kompensation nötig.
Risiken und typische Fehlanwendungen
Zu breite Transaktionen erhöhen Sperren und Latenz; zu schmale Grenzen verletzen Invarianten. Selbstaufrufe bei Proxy-basierten Frameworks können Annotationen umgehen.
Abgrenzung
Transaction Boundary definiert lokale Atomarität. Workflow oder Saga koordinieren mehrere lokale Transaktionen über Zeit.
Verwandte Refactorings und Muster
Unit of Work, Transactional Outbox, Saga und Optimistic Locking.
Enterprise-Java-Einordnung
ApproveClaim lädt den Schadenfall, wendet die Entscheidung an, speichert Aggregate und Outbox-Eintrag in einer lokalen Transaktion. Auszahlung erfolgt später asynchron.
Teststrategie
Integrationstests prüfen Commit und Rollback sowie Outbox-Atomarität. Nebenläufigkeitstests decken Konflikte und Wiederholung ab.
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Eine gesamte öffentliche Methode ist synchronized, obwohl nur ein kleiner Zustandsübergang geschützt werden muss. Langsame Berechnung oder I/O hält den Monitor und blockiert unabhängige Aufrufer.
Erkennungsmerkmale
synchronized an langen Methoden; Remote- oder Dateiaufrufe innerhalb des Monitors; keine Timeout- oder Interruptstrategie; unklare Zuordnung, welcher Zustand geschützt wird.
Ziel und Nutzen
Ein dediziertes Lock Object macht Schutzbereich und Besitz explizit. Kritische Abschnitte können verkleinert, mit try/finally abgesichert und bei Bedarf mit tryLock oder mehreren unabhängigen Locks gestaltet werden.
Sicherer Refactoring-Ablauf
Gemeinsam veränderlichen Zustand und zu schützende Invariante bestimmen.
Langsame oder reine Arbeit aus dem kritischen Abschnitt verschieben.
Ein privates finales Lock Object einführen.
Lock und Unlock strikt in try/finally kapseln.
Parallelität, Fairness, Timeout und Deadlockrisiken prüfen.
Wann sinnvoll - und wann nicht
Ein Lock ist nicht automatisch die beste Lösung. Unveränderlichkeit, atomare Datenstrukturen oder Actor-/Queue-Verarbeitung können geteilter Mutation ganz vermeiden.
Risiken und typische Fehlanwendungen
Mehrere Locks erzeugen Ordnungs- und Deadlockprobleme. Öffentlich erreichbare Lockobjekte dürfen nicht verwendet werden. Das Herausziehen von Arbeit darf die atomare Invariante nicht aufbrechen.
Abgrenzung
synchronized nutzt den Objektmonitor implizit. Ein Lock Object bietet explizitere Kontrolle, beseitigt aber nicht die Notwendigkeit eines korrekten Nebenläufigkeitsmodells.
Verwandte Refactorings und Muster
Monitor Object, Bulkhead, Concurrent Repository und Immutable Snapshot.
Enterprise-Java-Einordnung
InventoryReservation schützt nur die atomare Änderung einer SKU-Menge. Datenbank- oder Eventaufrufe erfolgen nach Freigabe des lokalen Locks.
Teststrategie
Parallelitätstests synchronisieren Startpunkte und prüfen Invarianten unter Last. Timeouts helfen, Deadlocks sichtbar zu machen; Tests dürfen nicht nur zufällig auf Rennen hoffen.
Mehrere Threads lesen eine veränderliche Konfigurations-Map während eines Reloads und sehen Mischzustände.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Publiziere vollständig gebaute immutable Snapshots atomar. Leser arbeiten immer auf genau einer Version.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
RoutingConfiguration.java
package com.aydinsude.workbench.catalog;
import java.util.*; import java.util.concurrent.atomic.AtomicReference;
// Refactoring: Introduce Immutable Snapshot
// Copy-on-Write Snapshot: Leser sehen stets eine vollständige Konfigurationsversion.
public final class RoutingConfiguration {
public record Snapshot(long version,Map<String,String> routes){ public Snapshot { routes=Map.copyOf(routes); } }
private final AtomicReference<Snapshot> current=new AtomicReference<>(new Snapshot(0,Map.of()));
public Snapshot current(){ return current.get(); }
public void replace(long version,Map<String,String> routes){ var next=new Snapshot(version,routes); current.updateAndGet(old->{ if(version<=old.version()) throw new IllegalArgumentException("version"); return next; }); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.*; import org.junit.jupiter.api.Test;
class RoutingConfigurationTest { @Test void snapshot_is_atomic_and_immutable(){ var c=new RoutingConfiguration(); var source=new HashMap<String,String>(Map.of("AT","eu")); c.replace(1,source); source.put("DE","eu"); assertEquals(Map.of("AT","eu"),c.current().routes()); assertThrows(IllegalArgumentException.class,()->c.replace(1,Map.of())); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Mehrere Threads oder Verarbeitungsschritte lesen eine Konfiguration oder einen Zustand, während dieser schrittweise verändert wird. Leser können eine Mischung aus alten und neuen Werten beobachten.
Ein Immutable Snapshot bündelt einen konsistenten Stand in einem unveränderlichen Objekt. Aktualisierung erzeugt vollständig einen neuen Snapshot und veröffentlicht ihn atomar; Leser benötigen keine feingranularen Locks.
Sicherer Refactoring-Ablauf
Zusammengehörige Werte und ihre gemeinsame Konsistenzgrenze bestimmen.
Einen unveränderlichen Snapshottyp mit defensiven Kopien definieren.
Neue Konfiguration vollständig validieren und außerhalb des kritischen Pfads aufbauen.
Snapshot über eine atomare Referenz austauschen.
In-place-Mutationen und einzelne volatile Felder entfernen.
Wann sinnvoll - und wann nicht
Snapshots eignen sich für überwiegend gelesene Zustände mit vergleichsweise seltenen Aktualisierungen. Sehr große oder hochfrequent veränderte Daten können Copy-on-Write zu teuer machen.
Risiken und typische Fehlanwendungen
Flache Unveränderlichkeit reicht nicht, wenn enthaltene Collections mutable bleiben. Alte Snapshots können bei falscher Referenzhaltung Speicher binden. Versions- und Aktualisierungssemantik müssen sichtbar sein.
Abgrenzung
Ein Snapshot ist ein unveränderlicher konsistenter Stand. Ein Cache kann Werte einzeln laden und ablaufen lassen und bietet nicht automatisch gemeinsame Konsistenz.
Verwandte Refactorings und Muster
Copy-on-Write, Configuration Snapshot, Value Object und Atomic Reference.
Enterprise-Java-Einordnung
PricingConfigurationSnapshot enthält Tarifversion, Rundungsregeln und Limits. Reload baut und validiert einen neuen Snapshot, bevor er atomar für neue Requests aktiviert wird.
Teststrategie
Tests prüfen defensive Kopien, atomare Sichtbarkeit und dass Leser ausschließlich alte oder neue vollständige Versionen sehen. Parallelitätstests verwenden kontrollierte Barrieren.
TTL, Cacheability und Schlüsselbildung sind über Controller und Adapter verteilt und widersprechen sich.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere eine reine Cache-Policy, die Schlüssel, Cachebarkeit und Gültigkeitsdauer erklärt.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CachePolicy.java
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Extract Cache Policy
// Policy Pattern: Cache-Semantik wird von Speichertechnik getrennt.
public final class CachePolicy {
public record Decision(boolean cacheable,String key,Duration ttl) {}
private final Duration ttl;
public CachePolicy(Duration ttl){ if(ttl==null||ttl.isZero()||ttl.isNegative()) throw new IllegalArgumentException(); this.ttl=ttl; }
public Decision forProduct(String productId,boolean personalized){ if(productId==null||productId.isBlank()) throw new IllegalArgumentException(); return personalized?new Decision(false,"",Duration.ZERO):new Decision(true,"product:"+productId,ttl); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import org.junit.jupiter.api.Test;
class CachePolicyTest { @Test void policy_explains_cache_semantics(){ var p=new CachePolicy(Duration.ofMinutes(5)); var d=p.forProduct("P-1",false); assertTrue(d.cacheable()); assertEquals("product:P-1",d.key()); assertFalse(p.forProduct("P-1",true).cacheable()); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Cachingentscheidungen sind als verstreute if-Abfragen, feste TTLs und direkte Map-Zugriffe in Fach- oder Adaptercode eingebettet. Schlüssel, Gültigkeit, negative Einträge und Invalidierung sind uneinheitlich.
Erkennungsmerkmale
Hardcodierte Duration-Werte; Cachezugriff in Use Cases; unterschiedliche Schlüssel für dieselben Daten; Cachefehler verändern Fachverhalten; Tests schlafen bis TTL abläuft.
Ziel und Nutzen
Eine Cache Policy macht Schlüsselbildung, TTL, Cachebarkeit, negative Ergebnisse und Invalidierungsanlässe ausdrücklich. Der Cacheadapter bleibt technisch, die Policy kann je Datentyp oder Mandant getestet werden.
Sicherer Refactoring-Ablauf
Lesepfade, Aktualisierungsquellen und Konsistenzanforderungen erfassen.
Cache Key und Cacheability als typisierte Regeln modellieren.
TTL und negative Cachefälle begründen statt pauschal setzen.
Policy vom Speicheradapter trennen und Clock injizieren.
Invalidierung, Stampede-Schutz und Fallbackverhalten testen.
Wann sinnvoll - und wann nicht
Daten mit strenger Echtzeitkonsistenz oder sehr geringer Wiederverwendung sollten nicht automatisch gecacht werden. Cache darf nicht zur versteckten primären Datenquelle werden.
Risiken und typische Fehlanwendungen
Unbegrenzte oder falsche Schlüssel führen zu Speicherproblemen und Datenlecks zwischen Mandanten. Lange TTL versteckt Aktualisierungen; Cache Stampede kann Backends überlasten.
Abgrenzung
Die Policy entscheidet, was und wie gecacht wird. Cache-Aside beschreibt den Ablauf, wie Anwendung und Cache beim Lesen zusammenarbeiten.
Verwandte Refactorings und Muster
Cache-Aside, Read-Through Cache, Immutable Snapshot und Single Flight.
Enterprise-Java-Einordnung
ProductCatalogCachePolicy definiert Schlüssel aus Tenant und ProductId, kurze negative TTL und Invalidierung bei ProductChanged. Der Redis-Adapter setzt nur die technische Speicherung.
Teststrategie
Fake Clock prüft Ablauf ohne Wartezeit. Tests decken Schlüsselisolation, negative Ergebnisse, Invalidierung und Backendfehler ab.
120. Introduce Circuit Breaker Policy Circuit Breaker / State Machine
Ausgangsproblem
Fehlerzähler, Öffnungszeit und Wiederanlauf sind direkt in einem HTTP-Client vermischt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere den Zustandsautomaten als unabhängige Policy. Der Adapter meldet Erfolg oder Fehler und fragt vor dem Aufruf nach Erlaubnis.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
CircuitBreakerPolicy.java
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Circuit Breaker Policy
// Circuit Breaker Pattern: Ausfallzustand und Wiederanlauf sind explizit modelliert.
public final class CircuitBreakerPolicy {
public enum State { CLOSED, OPEN }
private final int threshold; private final Duration coolDown; private int failures; private Instant openedAt; private State state=State.CLOSED;
public CircuitBreakerPolicy(int threshold,Duration coolDown){ if(threshold<1||coolDown==null||coolDown.isNegative()||coolDown.isZero()) throw new IllegalArgumentException(); this.threshold=threshold; this.coolDown=coolDown; }
public synchronized boolean allow(Instant now){ if(state==State.CLOSED) return true; if(!now.isBefore(openedAt.plus(coolDown))){ state=State.CLOSED; failures=0; openedAt=null; return true; } return false; }
public synchronized void success(){ failures=0; state=State.CLOSED; openedAt=null; }
public synchronized void failure(Instant now){ if(++failures>=threshold){ state=State.OPEN; openedAt=now; } }
public synchronized State state(){ return state; }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import org.junit.jupiter.api.Test;
class CircuitBreakerPolicyTest { @Test void breaker_opens_and_recovers(){ var p=new CircuitBreakerPolicy(2,Duration.ofSeconds(30)); var now=Instant.EPOCH; p.failure(now); p.failure(now); assertEquals(CircuitBreakerPolicy.State.OPEN,p.state()); assertFalse(p.allow(now.plusSeconds(10))); assertTrue(p.allow(now.plusSeconds(30))); } }
Einsatzgrenze: Die neue Abstraktion muss eine echte technische oder fachliche Grenze stabilisieren. Ein Port, eine Policy oder ein Kontext ohne konkrete Änderungsachse erhöht nur die Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Client ruft einen dauerhaft gestörten Remotedienst immer wieder auf. Jeder Aufruf wartet auf Timeout, bindet Ressourcen und verstärkt den Ausfall trotz bereits eindeutiger Fehlersignale.
Erkennungsmerkmale
Viele aufeinanderfolgende Timeouts; Retry auf jedem Request; Thread- oder Connection-Pools laufen voll; keine schnelle Ablehnung; Erholung wird nicht kontrolliert getestet.
Ziel und Nutzen
Eine Circuit Breaker Policy verfolgt relevante Fehler und wechselt zwischen geschlossen, offen und halb offen. Bei anhaltender Störung scheitern Aufrufe schnell; nach einer Wartezeit prüfen begrenzte Probeaufrufe die Erholung.
Sicherer Refactoring-Ablauf
Zu schützende Remoteabhängigkeit und relevante Fehler definieren.
Schwellenwert, Beobachtungsfenster und Öffnungsdauer festlegen.
Policy außerhalb des Fachmodells am Clientadapter platzieren.
Halboffene Probe und paralleles Verhalten ausdrücklich implementieren.
Metriken, Fallback und Zusammenspiel mit Retry und Deadline testen.
Wann sinnvoll - und wann nicht
Circuit Breaker ist nicht für lokale Validierungsfehler oder einzelne fachliche Ablehnungen gedacht. Bei sehr seltenen Aufrufen liefert ein statistischer Schwellenwert möglicherweise wenig Nutzen.
Risiken und typische Fehlanwendungen
Falsch kombinierte Retries verzögern das Öffnen oder erzeugen Stürme. Ein globaler Breaker kann unabhängige Mandanten oder Endpunkte unnötig koppeln. Fallbacks dürfen keine falschen Daten versprechen.
Abgrenzung
Retry versucht einen wahrscheinlich vorübergehenden Fehler erneut. Circuit Breaker verhindert weitere Versuche, wenn Erfolg kurzfristig unwahrscheinlich ist.
Verwandte Refactorings und Muster
Retry, Timeout, Deadline, Bulkhead und Load Shedding.
Enterprise-Java-Einordnung
CreditBureauGateway besitzt einen eigenen Breaker pro Provider. Bei offenem Zustand wird sofort ManualReview ausgelöst; halb offene Probeaufrufe sind streng begrenzt.
Teststrategie
Deterministische State-Machine-Tests prüfen Übergänge, Zeitfenster und Probeverhalten. Integrationstests simulieren Timeout, Erholung und parallele Requests.
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Anwendungslogik liest Mandant, Benutzer, Korrelation oder Locale aus ThreadLocal, statischen Helfern oder Framework-Kontexten. Die Abhängigkeit ist in Signaturen unsichtbar und bricht bei asynchroner Verarbeitung, Tests und parallelen Requests leicht auf.
Erkennungsmerkmale
Methoden liefern unterschiedliche Ergebnisse abhängig von globalem Zustand; Tests müssen ThreadLocal setzen; Kontext geht beim Executor-Wechsel verloren; Fachcode importiert HTTP- oder Security-APIs; Logging und Berechtigungen lesen dieselben Daten auf verschiedenen Wegen.
Ziel und Nutzen
Ein unveränderlicher Context Object macht die tatsächlich benötigten Laufzeitinformationen explizit. Aufrufer sehen die Abhängigkeit, asynchrone Übergaben werden kontrollierbar und innere Schichten bleiben frei von Containerdetails.
Sicherer Refactoring-Ablauf
Benötigte Kontextwerte und ihre fachliche Bedeutung inventarisieren.
Einen kleinen unveränderlichen Kontexttyp mit typisierten Werten einführen.
Am äußeren Adapter den Framework-Kontext genau einmal übersetzen.
Kontext explizit durch Use-Case- und Port-Signaturen führen.
ThreadLocal- und statische Zugriffe schrittweise entfernen und mit Paralleltests absichern.
Wann sinnvoll - und wann nicht
Sinnvoll bei Request-, Tenant-, Security- oder Correlation-Kontext, der mehrere Schichten durchläuft. Für rein lokale Hilfsdaten genügt ein normaler Parameter; ein universeller God Context wäre kontraproduktiv.
Risiken und typische Fehlanwendungen
Ein überladener Kontext wird zum versteckten Service Locator. Mutable Maps, optionale beliebige Werte oder Domänenservices im Kontext verwischen Verantwortlichkeiten. Datenschutzrelevante Daten dürfen nicht unkontrolliert geloggt werden.
Abgrenzung
Ein Context Object transportiert Laufzeitinformationen. Dependency Injection liefert langlebige Abhängigkeiten; ein Command oder Request-DTO transportiert fachliche Eingabedaten.
Ein Claims-Use-Case erhält RequestContext mit TenantId, PrincipalId und CorrelationId. Der REST-Adapter erstellt ihn aus Jakarta Security beziehungsweise Spring Security; Domäne und Anwendung kennen nur eigene Typen.
Teststrategie
Unit-Tests erzeugen Kontexte direkt. Parallel- und Executor-Tests prüfen Isolation und Weitergabe; Architekturtests verhindern erneute Framework-Imports in inneren Paketen.
Timeoutwerte werden mehrfach als Millisekunden übergeben und verlieren ihre fachliche Bedeutung.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Modelliere eine absolute Deadline mit Clock-basierter Restzeit und eindeutiger Ablaufprüfung.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
Deadline.java
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Deadline
// Value Object: Ein absolutes Zeitbudget ersetzt verstreute Timeout-Zahlen.
public record Deadline(Instant expiresAt) {
public Deadline { if (expiresAt == null) throw new IllegalArgumentException("expiresAt"); }
public static Deadline after(Duration duration, Clock clock){
if(duration==null||duration.isNegative()||duration.isZero()||clock==null) throw new IllegalArgumentException();
return new Deadline(clock.instant().plus(duration));
}
public boolean expired(Clock clock){ return !clock.instant().isBefore(expiresAt); }
public Duration remaining(Clock clock){ var d=Duration.between(clock.instant(),expiresAt); return d.isNegative()?Duration.ZERO:d; }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import org.junit.jupiter.api.Test;
class DeadlineTest { @Test void deadline_exposes_remaining_budget(){ var clock=Clock.fixed(Instant.EPOCH,ZoneOffset.UTC); var d=Deadline.after(Duration.ofSeconds(5),clock); assertEquals(Duration.ofSeconds(5),d.remaining(clock)); assertFalse(d.expired(clock)); assertTrue(d.expired(Clock.fixed(Instant.EPOCH.plusSeconds(5),ZoneOffset.UTC))); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Mehrere entfernte Aufrufe besitzen jeweils eigene Timeouts, ohne ein gemeinsames Endzeitbudget zu berücksichtigen. Eine Anfrage kann dadurch lange weiterarbeiten, obwohl ihr fachlich zulässiges Zeitfenster bereits überschritten ist.
Erkennungsmerkmale
Jeder Adapter setzt einen festen Timeout; Retries addieren unkontrolliert Laufzeit; nachgelagerte Arbeit startet kurz vor Ablauf; Abbruchentscheidungen basieren auf verstreuten Uhrzeitvergleichen; Tests hängen von realer Zeit ab.
Ziel und Nutzen
Ein Deadline Value Object beschreibt den spätesten zulässigen Abschlusszeitpunkt. Jede Stufe kann das verbleibende Budget berechnen und entscheiden, ob ein weiterer Versuch noch sinnvoll ist.
Sicherer Refactoring-Ablauf
Fachliches Gesamtzeitbudget am Eintrittspunkt bestimmen.
Clock und Deadline als eigene Typen einführen.
Verbleibende Dauer statt ursprünglichem Timeout an Adapter übergeben.
Vor teuren Schritten und Retries Budget prüfen.
Timeout-, Abbruch- und Fallbackpfade deterministisch testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei Orchestrierungen, interaktiven APIs und verteilten Workflows. Für einen einzelnen lokalen Aufruf reicht meist ein einfacher Timeout.
Risiken und typische Fehlanwendungen
Deadlines dürfen nicht mit absoluten Systemzeiten unterschiedlicher Hosts vermischt werden. Negatives Restbudget muss eindeutig behandelt werden. Zu aggressive Budgets erzeugen unnötige Abbrüche; fehlende Reserve verhindert geordnetes Aufräumen.
Abgrenzung
Ein Timeout begrenzt einen einzelnen Schritt. Eine Deadline begrenzt den gesamten Ablauf und wird durch die Kette weitergegeben.
Verwandte Refactorings und Muster
Deadline, Time Budget, Clock Port, Cancellation Token, Retry Policy und Circuit Breaker.
Enterprise-Java-Einordnung
Eine Zahlungsautorisierung erhält eine Deadline von zwei Sekunden. Fraud-Check, Provider-Call und Audit bekommen jeweils nur das verbleibende Budget; ein Retry startet nur, wenn noch genügend Reserve besteht.
Teststrategie
Fake Clock und tabellarische Szenarien prüfen Restbudget, Grenzzeitpunkt und Retry-Entscheidung. Integrationstests prüfen Abbruch und Ressourcenfreigabe.
Ein Adapter verwaltet Semaphore, Warteschlange und Ablehnung direkt neben dem Remote-Aufruf.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere eine kleine Kapazitäts-Policy, die Slots reserviert und sicher freigibt.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
BulkheadPolicy.java
package com.aydinsude.workbench.catalog;
import java.util.concurrent.Semaphore;
// Refactoring: Extract Bulkhead Policy
// Bulkhead Pattern: Begrenzte Parallelität isoliert knappe Ressourcen.
public final class BulkheadPolicy {
private final Semaphore slots;
public BulkheadPolicy(int capacity){ if(capacity<1) throw new IllegalArgumentException("capacity"); slots=new Semaphore(capacity,true); }
public Lease tryAcquire(){ return slots.tryAcquire()?new Lease(slots):null; }
public int available(){ return slots.availablePermits(); }
public static final class Lease implements AutoCloseable {
private final Semaphore semaphore; private boolean closed;
private Lease(Semaphore semaphore){ this.semaphore=semaphore; }
@Override public synchronized void close(){ if(!closed){ closed=true; semaphore.release(); } }
}
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class BulkheadPolicyTest { @Test void capacity_is_released_exactly_once(){ var p=new BulkheadPolicy(1); var lease=p.tryAcquire(); assertNotNull(lease); assertNull(p.tryAcquire()); lease.close(); lease.close(); assertEquals(1,p.available()); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Alle externen Aufrufe teilen denselben Thread-, Connection- oder Queue-Pool. Eine langsame oder fehlerhafte Integration verbraucht dadurch sämtliche Kapazität und blockiert fachlich unabhängige Funktionen.
Erkennungsmerkmale
Ein gemeinsamer Executor bedient alle Provider; Warteschlangen wachsen unbeschränkt; ein Ausfall macht das gesamte System langsam; Kapazitätszahlen stehen direkt im Adapter; Lasttests zeigen Kaskadeneffekte.
Ziel und Nutzen
Eine Bulkhead Policy teilt knappe Ressourcen bewusst nach Abhängigkeit oder Geschäftsfunktion. Fehler bleiben in einem abgegrenzten Bereich und wichtige Pfade behalten reservierte Kapazität.
Sicherer Refactoring-Ablauf
Gemeinsam genutzte Engpassressourcen und kritische Pfade messen.
Isolationsgrenzen und Kapazitätsbudgets fachlich benennen.
Policy von Executor-, Pool- oder Semaphore-Implementierung trennen.
Überlastreaktion und Warteschlangenlimit explizit modellieren.
Last- und Ausfalltests für jede Partition durchführen.
Wann sinnvoll - und wann nicht
Sinnvoll bei externen Integrationen, Mandanten oder Funktionen mit stark unterschiedlichem Risiko. Bei sehr kleiner Last kann die zusätzliche Partitionierung unnötig Kapazität verschwenden.
Risiken und typische Fehlanwendungen
Zu viele kleine Bulkheads reduzieren Gesamtauslastung. Unbegrenzte Warteschlangen umgehen die Isolation. Statische Größen ohne Messung altern schnell; Ablehnung braucht eine fachlich verständliche Reaktion.
Abgrenzung
Bulkhead isoliert Kapazität. Rate Limiter begrenzt Zulassungsrate; Circuit Breaker stoppt aussichtslose Aufrufe; Load Shedding lehnt unter Überlast gezielt Arbeit ab.
Verwandte Refactorings und Muster
Bulkhead, Capacity Policy, Semaphore Isolation, Executor Ownership und Load Shedding.
Enterprise-Java-Einordnung
Adressprüfung und Zahlungsprovider erhalten getrennte Executor- und Connection-Budgets. Ein langsamer Adressdienst darf Checkout-Zahlungen nicht blockieren.
Teststrategie
Lasttests sättigen gezielt einen Bulkhead und prüfen, dass andere Pfade ihre Latenz halten. Unit-Tests prüfen Zulassung, Queue-Limit und Ablehnungsgrund.
Rate-Limit-Zähler sind mit Controller- und HTTP-Code vermischt und kaum deterministisch testbar.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere eine tokenbasierte Policy mit expliziter Clock und klarer Entscheidung.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
RateLimiterPolicy.java
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Rate Limiter Policy
// Token Bucket Pattern: Durchsatzregeln werden vom Transport getrennt.
public final class RateLimiterPolicy {
private final int capacity; private final Duration refillEvery; private int tokens; private Instant lastRefill;
public RateLimiterPolicy(int capacity, Duration refillEvery, Clock clock){
if(capacity<1||refillEvery==null||refillEvery.isZero()||refillEvery.isNegative()||clock==null) throw new IllegalArgumentException();
this.capacity=capacity; this.refillEvery=refillEvery; this.tokens=capacity; this.lastRefill=clock.instant();
}
public synchronized boolean allow(Clock clock){
var elapsed=Duration.between(lastRefill,clock.instant()); long refill=elapsed.dividedBy(refillEvery);
if(refill>0){ tokens=(int)Math.min(capacity,tokens+refill); lastRefill=lastRefill.plus(refillEvery.multipliedBy(refill)); }
if(tokens==0) return false; tokens--; return true;
}
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import org.junit.jupiter.api.Test;
class RateLimiterPolicyTest { @Test void tokens_refill_deterministically(){ var base=Instant.EPOCH; var p=new RateLimiterPolicy(2,Duration.ofSeconds(10),Clock.fixed(base,ZoneOffset.UTC)); var c=Clock.fixed(base,ZoneOffset.UTC); assertTrue(p.allow(c)); assertTrue(p.allow(c)); assertFalse(p.allow(c)); assertTrue(p.allow(Clock.fixed(base.plusSeconds(10),ZoneOffset.UTC))); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Zugriffslimits sind als Zähler, Zeitvergleiche und magische Zahlen in Controllern oder Clients verteilt. Unterschiedliche Instanzen treffen widersprüchliche Entscheidungen und der fachliche Grund einer Ablehnung bleibt unsichtbar.
Erkennungsmerkmale
if-Blöcke prüfen Sekundenfenster; Limits sind hart codiert; Retry-After wird falsch berechnet; Tests schlafen real; Benutzer-, Mandanten- und Providerlimits sind vermischt.
Ziel und Nutzen
Eine Rate Limiter Policy kapselt Schlüssel, Kapazität, Nachfüllrate und Entscheidung. Die technische Speicherung von Token oder Zählern bleibt austauschbar und Aufrufer erhalten ein typisiertes Ergebnis.
Sicherer Refactoring-Ablauf
Zu schützende Ressource und fairen Limit-Schlüssel bestimmen.
Algorithmus und Zeitsemantik explizit auswählen.
Zulassungsentscheidung als Value/Decision Object modellieren.
Speicheradapter und verteilte Atomizität hinter einen Port legen.
Grenz-, Burst- und Mehrinstanzszenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll zum Schutz knapper Dienste, fairer Mandantennutzung und externer Quoten. Kein Ersatz für fachliche Autorisierung oder Kapazitätsplanung.
Risiken und typische Fehlanwendungen
Ein falscher Schlüssel benachteiligt Nutzer. Lokale In-Memory-Zähler sind in Clustern inkonsistent. Starre Limits ohne Burst-Modell erzeugen unnötige Ablehnung; Uhrdrift und atomare Updates müssen berücksichtigt werden.
Abgrenzung
Rate Limiting steuert Häufigkeit. Throttling kann Verarbeitung verlangsamen; Admission Control entscheidet zusätzlich anhand aktueller Systemressourcen.
Ein Partner-API-Limit wird je PartnerId und Operation ausgewertet. Der Adapter liefert Allowed oder Rejected(retryAfter), statt HTTP-Details in die Anwendung zu ziehen.
Produzenten schreiben unbegrenzt in Warteschlangen; Überlastung wird erst als Speicherproblem sichtbar.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Modelliere Annahme, Ablehnung und verbleibende Kapazität als explizite Policy.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
BackpressurePolicy.java
package com.aydinsude.workbench.catalog;
import java.util.concurrent.atomic.AtomicInteger;
// Refactoring: Introduce Backpressure Policy
// Backpressure Pattern: Überlastung wird als fachlich sichtbare Entscheidung behandelt.
public final class BackpressurePolicy {
public enum Decision { ACCEPT, REJECT }
private final int maximum; private final AtomicInteger inFlight=new AtomicInteger();
public BackpressurePolicy(int maximum){ if(maximum<1) throw new IllegalArgumentException(); this.maximum=maximum; }
public Decision tryEnter(){ while(true){ int current=inFlight.get(); if(current>=maximum) return Decision.REJECT; if(inFlight.compareAndSet(current,current+1)) return Decision.ACCEPT; } }
public void leave(){ int next=inFlight.decrementAndGet(); if(next<0){ inFlight.incrementAndGet(); throw new IllegalStateException("no work in flight"); } }
public int inFlight(){ return inFlight.get(); }
}
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Produzenten erzeugen Arbeit schneller als Konsumenten sie verarbeiten können. Warteschlangen, Speicher und Latenz wachsen unkontrolliert, weil keine explizite Regel für Überlast, Puffer und Verlust existiert.
Erkennungsmerkmale
Unbegrenzte Queues; Speicheranstieg unter Last; Timeouts weit hinter dem Eintritt; pauschales Drop oder blockierendes put; Reactive Streams ohne dokumentierte Nachfrage- und Overflow-Strategie.
Ziel und Nutzen
Eine Backpressure Policy macht Kapazität, Puffergrenze und Überlastreaktion zu einer bewussten Entscheidung: blockieren, ablehnen, zusammenfassen, älteste/neuste Elemente verwerfen oder auf persistenten Kanal ausweichen.
Sicherer Refactoring-Ablauf
Produzent, Konsument und maximale sinnvolle Verzögerung messen.
Puffergröße und fachlich zulässige Verluststrategie festlegen.
Overflow-Entscheidung typisieren und von Framework-Operatoren trennen.
Metriken, Alarmgrenzen und Recovery-Verhalten ergänzen.
Dauerlast-, Burst- und langsamer-Konsument-Szenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei Streams, Batch-Zufuhr und asynchronen Pipelines. Bei strikt synchroner Request-Response-Kommunikation reichen oft Deadline und Admission Control.
Risiken und typische Fehlanwendungen
Blindes Puffern verschiebt den Ausfall. Datenverlust darf nicht implizit sein. Blockierung kann Deadlocks erzeugen; pauschales Drop verletzt Fachanforderungen. Policy muss zur Datenklasse passen.
Abgrenzung
Backpressure reguliert den Fluss entlang einer Pipeline. Rate Limiting begrenzt Eintritt; Load Shedding verwirft Arbeit zum Schutz des Systems.
Verwandte Refactorings und Muster
Backpressure, Overflow Policy, Bounded Queue, Reactive Streams und Load Shedding.
Enterprise-Java-Einordnung
Telemetrieereignisse dürfen bei Überlast zusammengefasst werden, Finanzbuchungen dagegen werden persistent gepuffert und der Produzent erhält ein eindeutiges Nack.
Teststrategie
Lasttests prüfen Queuegrenze und stabile Speichernutzung. Property-Tests sichern, welche Elemente bei jeder Strategie erhalten bleiben.
Domainobjekte werden direkt mit Transportformaten und Feldnamen gekoppelt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere einen Serializer-Port und ein kanonisches Transfermodell an der Systemgrenze.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
SerializationBoundary.java
package com.aydinsude.workbench.catalog;
import java.nio.charset.StandardCharsets;
// Refactoring: Extract Serialization Boundary
// Anti-Corruption Layer: Transportformat und Domänenmodell bleiben getrennt.
public final class SerializationBoundary {
public record OrderMessage(String orderId, long amountCents) { public OrderMessage { if(orderId==null||orderId.isBlank()||amountCents<0) throw new IllegalArgumentException(); } }
public interface Serializer<T> { byte[] serialize(T value); }
public static final class CanonicalOrderSerializer implements Serializer<OrderMessage> {
public byte[] serialize(OrderMessage value){ return (value.orderId()+"|"+value.amountCents()).getBytes(StandardCharsets.UTF_8); }
}
}
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
JSON-, XML-, Avro- oder proprietäre Serialisierung ist direkt in Fachservices verteilt. Innere Modelle tragen Frameworkannotationen, Versionsfragen vermischen sich mit Fachlogik und externe Formate können die Domäne unkontrolliert verändern.
Erkennungsmerkmale
ObjectMapper-Aufrufe in Use Cases; Domänenobjekte mit Transportannotationen; Feldnamen werden in Fachcode ausgewertet; verschiedene Adapter serialisieren dieselben Typen unterschiedlich; Kompatibilitätsfehler treten spät auf.
Ziel und Nutzen
Eine Serialization Boundary kapselt Format, Schema, Version und technische Fehler. Innere Schichten arbeiten mit fachlichen Typen; Adapter übersetzen über explizite Verträge.
Sicherer Refactoring-Ablauf
Transportvertrag und Domänenmodell getrennt inventarisieren.
Serializer-Port mit richtungsbezogenen Methoden definieren.
DTO/Envelope und Mapper an der Grenze einführen.
Frameworkkonfiguration und Fehlerübersetzung in den Adapter verschieben.
Golden-Master-, Round-Trip- und Kompatibilitätstests ergänzen.
Wann sinnvoll - und wann nicht
Sinnvoll sobald Formate öffentlich, versioniert oder mehrfach vorhanden sind. Für einen rein internen triviale Record kann direkte Frameworkbindung akzeptabel sein, wenn die Kopplung bewusst ist.
Risiken und typische Fehlanwendungen
Ein generischer Serializer<Object> versteckt Verträge. Automatische polymorphe Typinformationen können Sicherheitsrisiken erzeugen. Round-Trip-Gleichheit ersetzt keine Abwärtskompatibilität.
Abgrenzung
Serialisierung bildet Daten in ein Format ab. Ein Mapper trennt Modelle; ein Anti-Corruption Layer schützt zusätzlich Semantik und Verhalten.
Verwandte Refactorings und Muster
Serializer Port, Data Mapper, Event Envelope, Schema Compatibility und Anti-Corruption Layer.
Enterprise-Java-Einordnung
IntegrationEventSerializer erzeugt versionierte JSON-Envelopes aus internen Domain Events. Unbekannte Versionen werden als typisierte Incompatibility gemeldet.
Teststrategie
Golden Master und Schema-Contract-Tests prüfen Feldnamen, Versionen, unbekannte Felder und alte Fixtures; Fuzzing deckt ungültige Eingaben ab.
Mehrere Threads verändern dieselbe HashMap; Synchronisation ist über Aufrufer verteilt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Kapsle atomare Operationen hinter einem thread-sicheren Repository-Vertrag.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
ConcurrentCustomerRepository.java
package com.aydinsude.workbench.catalog;
import java.util.*; import java.util.concurrent.ConcurrentHashMap;
// Refactoring: Replace Shared Mutable Collection with Concurrent Repository
// Repository Pattern: Atomare Zugriffe liegen an einer einzigen Grenze.
public final class ConcurrentCustomerRepository {
public record Customer(String id,String name){ public Customer { if(id==null||id.isBlank()||name==null||name.isBlank()) throw new IllegalArgumentException(); } }
private final ConcurrentHashMap<String,Customer> customers=new ConcurrentHashMap<>();
public boolean add(Customer customer){ return customers.putIfAbsent(customer.id(),customer)==null; }
public Optional<Customer> find(String id){ return Optional.ofNullable(customers.get(id)); }
public int size(){ return customers.size(); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class ConcurrentCustomerRepositoryTest { @Test void duplicate_insert_is_atomic(){ var r=new ConcurrentCustomerRepository(); var c=new ConcurrentCustomerRepository.Customer("C-1","Aydin"); assertTrue(r.add(c)); assertFalse(r.add(c)); assertEquals(1,r.size()); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Eine gemeinsam veränderliche Map oder Liste wird von mehreren Threads direkt gelesen und verändert. Synchronisation ist verstreut, mehrstufige Operationen sind nicht atomar und Fachinvarianten können zwischen einzelnen Collection-Aufrufen brechen.
Erkennungsmerkmale
Synchronized-Blöcke an vielen Stellen; ConcurrentHashMap wird direkt überall benutzt; check-then-act-Sequenzen; Collection wird nach außen gegeben; Tests sind sporadisch instabil.
Ziel und Nutzen
Ein Concurrent Repository kapselt Datenstruktur, Atomizität und fachlich benannte Operationen. Aufrufer sehen keine Collection-API und können Invarianten nicht durch Teiloperationen umgehen.
Sicherer Refactoring-Ablauf
Alle Zugriffs- und Änderungsmuster erfassen.
Fachlich atomare Operationen benennen.
Repository-Interface ohne Collection-Leak definieren.
Implementierung mit compute, Locks oder immutable snapshots atomar machen.
Paralleltests und linearisierbare Invarianten prüfen.
Wann sinnvoll - und wann nicht
Sinnvoll für prozesslokale gemeinsam genutzte Zustände. Bei dauerhaftem oder verteiltem Zustand ist meist ein externer Store mit passenden Konsistenzgarantien nötig.
Risiken und typische Fehlanwendungen
Concurrent Collection allein macht Sequenzen nicht atomar. Rückgabe veränderlicher Werte oder Collections öffnet die Kapselung erneut. Unbegrenztes Wachstum und fehlende Eviction bleiben Betriebsrisiken.
Abgrenzung
Ein Repository bietet fachliche Zugriffe. Eine Concurrent Map ist nur eine Datenstruktur; Unit of Work oder Datenbanktransaktionen lösen andere Konsistenzprobleme.
Verwandte Refactorings und Muster
Repository, Monitor Object, Concurrent Map, Atomic Operation und Immutable Snapshot.
Enterprise-Java-Einordnung
Eine IdempotencyRepository bietet reserve(key), complete(key,result) und release(key) als atomare Operationen statt get/put-Kombinationen.
Teststrategie
Mehrthreadtests starten konkurrierende Reservierungen und prüfen genau einen Gewinner. Invariant- und Stress-Tests laufen mit reproduzierbarem Seed.
Reservierungen werden mit separaten acquire/release-Aufrufen verwaltet und bei Exceptions vergessen.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Führe ein AutoCloseable-Lease-Objekt ein, das Besitz und Freigabe zusammenführt.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
Lease.java
package com.aydinsude.workbench.catalog;
import java.time.*; import java.util.concurrent.atomic.AtomicBoolean;
// Refactoring: Introduce Lease Object
// Lease Pattern: Zeitlich begrenzter Besitz wird als Objekt mit Lebenszyklus modelliert.
public final class Lease implements AutoCloseable {
private final String resourceId; private final Instant validUntil; private final Runnable release; private final AtomicBoolean closed=new AtomicBoolean();
public Lease(String resourceId,Instant validUntil,Runnable release){ if(resourceId==null||resourceId.isBlank()||validUntil==null||release==null) throw new IllegalArgumentException(); this.resourceId=resourceId; this.validUntil=validUntil; this.release=release; }
public String resourceId(){ return resourceId; }
public boolean validAt(Instant now){ return !closed.get() && now.isBefore(validUntil); }
@Override public void close(){ if(closed.compareAndSet(false,true)) release.run(); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test;
class LeaseTest { @Test void release_happens_once(){ var count=new AtomicInteger(); var lease=new Lease("R-1",Instant.EPOCH.plusSeconds(10),count::incrementAndGet); assertTrue(lease.validAt(Instant.EPOCH)); lease.close(); lease.close(); assertEquals(1,count.get()); assertFalse(lease.validAt(Instant.EPOCH)); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Eine Ressource wird reserviert oder gesperrt, aber Besitz, Ablauf und Freigabe sind über mehrere primitive Werte und Methoden verteilt. Fehlerpfade vergessen Freigaben; veraltete Besitzer blockieren dauerhaft.
Erkennungsmerkmale
lock()/unlock() in verschiedenen Schichten; separate ownerId und expiresAt; Freigabe in finally fehlt; Token werden als Strings weitergereicht; Erneuerung ist unklar.
Ziel und Nutzen
Ein Lease Object bündelt Besitznachweis, Gültigkeitsdauer und kontrollierte Freigabe. Es kann AutoCloseable implementieren und verhindert, dass fremde oder abgelaufene Besitzer Änderungen ausführen.
Sicherer Refactoring-Ablauf
Reservierungsprotokoll und Besitzidentität dokumentieren.
LeaseId, Owner und Expiry typisieren.
Acquire als Factory/Port und Release/Renew als Lease-Verhalten modellieren.
try-with-resources oder expliziten Workflow einführen.
Ablauf, Erneuerung, Doppel-Freigabe und Prozessabbruch testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei zeitlich begrenztem Ressourcenbesitz, verteilten Locks und Consumer-Partitionen. Für rein lexikalische lokale Ressourcen genügt oft AutoCloseable ohne Ablauf.
Risiken und typische Fehlanwendungen
Lease ist keine perfekte Exklusivitätsgarantie bei Netzwerkpartitionen. Fencing Tokens können nötig sein. Uhrabhängigkeit, zu lange Laufzeiten und stillschweigende Erneuerung erzeugen Sicherheitslücken.
Abgrenzung
Ein Lock blockiert Zugriff. Eine Lease gewährt Besitz nur für begrenzte Zeit und benötigt häufig einen Besitz- oder Fencing-Token.
Verwandte Refactorings und Muster
Lease, RAII/AutoCloseable, Distributed Lock, Fencing Token und Leader Election.
Enterprise-Java-Einordnung
Ein ExportWorker erwirbt eine PartitionLease mit monotonem FencingToken. Jede Schreiboperation prüft den Token; abgelaufene Worker können keine Ergebnisse mehr veröffentlichen.
Teststrategie
Fake-Clock-Tests prüfen Ablauf und Renew. Konkurrenztests prüfen Besitzwechsel und Fencing; Fehlerpfadtests sichern garantierte Freigabe.
Feature-Checks sind als String-Vergleiche über Controller, Services und Adapter verteilt.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Extrahiere eine typisierte Policy, die Kontext und Rollout-Regel zentral auswertet.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
FeatureTogglePolicy.java
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Extract Feature Toggle Policy
// Feature Toggle Pattern: Rolloutregeln werden zentral und typisiert entschieden.
public final class FeatureTogglePolicy {
public enum Feature { NEW_PRICING, FAST_CHECKOUT }
public record Context(String tenantId,Set<Feature> enabled){ public Context { if(tenantId==null||tenantId.isBlank()||enabled==null) throw new IllegalArgumentException(); enabled=Set.copyOf(enabled); } }
public boolean enabled(Feature feature,Context context){ if(feature==null||context==null) throw new IllegalArgumentException(); return context.enabled().contains(feature); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.*; import org.junit.jupiter.api.Test;
class FeatureTogglePolicyTest { @Test void rollout_is_typed(){ var p=new FeatureTogglePolicy(); var c=new FeatureTogglePolicy.Context("tenant-a",Set.of(FeatureTogglePolicy.Feature.NEW_PRICING)); assertTrue(p.enabled(FeatureTogglePolicy.Feature.NEW_PRICING,c)); assertFalse(p.enabled(FeatureTogglePolicy.Feature.FAST_CHECKOUT,c)); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Feature-Schalter werden als verteilte if-Abfragen mit Stringnamen gelesen. Zielgruppe, Rollout, Notfallabschaltung und Abhängigkeiten sind nicht zentral definiert; veraltete Toggles bleiben dauerhaft im Code.
Erkennungsmerkmale
config.getBoolean("new-x") in Fachklassen; unterschiedliche Defaults; Nutzerzuordnung nicht deterministisch; Toggles verschachteln sich; niemand kennt Owner oder Ablaufdatum.
Ziel und Nutzen
Eine Feature Toggle Policy kapselt fachlich benannten Schalter, Zielgruppe, stabile Kohortenzuordnung, Default, Gültigkeit und Entscheidungsevidenz. Der eigentliche Use Case erhält eine klare Entscheidung statt Konfigurationsdetails.
Sicherer Refactoring-Ablauf
Toggle-Arten und Verantwortliche inventarisieren.
Typisierten FeatureKey und EvaluationContext einführen.
Policy und Toggle-Provider trennen.
Entscheidung als Enabled/Disabled mit Grund modellieren.
Ablaufdatum, Telemetrie und Cleanup-Test ergänzen.
Wann sinnvoll - und wann nicht
Sinnvoll für kontrollierte Releases, Experimente und Kill Switches. Kein Ersatz für dauerhafte fachliche Varianten oder Berechtigungen.
Risiken und typische Fehlanwendungen
Toggle-Schulden und kombinatorische Zustände wachsen schnell. Nicht deterministische Zuordnung verfälscht Tests. Sicherheitsregeln dürfen nicht allein über Release-Toggles gesteuert werden.
Feature Toggle, Canary Cohort, Kill Switch, Branch by Abstraction und Rollout Guard.
Enterprise-Java-Einordnung
PricingV2Policy aktiviert eine neue Tarifberechnung für stabile Kundenkohorten. Beide Pfade schreiben vergleichbare Metriken; nach erfolgreichem Rollout wird der alte Pfad entfernt.
130. Introduce Health Probe Port Health Probe / Port
Ausgangsproblem
Health-Endpunkte greifen direkt auf Datenbank-Clients und technische Exceptions zu.
Sichere Transformation
Bestehendes Verhalten mit einem fokussierten Characterization Test absichern.
Definiere einen kleinen Probe-Port und übersetze technische Zustände in ein stabiles Ergebnis.
Aufrufer schrittweise migrieren, nach jedem Schritt kompilieren und Tests ausführen.
Java-21-Zielcode
HealthProbePort.java
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Introduce Health Probe Port
// Ports and Adapters: Betriebszustand wird über einen stabilen Vertrag veröffentlicht.
public interface HealthProbePort {
record Result(Status status,String detail,Duration latency){ public Result { if(status==null||detail==null||latency==null||latency.isNegative()) throw new IllegalArgumentException(); } }
enum Status { UP, DEGRADED, DOWN }
Result check();
static HealthProbePort fixed(Status status,String detail,Duration latency){ return ()->new Result(status,detail,latency); }
}
JUnit-Test
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.*; import org.junit.jupiter.api.Test;
class HealthProbePortTest { @Test void probe_exposes_stable_result(){ var p=HealthProbePort.fixed(HealthProbePort.Status.UP,"database reachable",Duration.ofMillis(12)); var r=p.check(); assertEquals(HealthProbePort.Status.UP,r.status()); assertEquals(Duration.ofMillis(12),r.latency()); } }
Einsatzgrenze: Die neue Abstraktion ist sinnvoll, wenn sie eine reale Änderungs-, Last- oder Integrationsgrenze sichtbar macht. Ohne konkrete Änderungsachse wäre sie unnötige Indirektion.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Health-Endpunkte fragen Datenbank, Broker oder Fremdsysteme direkt aus Controller- oder Frameworkcode ab. Technische Details, Betriebssemantik und Fachverfügbarkeit werden vermischt; teure Checks überlasten Abhängigkeiten.
Erkennungsmerkmale
Ein /health-Handler enthält JDBC- und HTTP-Aufrufe; Liveness und Readiness sind identisch; Fehlertexte werden direkt exponiert; Tests benötigen reale Infrastruktur.
Ziel und Nutzen
Ein Health Probe Port liefert ein typisiertes, kleines Zustandsmodell. Adapter implementieren günstige technische Prüfungen; die Komposition entscheidet getrennt über Liveness, Readiness und Degraded.
Sicherer Refactoring-Ablauf
Betriebsfragen Liveness, Readiness und fachliche Degradation trennen.
HealthStatus und ComponentCheck typisieren.
Ports für benötigte Prüfungen definieren.
Framework-Health-Adapter auf das interne Modell abbilden.
Timeout, Cache und sensible Detailausgabe festlegen.
Wann sinnvoll - und wann nicht
Sinnvoll für produktive Dienste und orchestrierte Laufzeiten. Ein Probe darf keine umfangreiche End-to-End-Geschäftstransaktion ausführen.
Risiken und typische Fehlanwendungen
Abhängige Dienste in Liveness einzubauen erzeugt Neustartschleifen. Teure Prüfungen können selbst Ausfälle verursachen. Interne Fehlerdetails dürfen nicht öffentlich werden.
Abgrenzung
Liveness beantwortet, ob der Prozess neu gestartet werden muss. Readiness beantwortet, ob er Verkehr annehmen kann. Observability-Metriken ersetzen keine Probe.
Verwandte Refactorings und Muster
Health Probe Port, Liveness/Readiness, Circuit Breaker State und Degradation Policy.
Enterprise-Java-Einordnung
OrderService bleibt live, wenn Kafka ausfällt, wird aber für neue Bestellungen not ready; reine Lesezugriffe können als degraded weiterlaufen.
Kapitel 14 Outbox, Transaktionsgrenzen und zuverlässige Zustellung10 Refactorings
Zuverlässige verteilte Verarbeitung: Outbox, Inbox, Saga, Kompensation, Event-Versionierung, Partitionierung, Dead Letters, Zustellversuche und Observability.
131. Extract Outbox Writer Transactional Outbox
Ausgangsproblem
Geschäftsänderung und Event-Publikation werden getrennt gespeichert; bei Fehlern entsteht Inkonsistenz.
Sichere Transformation
Extrahiere einen OutboxWriter, der Eventdaten innerhalb derselben Transaktion als unveränderlichen Eintrag persistiert.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Extract Outbox Writer
// Pattern: Transactional Outbox - fachliche Änderung und Publikationsauftrag werden atomar gespeichert.
public final class OutboxWriter {
public record Entry(UUID id,String aggregateId,String type,String payload,Instant occurredAt){
public Entry { Objects.requireNonNull(id); if(aggregateId==null||aggregateId.isBlank()) throw new IllegalArgumentException("aggregateId"); if(type==null||type.isBlank()) throw new IllegalArgumentException("type"); Objects.requireNonNull(payload); Objects.requireNonNull(occurredAt); }
}
private final List<Entry> entries=new ArrayList<>();
public Entry append(String aggregateId,String type,String payload,Instant occurredAt){ var e=new Entry(UUID.nameUUIDFromBytes((aggregateId+type+payload+occurredAt).getBytes()),aggregateId,type,payload,occurredAt); entries.add(e); return e; }
public List<Entry> pending(){ return List.copyOf(entries); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import org.junit.jupiter.api.Test;
class OutboxWriterTest { @Test void appends_immutable_entry(){ var w=new OutboxWriter(); var e=w.append("O-1","OrderPlaced","{}",Instant.EPOCH); assertEquals("O-1",e.aggregateId()); assertEquals(1,w.pending().size()); assertThrows(UnsupportedOperationException.class,()->w.pending().clear()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Anwendungsservices schreiben fachliche Daten und bauen zusätzlich Outbox-Zeilen mit SQL, JSON und Headern zusammen. Transaktionslogik, Ereignisvertrag und Persistenzdetails sind vermischt und werden je Use Case unterschiedlich umgesetzt.
Erkennungsmerkmale
Direkte INSERTs in outbox; Event-JSON wird im Service erzeugt; fehlende globale EventId; unterschiedliche Statuswerte; Tests prüfen SQL statt fachliches Verhalten.
Ziel und Nutzen
Ein Outbox Writer Port übernimmt die atomare Ablage eines typisierten Integration Events im laufenden Transaktionskontext. Ein Adapter verantwortet Envelope, Serialisierung und Persistenzschema.
Sicherer Refactoring-Ablauf
Vorhandene Dual-Write- und Outboxstellen inventarisieren.
Stabilen Event- und Envelope-Vertrag definieren.
OutboxWriter als Port mit append(event) einführen.
Persistenz und Serialisierung in einen Adapter verschieben.
Use Cases innerhalb derselben Transaktion auf den Port umstellen.
Wann sinnvoll - und wann nicht
Sinnvoll wenn Datenänderung und spätere Veröffentlichung zuverlässig gekoppelt werden müssen. Bei rein lokaler Reaktion ohne externe Zustellung genügt ein internes Domain Event.
Risiken und typische Fehlanwendungen
Outbox-Schreiben außerhalb der Fachtransaktion zerstört die Garantie. Große Payloads, fehlende Retention und unklare Eventversionen machen die Tabelle unbeherrschbar.
Abgrenzung
Der Outbox Writer persistiert Ereignisse. Der Outbox Publisher liest und veröffentlicht sie später; beide Rollen müssen getrennt bleiben.
Verwandte Refactorings und Muster
Transactional Outbox, Integration Event, Event Envelope, Publisher Port und Unit of Work.
Enterprise-Java-Einordnung
Nach PolicyIssued speichert der Use Case Aggregat und PolicyIssuedIntegrationEvent über denselben Unit-of-Work-Kontext. Der Writer erzeugt EventId, Version und occurredAt.
Teststrategie
Transaktionstests prüfen atomare Speicherung. Contract-Tests sichern Envelope und Serialisierung; Fehlerfälle dürfen keine halbe Fachänderung hinterlassen.
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Message Consumer führen bei wiederholter Zustellung dieselbe fachliche Wirkung mehrfach aus. Einzelne Handler prüfen ad hoc IDs oder verlassen sich fälschlich auf genau-einmalige Brokerzustellung.
Erkennungsmerkmale
Doppelte Rechnungen oder E-Mails; get-then-put-Deduplizierung; MessageId fehlt; Verarbeitung und Markierung liegen in verschiedenen Transaktionen; TTL ist unklar.
Ziel und Nutzen
Eine Inbox-Deduplizierung reserviert eine stabile MessageId atomar, führt die Wirkung genau einmal pro Consumer-Kontext aus und speichert Abschlussstatus gemeinsam mit der Fachänderung.
Sicherer Refactoring-Ablauf
Stabile Nachrichtenidentität und Consumer-Scope festlegen.
InboxRepository mit atomarer reserve/complete-Semantik definieren.
Reservierung, Fachänderung und Abschluss in eine Transaktionsgrenze bringen.
Wiederholte und hängende Zustellungen explizit behandeln.
Retention, Replay und Monitoring festlegen.
Wann sinnvoll - und wann nicht
Sinnvoll für at-least-once-Messaging und wiederholbare Webhooks. Nicht nötig, wenn Wirkung selbst vollständig idempotent und kostengünstig ist, bleibt aber oft als Evidenz wertvoll.
Risiken und typische Fehlanwendungen
Check-then-act ist racy. Zu frühes complete verliert Nachrichten; zu spätes Markieren kann Doppelwirkungen erzeugen. Eine globale MessageId ohne Consumer-Scope kann legitime Verbraucher blockieren.
Abgrenzung
Idempotent Consumer beschreibt die Eigenschaft. Inbox ist ein persistentes Muster, das Verarbeitung und Deduplizierung dokumentiert.
Verwandte Refactorings und Muster
Inbox Pattern, Idempotent Consumer, MessageId, Transaction Boundary und Replay.
Enterprise-Java-Einordnung
InvoiceConsumer reserviert (consumerName,eventId), bucht die Rechnung und markiert den Eintrag in derselben Datenbanktransaktion als completed.
Teststrategie
Integrationstests liefern dieselbe Nachricht parallel und wiederholt. Crash-Tests prüfen Zustände zwischen reserve und complete; Retentiontests sichern Replay-Anforderungen.
133. Replace Dual Write with Transactional Outbox Transactional Boundary
Ausgangsproblem
Servicecode schreibt Datenbank und Broker nacheinander; ein Teilausfall hinterlässt widersprüchlichen Zustand.
Sichere Transformation
Ersetze den direkten Brokeraufruf durch einen atomaren Commit von Aggregat und Outbox-Eintrag.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Replace Dual Write with Transactional Outbox
// Unit of Work + Outbox: Business-State und Event werden gemeinsam committed.
public final class TransactionalOrderCommit {
public record Order(String id,String status){}
public record Commit(Order order,OutboxWriter.Entry event){}
private final Map<String,Order> orders=new HashMap<>(); private final OutboxWriter outbox;
public TransactionalOrderCommit(OutboxWriter outbox){ this.outbox=Objects.requireNonNull(outbox); }
public Commit place(String id,Instant now){ if(id==null||id.isBlank()) throw new IllegalArgumentException("id"); var order=new Order(id,"PLACED"); var event=outbox.append(id,"OrderPlaced","{\"id\":\""+id+"\"}",now); orders.put(id,order); return new Commit(order,event); }
public Optional<Order> find(String id){ return Optional.ofNullable(orders.get(id)); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import org.junit.jupiter.api.Test;
class TransactionalOrderCommitTest { @Test void state_and_event_are_created_together(){ var outbox=new OutboxWriter(); var c=new TransactionalOrderCommit(outbox); c.place("O-9",Instant.EPOCH); assertTrue(c.find("O-9").isPresent()); assertEquals(1,outbox.pending().size()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Use Case speichert Geschäftsdaten und veröffentlicht anschließend direkt an einen Broker. Fällt eine Seite aus, entstehen dauerhaft inkonsistente Zustände: gespeicherte Daten ohne Ereignis oder Ereignis ohne bestätigte Daten.
Erkennungsmerkmale
Repository.save gefolgt von publisher.send; komplexe Kompensations-catches; Retry kann doppelt veröffentlichen; Transaktionen enden vor oder nach Netzwerkaufruf unklar.
Ziel und Nutzen
Die Transactional Outbox ersetzt den nicht atomaren Netzwerk-Dual-Write durch zwei zuverlässige Schritte: Fachänderung plus Outbox in einer lokalen Transaktion, danach asynchrone idempotente Veröffentlichung.
Sicherer Refactoring-Ablauf
Inkonsistenzfenster und erforderliche Ereignisse identifizieren.
Integration Event und Outboxschema definieren.
Fachänderung und Outboxinsert atomar speichern.
Separaten Publisher mit Claim/Retry/Idempotenz einführen.
Direkten Brokeraufruf entfernen und Betriebsmetriken ergänzen.
Wann sinnvoll - und wann nicht
Sinnvoll wenn lokale Datenbank und externer Broker nicht in einer gemeinsamen zuverlässigen Transaktion liegen. Für synchrone Antwortdaten ist das Muster nicht automatisch passend.
Risiken und typische Fehlanwendungen
Outbox garantiert keine sofortige Zustellung und keine globale Reihenfolge. Publisher, Retention, Poison Events und Schemaevolution werden zu Betriebsverantwortung. Verbraucher müssen Duplikate tolerieren.
Abgrenzung
2PC versucht globale Atomizität. Outbox akzeptiert zeitliche Verzögerung und erreicht zuverlässige Eventual Consistency mit lokalen Transaktionen.
Verwandte Refactorings und Muster
Transactional Outbox, Inbox, Idempotent Consumer, CDC und Integration Event.
Enterprise-Java-Einordnung
Order- und Outboxzeile werden gemeinsam committed. Ein Poller oder CDC-Connector veröffentlicht später; der API-Aufruf muss nicht auf Kafka warten.
Teststrategie
Datenbanktests prüfen atomaren Commit/Rollback. Publishertests simulieren Crash nach Send vor Ack; End-to-End-Tests prüfen eventual delivery und Duplikattoleranz.
134. Extract Saga Coordinator Saga / Process Manager
Ausgangsproblem
Mehrstufige Geschäftsabläufe sind als verschachtelte Serviceaufrufe implementiert und verlieren Fortschritt und Fehlerzustand.
Sichere Transformation
Extrahiere einen SagaCoordinator mit expliziten Schritten, Status und deterministischem Übergang.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Saga Coordinator
// Pattern: Saga / Process Manager - verteilt ausgeführte Schritte erhalten expliziten Zustand.
public final class SagaCoordinator {
public enum Status { NEW, PAYMENT_RESERVED, INVENTORY_RESERVED, COMPLETED, COMPENSATING, FAILED }
public record Saga(String id,Status status,List<String> history){ public Saga { history=List.copyOf(history); } }
public Saga start(String id){ return new Saga(id,Status.NEW,List.of("started")); }
public Saga advance(Saga saga,String signal){ var h=new ArrayList<>(saga.history()); Status next=switch(saga.status()){ case NEW -> signal.equals("payment-ok")?Status.PAYMENT_RESERVED:Status.FAILED; case PAYMENT_RESERVED -> signal.equals("inventory-ok")?Status.INVENTORY_RESERVED:Status.COMPENSATING; case INVENTORY_RESERVED -> signal.equals("confirm")?Status.COMPLETED:Status.COMPENSATING; default -> saga.status(); }; h.add(signal+" -> "+next); return new Saga(saga.id(),next,h); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class SagaCoordinatorTest { @Test void transitions_are_explicit(){ var c=new SagaCoordinator(); var s=c.start("S-1"); s=c.advance(s,"payment-ok"); s=c.advance(s,"inventory-ok"); s=c.advance(s,"confirm"); assertEquals(SagaCoordinator.Status.COMPLETED,s.status()); assertEquals(4,s.history().size()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Anwendungsservice ruft mehrere lokale oder entfernte Schritte nacheinander auf und enthält Zustandsvariablen, Retry- und Kompensationslogik in einer großen Methode. Wiederaufnahme nach Absturz ist nicht möglich.
Erkennungsmerkmale
Tiefe try/catch-Ketten; boolesche flags für erledigte Schritte; manuelle Rückrufe in umgekehrter Reihenfolge; Prozesszustand nur im Speicher; Timeoutlogik im Service.
Ziel und Nutzen
Ein Saga Coordinator modelliert den langlebigen Prozess als expliziten Zustand mit Schritten, Ereignissen und Kompensationen. Er entscheidet, welcher Command als Nächstes ausgeführt wird und kann nach Neustart fortsetzen.
Sicherer Refactoring-Ablauf
Prozessschritte, Erfolgsergebnisse und Fehlerfälle aufzeichnen.
Persistenten SagaState mit Korrelation und Version einführen.
Übergänge als reine Entscheidungslogik modellieren.
Befehle und Antworten über Ports anbinden.
Timeout, Retry, Kompensation und Wiederaufnahme testen.
Wann sinnvoll - und wann nicht
Sinnvoll für verteilte Geschäftsprozesse ohne globale ACID-Transaktion. Für kurze lokale Operationen ist eine normale Transaktion einfacher und sicherer.
Risiken und typische Fehlanwendungen
Ein zentraler Coordinator kann zu einem fachlosen Orchestrierungsmonster werden. Kompensation ist nicht immer echte Rückabwicklung. Parallelität, Duplikate und verspätete Antworten müssen explizit behandelt werden.
Abgrenzung
Saga bezeichnet den verteilten Prozess. Process Manager/Saga Coordinator ist eine konkrete orchestrierende Umsetzung; Choreografie verteilt Übergänge auf Ereignisreaktionen.
Verwandte Refactorings und Muster
Saga, Process Manager, State Machine, Command, Compensation und Outbox/Inbox.
Enterprise-Java-Einordnung
Eine Reisebuchung koordiniert Flug, Hotel und Zahlung. Jeder bestätigte Schritt wird persistiert; bei Zahlungsfehler erzeugt der Coordinator CancelHotel und CancelFlight.
Teststrategie
State-Transition-Tests prüfen jeden Eingang. Replay- und Crash-Tests sichern Wiederaufnahme; Contract-Tests prüfen Commands und Events.
Rollback-Logik ist als verstreute catch-Blöcke implementiert und kann nicht protokolliert oder wiederholt werden.
Sichere Transformation
Modelliere jede Kompensation als idempotentes Command-Objekt mit eigenem Schlüssel.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Compensation Command
// Command Pattern: Kompensationen werden explizit, protokollierbar und idempotent.
public interface CompensationCommand {
String idempotencyKey(); void execute();
final class Runner {
private final Set<String> completed=new HashSet<>();
public boolean run(CompensationCommand command){ Objects.requireNonNull(command); if(!completed.add(command.idempotencyKey())) return false; try { command.execute(); return true; } catch(RuntimeException ex){ completed.remove(command.idempotencyKey()); throw ex; } }
}
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test;
class CompensationCommandTest { @Test void command_runs_once(){ var n=new AtomicInteger(); CompensationCommand c=new CompensationCommand(){ public String idempotencyKey(){return "refund-1";} public void execute(){n.incrementAndGet();} }; var r=new CompensationCommand.Runner(); assertTrue(r.run(c)); assertFalse(r.run(c)); assertEquals(1,n.get()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fehlerbehandlung ruft beliebige Gegenmethoden auf, ohne die fachliche Bedeutung, Idempotenz und benötigten Daten der Kompensation zu modellieren. Nach Prozessabbruch ist nicht nachvollziehbar, was rückgängig gemacht werden soll.
Erkennungsmerkmale
catch-Blöcke mit refund(), delete() oder undo(); Parameter werden aus aktuellem Zustand rekonstruiert; doppelte Kompensation hat Nebenwirkungen; kein Audit oder Status.
Ziel und Nutzen
Ein Compensation Command beschreibt eine fachlich benannte ausgleichende Handlung mit stabiler Identität und den beim ursprünglichen Schritt gesicherten Daten. Er kann gespeichert, wiederholt und auditiert werden.
Sicherer Refactoring-Ablauf
Für jeden kompensierbaren Schritt die reale fachliche Ausgleichswirkung definieren.
Erforderliche Originaldaten beim Vorwärtsschritt sichern.
Typisierten Command mit CompensationId einführen.
Handler idempotent und statusgeführt implementieren.
Saga-Übergänge und manuelle Eskalation ergänzen.
Wann sinnvoll - und wann nicht
Sinnvoll in Sagas und langlebigen Workflows. Nicht jede Handlung ist kompensierbar; dann braucht es manuelle Korrektur oder eine andere Prozessgestaltung.
Risiken und typische Fehlanwendungen
Kompensation ist häufig keine zeitliche Rückkehr zum alten Zustand. Externe Fristen, Gebühren oder bereits konsumierte Leistungen müssen berücksichtigt werden. Blinde Deletes zerstören Auditierbarkeit.
Abgrenzung
Rollback stellt eine lokale Transaktion atomar zurück. Compensation ist eine neue fachliche Transaktion, die Auswirkungen ausgleicht.
Verwandte Refactorings und Muster
Compensation Command, Saga, Command Pattern, Idempotency Key und Audit Trail.
Enterprise-Java-Einordnung
RefundPaymentCommand enthält PaymentId, ursprüngliche CaptureId, Betrag und Reason. Wiederholte Ausführung liefert denselben Refund-Beleg statt erneut zu zahlen.
Teststrategie
Tests prüfen Idempotenz, fachliche Grenzfälle und verspätete Antworten. Prozess-Tests simulieren Fehler während der Kompensation und manuelle Recovery.
136. Extract Event Version Translator Upcaster / Anti-Corruption Layer
Ausgangsproblem
Consumercode enthält verstreute if-Abfragen für alte Eventversionen und vermischt Migration mit Fachlogik.
Sichere Transformation
Extrahiere einen Translator, der alte Eventformen in ein kanonisches aktuelles Modell überführt.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Event Version Translator
// Upcaster + Anti-Corruption Layer: alte Eventschemata werden an einer Grenze normalisiert.
public final class EventVersionTranslator {
public record Raw(int version,Map<String,String> fields){ public Raw { fields=Map.copyOf(fields); } }
public record CustomerChanged(String customerId,String email){}
public CustomerChanged translate(Raw raw){ return switch(raw.version()){ case 1 -> new CustomerChanged(required(raw,"id"),required(raw,"mail")); case 2 -> new CustomerChanged(required(raw,"customerId"),required(raw,"email")); default -> throw new IllegalArgumentException("unsupported version "+raw.version()); }; }
private static String required(Raw r,String key){ var v=r.fields().get(key); if(v==null||v.isBlank()) throw new IllegalArgumentException(key); return v; }
}
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Consumer enthalten verzweigte Logik für alte Eventversionen oder erwarten, dass alle Producer gleichzeitig migrieren. Historische Ereignisse können nicht mehr gelesen werden und Domänenhandler kennen technische Schemahistorie.
Erkennungsmerkmale
if(version==1) in Handlern; alte Felder gelangen in Fachcode; Migrationen verändern gespeicherte Events; unbekannte Versionen führen zu Nullwerten; jeder Consumer übersetzt anders.
Ziel und Nutzen
Ein Event Version Translator (Upcaster) übersetzt ältere Verträge schrittweise in die aktuelle interne Repräsentation. Fachhandler sehen nur eine Version und Schemaevolution bleibt an der Integrationsgrenze.
Sicherer Refactoring-Ablauf
Unterstützte Versionen und semantische Unterschiede dokumentieren.
Pro Versionsschritt eine kleine reine Übersetzung definieren.
Translator-Kette vor Deserialisierung ins aktuelle Modell setzen.
Unübersetzbare Änderungen explizit ablehnen oder migrieren.
Historische Fixtures und unbekannte Versionen testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei langlebigen Events, Event Stores und unabhängigen Deployments. Bei flüchtigen internen Nachrichten kann eine koordinierte Migration ausreichend sein.
Risiken und typische Fehlanwendungen
Ein Upcaster darf fehlende fachliche Information nicht erfinden. Lange Ketten werden Wartungslast; gelegentlich ist eine Offline-Migration sinnvoller. Übersetzung muss deterministisch bleiben.
Abgrenzung
Ein Data Mapper übersetzt Modelle derselben Zeit. Ein Version Translator übersetzt historische Vertragsstände; ein Anti-Corruption Layer schützt zusätzlich fremde Semantik.
Verwandte Refactorings und Muster
Upcaster, Schema Evolution, Event Envelope, Anti-Corruption Layer und Canonical Data Model.
Enterprise-Java-Einordnung
PolicyIssuedV1 mit annualPremiumCents wird zu V2 mit Money und currency übersetzt; eine definierte historische Standardwährung wird nur verwendet, wenn sie fachlich belegt ist.
Teststrategie
Fixture-Tests für jede Version und Translator-Kombination; Golden Master sichert aktuelle Ausgabe. Unbekannte oder beschädigte Versionen müssen deterministisch fehlschlagen.
Partition Keys werden ad hoc aus zufälligen Feldern gebildet; dadurch gehen Reihenfolge und Lastverteilung verloren.
Sichere Transformation
Führe eine typisierte Strategy ein, die den fachlichen Ordnungsbereich explizit bestimmt.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Partition Key Strategy
// Strategy Pattern: Reihenfolge und Verteilung werden bewusst pro Eventtyp entschieden.
public interface PartitionKeyStrategy<T> { String keyFor(T value);
record OrderEvent(String orderId,String customerId){}
final class ByOrder implements PartitionKeyStrategy<OrderEvent>{ public String keyFor(OrderEvent e){ if(e.orderId()==null||e.orderId().isBlank()) throw new IllegalArgumentException("orderId"); return "order:"+e.orderId(); } }
final class ByCustomer implements PartitionKeyStrategy<OrderEvent>{ public String keyFor(OrderEvent e){ if(e.customerId()==null||e.customerId().isBlank()) throw new IllegalArgumentException("customerId"); return "customer:"+e.customerId(); } }
}
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Der Schlüssel für Kafka-Partition, Shard oder Worker-Verteilung wird ad hoc aus verschiedenen Feldern gebildet. Reihenfolge, Lastverteilung und Datenlokalität ändern sich unbemerkt bei Schema- oder Codeänderungen.
Erkennungsmerkmale
key = customerId an einer Stelle und orderId an anderer; null-Fallbacks; zusammengesetzte Strings; Hot Partitions; fachlich zusammengehörige Events landen in unterschiedlicher Reihenfolge.
Ziel und Nutzen
Eine Partition Key Strategy macht das Ordnungs- und Verteilungsziel explizit. Sie erzeugt einen stabilen typisierten Schlüssel und kann versioniert, gemessen und getestet werden.
Sicherer Refactoring-Ablauf
Erforderliche Ordnungsdomäne und Parallelitätsziel definieren.
Kandidaten anhand Kardinalität und Hotspot-Risiko messen.
Strategy mit stabiler kanonischer Kodierung einführen.
Producer und Consumer auf denselben Vertrag umstellen.
Verteilung, Reihenfolge und Migrationspfad testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei partitionierten Streams, Shards und parallelen Workern. Bei nicht geordneten kleinen Datenmengen kann Round-Robin genügen.
Risiken und typische Fehlanwendungen
Ein Schlüssel mit geringer Kardinalität erzeugt Hotspots. Änderung des Algorithmus verschiebt Zuständigkeit und kann Reihenfolge brechen. Personenbezogene Rohdaten sollten nicht unnötig als Schlüssel exponiert werden.
Abgrenzung
Partition Key entscheidet Zuordnung und lokale Reihenfolge. Shard Key betrifft dauerhafte Datenverteilung; Routing Policy kann zusätzlich Zielregion oder Dienst wählen.
138. Extract Dead Letter Policy Dead Letter Channel / Policy
Ausgangsproblem
Fehlerhafte Nachrichten werden pauschal verworfen oder endlos wiederholt; Fehlerklassen bleiben ununterschieden.
Sichere Transformation
Extrahiere eine Policy, die Retry, Quarantäne und Verwerfen anhand von Versuchszahl und Fehlerklasse entscheidet.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Dead Letter Policy
// Dead Letter Channel Pattern: unzustellbare Nachrichten erhalten eine explizite Behandlung.
public final class DeadLetterPolicy {
public enum Action { RETRY, QUARANTINE, DISCARD }
public Action decide(int attempt,Throwable error){ if(attempt<1||error==null) throw new IllegalArgumentException(); if(error instanceof IllegalArgumentException) return Action.QUARANTINE; if(attempt>=3) return Action.QUARANTINE; return Action.RETRY; }
}
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fehlgeschlagene Nachrichten werden pauschal unendlich wiederholt, sofort verworfen oder in eine DLQ verschoben, ohne Fehlerklasse, Evidenz, Retention und Recovery festzulegen.
Erkennungsmerkmale
catch(Exception)->sendToDlq; keine Attempt-Zahl; technische und fachliche Fehler vermischt; sensible Payload ungefiltert; kein Replayprozess; DLQ wächst unbeobachtet.
Ziel und Nutzen
Eine Dead Letter Policy entscheidet anhand typisierter Fehler, Versuchszahl und Nachrichtsklasse über Retry, Quarantäne, Verwerfen oder manuelle Behandlung. Die Entscheidung ist auditierbar und testbar.
Sicherer Refactoring-Ablauf
Fehler in transient, permanent, fachlich und unbekannt klassifizieren.
DeliveryAttempt und FailureEvidence modellieren.
Policy mit klaren Entscheidungen definieren.
DLQ/Quarantäne-Adapter, Retention und Zugriffsschutz implementieren.
Replay- und manuelle Recovery-Prozesse testen.
Wann sinnvoll - und wann nicht
Sinnvoll für asynchrone Verarbeitung mit nicht automatisch lösbaren Fehlern. Eine DLQ darf kein Ersatz für saubere Fehlerklassifizierung sein.
Risiken und typische Fehlanwendungen
Unbegrenztes Retry erzeugt Stürme; sofortige DLQ bei transienten Fehlern verliert Verfügbarkeit. Payloads können Datenschutz- oder Geheimnisrisiken tragen. Replay ohne Idempotenz erzeugt Doppelwirkungen.
Abgrenzung
Dead Letter Channel ist der technische Zielkanal. Dead Letter Policy entscheidet, wann und mit welchen Metadaten eine Nachricht dorthin gelangt.
Verwandte Refactorings und Muster
Dead Letter Channel, Retry Schedule, Poison Message Quarantine, Delivery Attempt und Inbox.
Enterprise-Java-Einordnung
Unbekannte Tarifversion wird nach einem Versuch quarantined; Provider-Timeout folgt Retry Schedule. Die DLQ speichert EventId, SchemaVersion, ErrorCode und redigierte Evidenz.
Teststrategie
Decision-Table-Tests decken Fehlerklassen und Versuchszahlen ab. Integrationstests prüfen DLQ-Metadaten, Zugriffsschutz und kontrolliertes Replay.
139. Introduce Delivery Attempt Attempt Value Object
Ausgangsproblem
Versuchszahl, Erstzustellung und nächster Termin werden als lose Parameter weitergereicht und inkonsistent erhöht.
Sichere Transformation
Führe ein unveränderliches DeliveryAttempt-Objekt mit validiertem Übergang und Backoff-Berechnung ein.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Delivery Attempt
// Value Object: Zustellversuche und Backoff werden als konsistenter Zustand modelliert.
public record DeliveryAttempt(int number,Instant firstSeen,Instant nextAt){
public DeliveryAttempt { if(number<1||firstSeen==null||nextAt==null||nextAt.isBefore(firstSeen)) throw new IllegalArgumentException(); }
public static DeliveryAttempt first(Instant now){ return new DeliveryAttempt(1,now,now); }
public DeliveryAttempt failedAt(Instant now){ long seconds=Math.min(300L,1L<<Math.min(number,8)); return new DeliveryAttempt(number+1,firstSeen,now.plusSeconds(seconds)); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import org.junit.jupiter.api.Test;
class DeliveryAttemptTest { @Test void failure_creates_next_immutable_attempt(){ var a=DeliveryAttempt.first(Instant.EPOCH); var b=a.failedAt(Instant.EPOCH); assertEquals(1,a.number()); assertEquals(2,b.number()); assertTrue(b.nextAt().isAfter(Instant.EPOCH)); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Versuchszahl, Zeitpunkt, letzter Fehler und nächste Ausführung werden als lose Header oder primitive Parameter geführt. Retry-, DLQ- und Monitoringlogik interpretieren diese Informationen unterschiedlich.
Erkennungsmerkmale
Header wie x-retry-count; Integer ohne Ursprung; Zeitstempel werden überschrieben; Fehlertext statt Fehlerklasse; jeder Consumer berechnet nextAttempt neu.
Ziel und Nutzen
Ein Delivery Attempt Value Object bündelt unveränderlich den aktuellen Zustellversuch: Nummer, startedAt, vorherige Fehlerkategorie, nextEligibleAt und stabile MessageId. Policies erhalten einen klaren Vertrag.
Sicherer Refactoring-Ablauf
Vorhandene Retry-Metadaten inventarisieren.
Attempt-Invarianten und Zählsemantik festlegen.
Value Object mit next(failure,schedule) einführen.
Header-/Brokerdaten am Adapter übersetzen.
Retry- und Dead-Letter-Policies auf den Typ umstellen.
Wann sinnvoll - und wann nicht
Sinnvoll sobald Zustellung mehrfach versucht wird oder verschiedene Komponenten Retry-Metadaten austauschen. Für einen einzelnen synchronen Aufruf reicht ein lokaler Versuchszähler.
Risiken und typische Fehlanwendungen
Off-by-one-Semantik führt zu falschen Limits. Mutable Attempts zerstören Auditverlauf. Fehlerdetails müssen redigiert und Größen begrenzt werden; Broker-Redelivery und fachlicher Attempt dürfen nicht doppelt zählen.
Abgrenzung
Delivery Attempt beschreibt einen konkreten Versuch. Retry Policy entscheidet, ob und wann ein weiterer Versuch erlaubt ist.
Verwandte Refactorings und Muster
Delivery Attempt, Retry Schedule, Dead Letter Policy, Event Envelope und Observability Context.
Enterprise-Java-Einordnung
PaymentWebhookAttempt trägt attemptNo=3, firstSeenAt, lastFailure=PROVIDER_TIMEOUT und nextEligibleAt. Das System kann Support und Metriken konsistent informieren.
Teststrategie
Value-Object-Tests prüfen Invarianten und next-Berechnung. Contract-Tests sichern Header-Mapping; Decision-Table-Tests kombinieren Attempts mit Policies.
Trace-, Correlation- und Tenant-Informationen werden als lose Strings in Logs und Nachrichten kopiert.
Sichere Transformation
Extrahiere einen unveränderlichen ObservabilityContext und einen Decorator für konsistente Metadaten.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Observability Context
// Context Object + Decorator: technische Korrelation wird konsistent an Grenzen ergänzt.
public record ObservabilityContext(String traceId,String correlationId,String tenantId){
public ObservabilityContext { if(traceId==null||traceId.isBlank()||correlationId==null||correlationId.isBlank()||tenantId==null||tenantId.isBlank()) throw new IllegalArgumentException(); }
public Map<String,String> decorate(Map<String,String> metadata){ var result=new LinkedHashMap<>(metadata); result.put("traceId",traceId); result.put("correlationId",correlationId); result.put("tenantId",tenantId); return Map.copyOf(result); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.Map; import org.junit.jupiter.api.Test;
class ObservabilityContextTest { @Test void metadata_is_decorated_without_mutating_input(){ var input=Map.of("event","OrderPlaced"); var c=new ObservabilityContext("t1","c1","tenant"); var out=c.decorate(input); assertEquals("t1",out.get("traceId")); assertEquals(1,input.size()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn die Regel an mehreren Aufrufern auftaucht oder eine eigene Fehler-, Test- oder Lebenszyklussemantik besitzt. Für einen einmaligen lokalen Ausdruck wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Logging, Tracing und Metriken lesen Korrelation, TraceId, Tenant und fachliche Identitäten aus globalen MDCs oder bauen Labels ad hoc. Bei asynchronen Übergängen gehen Bezüge verloren und sensible Daten landen unkontrolliert in Telemetrie.
Erkennungsmerkmale
MDC.put in Fachservices; unterschiedliche Labelnamen; High-Cardinality-IDs als Metrics-Tag; Tracekontext fehlt in Commands/Events; Threadwechsel verliert Korrelation.
Ziel und Nutzen
Ein Observability Context definiert die erlaubten, typisierten Telemetriebezüge und wird an Grenzen explizit propagiert. Decorators setzen daraus Logging-, Tracing- und Metrics-Kontext, ohne Fachcode an konkrete Bibliotheken zu koppeln.
Sicherer Refactoring-Ablauf
Benötigte Korrelationen und Datenschutzregeln festlegen.
Kleinen unveränderlichen Kontext mit erlaubten Feldern definieren.
Framework- und Brokerheader am Adapter übersetzen.
Ports/Decorators für Logs, Traces und Metriken einführen.
Asynchrone Propagation und Cardinality-Budgets testen.
Wann sinnvoll - und wann nicht
Sinnvoll in verteilten und asynchronen Systemen. Nicht jede fachliche ID gehört in jeden Telemetriekanal; Metriken benötigen besonders niedrige Kardinalität.
Risiken und typische Fehlanwendungen
Ein God Context und unkontrollierte Baggage erhöhen Kopplung und Kosten. Personenbezogene Daten oder Tokens dürfen nicht propagiert werden. ThreadLocal allein ist bei Virtual Threads/Executors nicht automatisch ein stabiler Vertrag.
Abgrenzung
Correlation Context verbindet Vorgänge. Observability Context begrenzt und übersetzt diese Informationen für Logs, Traces und Metriken; Request Context kann weitere fachliche Laufzeitwerte enthalten.
Verwandte Refactorings und Muster
Observability Context, Correlation Identifier, Decorator, Context Propagation und Telemetry Port.
Enterprise-Java-Einordnung
Ein IntegrationEvent enthält correlationId und causationId. Der Consumer erzeugt ObservabilityContext; ein Handler-Decorator startet Span, setzt strukturierte Logfelder und verwendet nur operationName als Metrics-Label.
Teststrategie
Tests prüfen Header-Mapping, fehlende/ungültige IDs und Redaction. Async-Tests sichern Propagation; Cardinality-Tests verhindern dynamische IDs in Metriken.
Externe Nachrichtenformate sickern als untypisierte Maps in die Fachlogik.
Sichere Transformation
Extrahiere einen Translator, der den externen Vertrag einmalig in ein typisiertes internes Kommando übersetzt.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Map;
// Refactoring: Extract Message Translator
// Pattern: Message Translator / Adapter - externer Vertrag wird an der Systemgrenze normalisiert.
public final class MessageTranslator {
public record OrderCommand(String orderId, long amountCents) {
public OrderCommand { if(orderId==null||orderId.isBlank()) throw new IllegalArgumentException("orderId"); if(amountCents<0) throw new IllegalArgumentException("amountCents"); }
}
public OrderCommand translate(Map<String,String> message){
if(message==null) throw new IllegalArgumentException("message");
return new OrderCommand(message.get("order_id"),Long.parseLong(message.get("amount_cents")));
}
}
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Integrationsadapter kennt zugleich das externe Nachrichtenformat, interne Fachtypen und zahlreiche feldweise Sonderregeln. Änderungen an Partnerverträgen greifen dadurch bis in Anwendungsservices und Domänenlogik durch.
Erkennungsmerkmale
Controller oder Consumer lesen fremde Feldnamen direkt; Datums-, Währungs- und Statuskonvertierungen sind verstreut; dieselbe Transformation wird in mehreren Flows kopiert; Fachcode kennt Transport-DTOs.
Ziel und Nutzen
Ein Message Translator bildet genau eine externe Repräsentation auf ein internes, fachlich verständliches Modell ab. Formatwissen und Semantikübersetzung liegen an einer klaren Integrationsgrenze und sind separat testbar.
Sicherer Refactoring-Ablauf
Eingangsformat und internes Zielmodell getrennt inventarisieren.
Feldabbildungen, Normalisierungen und fachliche Defaults explizit dokumentieren.
Translator als zustandslosen Adapter mit typisierter Signatur einführen.
Aufrufer schrittweise vom Fremdformat auf das interne Modell umstellen.
Vertrags-, Grenzwert- und Fehlerszenarien mit festen Fixtures absichern.
Wann sinnvoll - und wann nicht
Sinnvoll bei fremden oder versionierten Verträgen und mehreren Integrationspartnern. Für identische interne Typen ohne semantische Übersetzung genügt oft ein einfacher Konstruktor oder Mapper.
Risiken und typische Fehlanwendungen
Ein Translator darf fehlende Fachinformation nicht stillschweigend erfinden. Zu große universelle Mapper werden selbst zum Integrationsmonolithen; Fehler müssen mit Feldbezug und Vertragsversion nachvollziehbar bleiben.
Abgrenzung
Ein Data Mapper übersetzt häufig zwischen Domäne und Persistenz. Ein Message Translator schützt die Integrationsgrenze; ein Anti-Corruption Layer umfasst zusätzlich mehrere Übersetzer und Regeln.
Verwandte Refactorings und Muster
Adapter, Anti-Corruption Layer, Canonical Data Model, Event Version Translator und Data Mapper.
Enterprise-Java-Einordnung
Ein PartnerOrderTranslator wandelt PartnerOrderV3 in CreateOrderCommand um, normalisiert ISO-Währung, Statuscode und optionale Lieferdaten und hält das Domänenmodell frei vom Partnerschema.
Teststrategie
Contract-Tests verwenden reale anonymisierte Payloads. Tabellengetriebene Tests prüfen Codes, fehlende Pflichtwerte und Zeitzonen; Round-Trip-Tests sind nur sinnvoll, wenn die Abbildung verlustfrei sein soll.
142. Introduce Canonical Message Canonical Data Model
Ausgangsproblem
Jeder Integrationsadapter verwendet eigene Headernamen und Payload-Metadaten.
Sichere Transformation
Führe eine unveränderliche kanonische Nachricht mit stabiler ID, Typ, Version, Payload und Metadaten ein.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Introduce Canonical Message
// Pattern: Canonical Data Model - Integrationen sprechen intern einen stabilen Nachrichtenvertrag.
public record CanonicalMessage(UUID id,String type,int version,String payload,Instant occurredAt,Map<String,String> metadata){
public CanonicalMessage { Objects.requireNonNull(id); if(type==null||type.isBlank()) throw new IllegalArgumentException("type"); if(version<1) throw new IllegalArgumentException("version"); Objects.requireNonNull(payload); Objects.requireNonNull(occurredAt); metadata=Map.copyOf(metadata); }
public CanonicalMessage withMetadata(String key,String value){ var next=new LinkedHashMap<>(metadata); next.put(key,value); return new CanonicalMessage(id,type,version,payload,occurredAt,next); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import java.util.*; import org.junit.jupiter.api.Test;
class CanonicalMessageTest { @Test void metadata_update_returns_new_message(){ var a=new CanonicalMessage(UUID.randomUUID(),"OrderPlaced",1,"{}",Instant.EPOCH,Map.of()); var b=a.withMetadata("tenant","t1"); assertTrue(a.metadata().isEmpty()); assertEquals("t1",b.metadata().get("tenant")); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Bei vielen Punkt-zu-Punkt-Integrationen übersetzt jedes System jedes fremde Format direkt. Die Zahl der Mapper wächst quadratisch und dieselben Begriffe erhalten je Schnittstelle andere Bedeutungen.
Erkennungsmerkmale
Viele paarweise Translatoren; unterschiedliche Bezeichnungen für dieselbe fachliche Information; Routing hängt an Partnerfeldern; neue Teilnehmer erfordern Änderungen in zahlreichen bestehenden Flows.
Ziel und Nutzen
Eine Canonical Message stellt für einen klar begrenzten Integrationsbereich eine gemeinsame Nachrichtenform bereit. Teilnehmer übersetzen nur zwischen ihrem Vertrag und diesem kanonischen Modell.
Sicherer Refactoring-Ablauf
Gemeinsamen fachlichen Kern und bewusst nicht gemeinsame Details bestimmen.
Kanonisches Schema klein, versioniert und unabhängig von einzelnen Partnern entwerfen.
Ein- und Ausgangstranslator je Teilnehmer einführen.
Routing und Verarbeitung auf kanonische Felder umstellen.
Governance, Kompatibilität und Änderungsprozess festlegen.
Wann sinnvoll - und wann nicht
Sinnvoll in stabilen Integrationsdomänen mit vielen Teilnehmern. Ein unternehmensweites Universalmodell ist meist zu groß; bei zwei einfachen Systemen erzeugt es unnötige Indirektion.
Risiken und typische Fehlanwendungen
Das Modell kann zum zentralen Engpass oder kleinsten gemeinsamen Nenner werden. Partnerbesonderheiten dürfen nicht als beliebige Extension Map hineinsickern. Versionierung und Ownership müssen eindeutig sein.
Abgrenzung
Canonical Data Model reduziert viele direkte Abbildungen. Published Language definiert einen bewusst veröffentlichten Vertrag; ein Anti-Corruption Layer schützt eine einzelne Domäne vor fremder Semantik.
Verwandte Refactorings und Muster
Canonical Data Model, Message Translator, Normalizer, Published Language und Schema Evolution.
Enterprise-Java-Einordnung
Ein kanonisches CustomerChangeEvent enthält CustomerId, ChangeType, EffectiveAt und ContactSnapshot. CRM, Billing und Support besitzen je einen Translator, aber keine gegenseitigen DTO-Abhängigkeiten.
Teststrategie
Schema-Contract-Tests prüfen Pflichtfelder und Versionen. Jeder Translator besitzt Golden-Master-Fixtures; Kompatibilitätstests sichern alte Consumer gegen additive Änderungen.
Routingregeln wachsen als zentrale if-else-Kette und kennen alle Zielsysteme.
Sichere Transformation
Extrahiere einen Router mit registrierten, typisierten Routen und explizitem Fehlerfall für unbekannte Typen.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*; import java.util.function.Consumer;
// Refactoring: Extract Message Router
// Pattern: Message Router + Strategy Registry - Nachrichtentypen wählen registrierte Handler.
public final class MessageRouter {
private final Map<String,Consumer<CanonicalMessage>> routes=new HashMap<>();
public MessageRouter register(String type,Consumer<CanonicalMessage> handler){ routes.put(Objects.requireNonNull(type),Objects.requireNonNull(handler)); return this; }
public void route(CanonicalMessage message){ var handler=routes.get(message.type()); if(handler==null) throw new IllegalStateException("No route for "+message.type()); handler.accept(message); }
public Set<String> routeTypes(){ return Set.copyOf(routes.keySet()); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import java.util.*; import java.util.concurrent.atomic.AtomicReference; import org.junit.jupiter.api.Test;
class MessageRouterTest { @Test void routes_by_type(){ var seen=new AtomicReference<String>(); var r=new MessageRouter().register("OrderPlaced",m->seen.set(m.type())); r.route(new CanonicalMessage(UUID.randomUUID(),"OrderPlaced",1,"{}",Instant.EPOCH,Map.of())); assertEquals("OrderPlaced",seen.get()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Weiterleitungsentscheidungen sind als wachsende if/switch-Ketten in Consumer- oder Controllercode eingebaut. Transport, fachliche Klassifizierung und Zielauswahl sind dadurch eng gekoppelt.
Erkennungsmerkmale
Viele Bedingungen auf Typ, Mandant oder Priorität; Zielnamen als Strings; neue Route ändert zentrale Methode; Defaultzweig verschluckt unbekannte Nachrichten; Routing ist schwer isoliert testbar.
Ziel und Nutzen
Ein Message Router kapselt Klassifizierung und Zielwahl. Er liefert eine typisierte Route oder delegiert über eine Strategy Registry an den passenden Handler.
Sicherer Refactoring-Ablauf
Vorhandene Routingkriterien und Prioritäten explizit machen.
Message-Metadaten in einen stabilen RoutingContext überführen.
Route und unbekannte Entscheidung als Typ modellieren.
Bedingungen in kleine Regeln oder registrierte Strategien verschieben.
Überlappende, fehlende und neue Routen mit Entscheidungstabellen testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei mehreren dynamischen Zielen und klaren Routingkriterien. Bei einer festen linearen Pipeline genügt direkte Verdrahtung.
Risiken und typische Fehlanwendungen
Ein Router darf keine umfangreiche Fachverarbeitung übernehmen. Reihenfolge überlappender Regeln muss deterministisch sein; ein stiller Default erzeugt Datenverlust. Dynamische Registries benötigen Ownership und Sichtbarkeit.
Abgrenzung
Ein Content-Based Router entscheidet anhand des Inhalts. Ein Message Router ist der allgemeinere Baustein; ein Process Manager steuert zusätzlich mehrschrittige Abläufe und Zustand.
Verwandte Refactorings und Muster
Message Router, Content-Based Router, Strategy Registry, Recipient List und Process Manager.
Enterprise-Java-Einordnung
InboundSupportEventRouter wählt FraudHandler, BillingHandler oder GeneralSupportHandler anhand eines typisierten EventKind und TenantPolicy, nicht anhand unkontrollierter Headerstrings.
Teststrategie
Decision-Table-Tests prüfen jede Route, Priorität und Unknown-Fall. Contract-Tests stellen sicher, dass Zielnamen und Handlerregistrierung vollständig und eindeutig sind.
Große Payloads werden durch jede Queue kopiert und überschreiten Transportgrenzen.
Sichere Transformation
Lagere die Payload in einen ClaimCheckStore aus und transportiere nur einen unveränderlichen Schlüssel.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*; import java.util.concurrent.ConcurrentHashMap;
// Refactoring: Introduce Claim Check Store
// Pattern: Claim Check - große Payload bleibt außerhalb des Nachrichtenkanals.
public final class ClaimCheckStore {
public record ClaimKey(UUID value){ public ClaimKey { Objects.requireNonNull(value); } }
private final Map<ClaimKey,byte[]> payloads=new ConcurrentHashMap<>();
public ClaimKey put(byte[] payload){ Objects.requireNonNull(payload); var key=new ClaimKey(UUID.randomUUID()); payloads.put(key,payload.clone()); return key; }
public byte[] get(ClaimKey key){ var data=payloads.get(key); if(data==null) throw new NoSuchElementException(key.toString()); return data.clone(); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class ClaimCheckStoreTest { @Test void stores_defensive_copies(){ var s=new ClaimCheckStore(); byte[] p={1,2}; var k=s.put(p); p[0]=9; assertArrayEquals(new byte[]{1,2},s.get(k)); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Große Nachrichten transportieren Dokumente oder umfangreiche Payloads durch jeden Broker und Verarbeitungsschritt. Das erhöht Latenz, Kosten und Fehleranfälligkeit, obwohl viele Schritte nur Metadaten benötigen.
Erkennungsmerkmale
Brokerlimits werden erreicht; dieselben Binärdaten werden mehrfach kopiert; Retries übertragen erneut große Inhalte; Logs oder DLQs enthalten sensible Payloads; Consumer laden Daten unnötig.
Ziel und Nutzen
Ein Claim Check Store lagert den großen Inhalt in einen dafür geeigneten Speicher aus. Die Nachricht enthält nur einen unveränderlichen, autorisierten Verweis und die für den Ablauf nötigen Metadaten.
Sicherer Refactoring-Ablauf
Größe, Sensitivität und Lebensdauer der Payload analysieren.
Speicherport, ClaimCheck Value Object und Zugriffsschutz definieren.
Payload vor dem Versand atomar oder kompensierbar ablegen.
Consumer laden Inhalt nur bei Bedarf und validieren Integrität.
Retention, Löschung, Wiederholung und verwaiste Claims testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei großen oder sensiblen Anhängen und mehreren Routingstufen. Für kleine Nachrichten ist der zusätzliche Speicherzugriff meist unnötig.
Risiken und typische Fehlanwendungen
Ein nackter URL-String ist kein sicherer Claim Check. Zugriffsrechte, Ablauf, Verschlüsselung und Cleanup müssen stimmen. Nichtatomare Ablage kann Nachrichten ohne Inhalt oder verwaiste Daten erzeugen.
Abgrenzung
Claim Check reduziert Nachrichtengröße. Message Store archiviert Nachrichten; Blob Storage ist nur eine Technik. Ein Content Enricher lädt zusätzliche Daten anhand eines Verweises.
Verwandte Refactorings und Muster
Claim Check, Content Enricher, Message Store, Outbox und Retention Policy.
Enterprise-Java-Einordnung
Ein DocumentSubmittedEvent enthält DocumentClaimId, SHA-256, MediaType und Size. Der Virenscanner lädt das Dokument über DocumentStorePort, andere Consumer verwenden nur Metadaten.
Teststrategie
Integrationstests prüfen Ablage, Zugriff, Hashvalidierung und Ablauf. Recovery-Tests simulieren Fehler zwischen Store und Publish; Sicherheitstests prüfen Mandantentrennung.
Teilantworten werden mit globalen Listen und impliziter Vollständigkeitslogik gesammelt.
Sichere Transformation
Extrahiere einen Aggregator, der erwartete Teile, Duplikate und Abschlusszustand pro Correlation-ID kapselt.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Message Aggregator
// Pattern: Aggregator - Teilnachrichten werden pro Correlation-ID deterministisch zusammengeführt.
public final class MessageAggregator {
public record Part(String correlationId,int index,String value){ public Part { if(correlationId==null||correlationId.isBlank()||index<0) throw new IllegalArgumentException(); Objects.requireNonNull(value); } }
private final int expected; private final Map<String,Map<Integer,String>> groups=new HashMap<>();
public MessageAggregator(int expected){ if(expected<1) throw new IllegalArgumentException("expected"); this.expected=expected; }
public Optional<List<String>> add(Part part){ var group=groups.computeIfAbsent(part.correlationId(),k->new HashMap<>()); group.putIfAbsent(part.index(),part.value()); if(group.size()!=expected) return Optional.empty(); return Optional.of(group.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(Map.Entry::getValue).toList()); }
}
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Zusammengehörige Teilnachrichten werden ad hoc in Maps gesammelt oder sofort weitergegeben. Es fehlt eine explizite Regel, wann eine Gruppe vollständig ist und welches Ergebnis daraus entsteht.
Erkennungsmerkmale
Globale mutable Maps; Zähler ohne erwartete Gesamtzahl; Timeouts als Scheduler-Hack; doppelte Teile verändern Ergebnisse; Speicher wird nie bereinigt; Korrelation ist uneinheitlich.
Ziel und Nutzen
Ein Message Aggregator gruppiert Nachrichten anhand eines Correlation Identifier, hält einen typisierten Aggregation State und entscheidet mit einer Completion Policy über Abschluss oder Ablauf.
Sicherer Refactoring-Ablauf
Korrelationsschlüssel und erwartete Teilmenge definieren.
Aggregation State unveränderlich oder atomar aktualisierbar modellieren.
Completion-, Timeout- und Duplikatregeln explizit machen.
State Store und Ergebnisbildung trennen.
Out-of-order-, Duplicate-, Timeout- und Recovery-Szenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei Splitter-Ergebnissen, Scatter-Gather und mehrteiligen Geschäftsantworten. Wenn Teile unabhängig verarbeitet werden können, ist Aggregation unnötig.
Risiken und typische Fehlanwendungen
Unbegrenzter Zustand führt zu Speicherlecks. Genau-einmal-Annahmen sind gefährlich; verspätete Teile und Wiederanlauf müssen definiert sein. Aggregation darf keine versteckte Langzeit-Saga werden.
Abgrenzung
Ein Aggregator bildet aus mehreren Nachrichten ein Ergebnis. Ein Resequencer ordnet, ohne zwingend zusammenzufassen; ein Process Manager steuert einen fachlichen Prozess.
Verwandte Refactorings und Muster
Aggregator, Correlation Identifier, Completion Policy, Splitter und Scatter-Gather.
Enterprise-Java-Einordnung
Ein PricingQuoteAggregator sammelt BasePrice, TaxQuote und ShippingQuote pro QuoteId und erzeugt erst bei vollständiger oder fachlich zulässiger Teilmenge ein QuoteResult.
Teststrategie
State-Machine-Tests prüfen Reihenfolgen und Duplikate. Fake Clock ermöglicht Timeouttests; Persistenz- und Restarttests sichern Recovery.
Parallel verarbeitete Nachrichten treffen ungeordnet ein und werden zu früh weitergegeben.
Sichere Transformation
Führe einen Resequencer ein, der pro Stream nur die nächste erwartete Sequenz freigibt und Lücken puffert.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Message Resequencer
// Pattern: Resequencer - ungeordnete Nachrichten werden pro Stream in Sequenz freigegeben.
public final class MessageResequencer {
public record Sequenced(long sequence,String payload){ public Sequenced { if(sequence<0) throw new IllegalArgumentException(); Objects.requireNonNull(payload); } }
private long next; private final NavigableMap<Long,String> buffer=new TreeMap<>();
public MessageResequencer(long firstSequence){ if(firstSequence<0) throw new IllegalArgumentException(); next=firstSequence; }
public List<Sequenced> accept(Sequenced message){ buffer.putIfAbsent(message.sequence(),message.payload()); var ready=new ArrayList<Sequenced>(); while(buffer.containsKey(next)){ ready.add(new Sequenced(next,buffer.remove(next))); next++; } return List.copyOf(ready); }
}
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Parallel verarbeitete oder unterschiedlich verzögerte Nachrichten treffen in falscher Reihenfolge ein, obwohl der Empfänger eine definierte Sequenz benötigt.
Ein Resequencer puffert Nachrichten pro Korrelationsschlüssel, erkennt Sequenznummern und gibt nur die nächste erwartete Nachricht frei. Gap- und Timeout-Policy machen fehlende Elemente explizit.
Sicherer Refactoring-Ablauf
Ordnungsdomäne und Sequenzsemantik festlegen.
SequenceNumber und CorrelationId typisieren.
Persistenten oder begrenzten Puffer einführen.
Gap-, Duplicate- und Ablaufentscheidung definieren.
Out-of-order-, Verlust- und Restart-Szenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll, wenn fachliche Korrektheit echte Reihenfolge verlangt und der Transport sie nicht garantiert. Bei kommutativen Updates sollte das Modell lieber reihenfolgeunabhängig werden.
Risiken und typische Fehlanwendungen
Unbegrenztes Warten blockiert Gruppen dauerhaft. Globale Reihenfolge skaliert schlecht; pro Aggregate ist meist sinnvoller. Sequenznummern müssen stabil erzeugt werden.
Abgrenzung
Resequencer stellt Reihenfolge her. Aggregator fasst Teile zusammen; Optimistic Versioning kann veraltete Updates ablehnen, ohne sie umzuschichten.
Verwandte Refactorings und Muster
Resequencer, Message Sequence, Consumer Offset, Aggregate Version und Gap Policy.
Enterprise-Java-Einordnung
Ein AccountProjectionResequencer verarbeitet AccountEvent(sequence=41) erst, wenn 40 vorliegt; nach definierter Frist wird eine Recovery-Anfrage statt stiller Fortschreibung erzeugt.
147. Extract Polling Consumer Polling Consumer / Port
Ausgangsproblem
Polling, Cursor-Verwaltung und Verarbeitung liegen in einer Endlosschleife ohne testbare Grenze.
Sichere Transformation
Extrahiere Source- und Handler-Ports und mache einen einzelnen Poll-Zyklus deterministisch ausführbar.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Polling Consumer
// Pattern: Polling Consumer - ein kontrollierter Zyklus trennt Quelle, Verarbeitung und Cursor.
public final class PollingConsumer {
public interface Source { List<String> poll(String cursor,int limit); }
public interface Handler { void handle(String message); }
private final Source source; private final Handler handler; private String cursor="0";
public PollingConsumer(Source source,Handler handler){ this.source=Objects.requireNonNull(source); this.handler=Objects.requireNonNull(handler); }
public int pollOnce(int limit){ var messages=source.poll(cursor,limit); messages.forEach(handler::handle); cursor=Integer.toString(Integer.parseInt(cursor)+messages.size()); return messages.size(); }
public String cursor(){ return cursor; }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.*; import org.junit.jupiter.api.Test;
class PollingConsumerTest { @Test void advances_cursor_after_handling(){ var seen=new ArrayList<String>(); var c=new PollingConsumer((cursor,limit)->List.of("a","b"),seen::add); assertEquals(2,c.pollOnce(10)); assertEquals("2",c.cursor()); assertEquals(List.of("a","b"),seen); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Zeitsteuerung, Abruf, Cursorverwaltung, Fehlerbehandlung und Fachverarbeitung stecken in einer Scheduler-Methode. Dadurch sind Pollingintervall, Backoff und Verarbeitung nicht getrennt testbar.
Erkennungsmerkmale
@Scheduled-Methode mit Datenbankzugriff und Businesslogik; Cursor als statisches Feld; feste Sleeps; Batchgröße und Fehlerreaktion verstreut; parallele Instanzen lesen dieselben Datensätze.
Ziel und Nutzen
Ein Polling Consumer kapselt ausschließlich den wiederholten Abruf über einen Source Port und übergibt gefundene Nachrichten an einen separaten Handler. Cursor, Lease und Polling Policy werden explizit.
Sicherer Refactoring-Ablauf
Quelle, Cursor und Sichtbarkeitssemantik definieren.
SourcePort und PollBatch Result Type einführen.
Polling Policy für Intervall, leere Ergebnisse und Fehler extrahieren.
Fachverarbeitung in idempotenten Handler verschieben.
Mehrinstanz-, Restart- und Empty-Poll-Szenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei Quellen ohne Pushmechanismus, Tabellen-Outbox oder Legacy-APIs. Bei nativer Eventzustellung ist Polling oft nur zusätzlicher Ballast.
Risiken und typische Fehlanwendungen
Zu aggressives Polling belastet Quelle; zu langsames erhöht Latenz. Cursor darf erst nach sicherer Verarbeitung fortgeschrieben werden. Mehrere Instanzen benötigen Claim/Lease oder partitionierte Zuständigkeit.
Abgrenzung
Polling Consumer zieht aktiv Daten. Event-Driven Consumer reagiert auf Push; Outbox Poller ist eine konkrete Anwendung mit Transaktionsanforderungen.
Verwandte Refactorings und Muster
Polling Consumer, Outbox Poller, Consumer Offset, Lease und Idempotent Receiver.
Enterprise-Java-Einordnung
OutboxPollingConsumer liest maximal 100 PendingOutboxRecord über OutboxSourcePort, publiziert über EventPublisher und bestätigt erst nach erfolgreicher Zustellung.
Teststrategie
Fake Scheduler und Source Fake prüfen leere, teilweise und fehlerhafte Batches. Restarttests sichern Cursor; Paralleltests prüfen Lease und Doppelverarbeitung.
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Mehrere Worker sollen dieselbe Queue oder Tabelle parallel bearbeiten, aber Zuständigkeit und Wiederfreigabe sind implizit. Doppelverarbeitung oder dauerhaft blockierte Arbeit entsteht bei Abstürzen.
Erkennungsmerkmale
Statusfeld PROCESSING ohne Ablaufzeit; Select-then-update-Rennen; WorkerId als String; keine Heartbeats; verwaiste Jobs benötigen manuelle Korrektur.
Ziel und Nutzen
Ein Lease Object beschreibt zeitlich begrenzten exklusiven Besitz. Atomarer Claim, Ablauf und optionale Verlängerung erlauben parallele Consumer mit kontrollierter Wiederübernahme.
Sicherer Refactoring-Ablauf
Arbeitseinheit und gewünschte Exklusivität bestimmen.
LeaseId, Owner, acquiredAt und expiresAt typisieren.
Atomaren Claim im Repository implementieren.
Renew-, Complete- und Expire-Regeln definieren.
Crash-, Clock- und Mehrworker-Szenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei tabellenbasierten Work Queues oder Ressourcen ohne Broker-Consumer-Gruppen. Wenn der Broker Exklusivität zuverlässig liefert, ist ein zusätzliches Lease oft unnötig.
Risiken und typische Fehlanwendungen
Lease ist keine Garantie gegen Seiteneffekte nach Ablauf; Handler müssen idempotent bleiben. Lange Leases verzögern Recovery, kurze erzeugen unnötige Renewals. Uhr- und Transaktionssemantik sind kritisch.
Abgrenzung
Competing Consumers verteilen Last. Lease regelt zeitlichen Besitz; Distributed Lock schützt meist eine Ressource statt einzelner Arbeitseinheiten.
Verwandte Refactorings und Muster
Competing Consumers, Lease, Claim Check, Inbox, Idempotent Receiver und Work Queue.
Enterprise-Java-Einordnung
MigrationWorkerRepository.claimNext(workerId, now, ttl) liefert einen MigrationLease. Nach erfolgreichem Schritt wird complete(leaseId) atomar bestätigt.
Teststrategie
Paralleltests prüfen eindeutigen Claim. Fake Clock simuliert Ablauf und Erneuerung; Crash-Recovery-Tests prüfen erneute Zustellung ohne doppelte Wirkung.
Diagnosekopien werden ad hoc in Geschäftslogik eingefügt und können die Hauptverarbeitung blockieren.
Sichere Transformation
Extrahiere einen Wire-Tap-Decorator, der Nachrichten unverändert weiterleitet und eine getrennte Beobachtungskopie erzeugt.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Objects; import java.util.function.Consumer;
// Refactoring: Extract Wire Tap
// Pattern: Wire Tap + Decorator - Beobachtung wird ohne Änderung der Hauptnachricht ergänzt.
public final class WireTap implements Consumer<CanonicalMessage> {
private final Consumer<CanonicalMessage> downstream; private final Consumer<CanonicalMessage> tap;
public WireTap(Consumer<CanonicalMessage> downstream,Consumer<CanonicalMessage> tap){ this.downstream=Objects.requireNonNull(downstream); this.tap=Objects.requireNonNull(tap); }
@Override public void accept(CanonicalMessage message){ tap.accept(message); downstream.accept(message); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import java.util.*; import org.junit.jupiter.api.Test;
class WireTapTest { @Test void observes_and_forwards_same_message(){ var calls=new ArrayList<String>(); var w=new WireTap(m->calls.add("down:"+m.type()),m->calls.add("tap:"+m.type())); w.accept(new CanonicalMessage(UUID.randomUUID(),"X",1,"{}",Instant.EPOCH,Map.of())); assertEquals(List.of("tap:X","down:X"),calls); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Logging, Audit oder Diagnose wird direkt in die Hauptverarbeitung eingebaut und verändert deren Fehler- und Latenzverhalten. Beobachtung ist dadurch mit Fachlogik gekoppelt.
Erkennungsmerkmale
Handler ruft zusätzlich AuditService; Diagnosefehler brechen Businessflow; komplette Payload wird geloggt; Beobachtung ist in vielen Handlern kopiert; Ein-/Ausschalten erfordert Fachcodeänderung.
Ziel und Nutzen
Ein Wire Tap erzeugt an einer klaren Kanalgrenze eine kontrollierte Kopie ausgewählter Nachrichtendaten für Beobachtung, Audit oder Analyse, ohne den Hauptfluss fachlich zu verändern.
Sicherer Refactoring-Ablauf
Beobachtungszweck und erlaubte Daten festlegen.
Redaktion und Metadatenprojektion vor der Kopie definieren.
Tap als Decorator oder Kanalinterceptor einführen.
Fehler- und Backpressure-Verhalten unabhängig vom Hauptfluss bestimmen.
Datenschutz-, Ausfall- und Lastszenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll für Diagnose, Audit und nichtkritische Analyse. Kritische fachliche Zustellung darf nicht als unverbindlicher Wire Tap modelliert werden.
Risiken und typische Fehlanwendungen
Unkontrollierte Payloadkopien verletzen Datenschutz und erhöhen Kosten. Ein synchroner Tap kann Hauptlatenz verschlechtern; ein still fehlernder Tap kann Auditversprechen brechen. Zweck und Garantie müssen klar sein.
Abgrenzung
Wire Tap kopiert Beobachtungsdaten. Decorator erweitert einen Aufruf; Domain Event löst fachlich relevante Folgewirkung aus.
Verwandte Refactorings und Muster
Wire Tap, Decorator, Audit Port, Observability Context und Data Masking Policy.
Enterprise-Java-Einordnung
Ein PaymentEventChannelDecorator sendet redigierte Metadaten an SecurityAnalytics, während die originale Nachricht unverändert an den PaymentHandler geht.
Antworten werden anhand von Thread, Reihenfolge oder globalem Zustand einem Request zugeordnet.
Sichere Transformation
Führe einen expliziten Correlation-Store ein, der Request-ID und erwarteten Antworttyp sicher zusammenführt.
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*; import java.util.concurrent.ConcurrentHashMap;
// Refactoring: Introduce Request-Reply Correlation
// Pattern: Request-Reply + Correlation Identifier - Antworten werden explizit dem ursprünglichen Request zugeordnet.
public final class RequestReplyCorrelation {
public record Pending(UUID correlationId,String expectedType){ public Pending { Objects.requireNonNull(correlationId); if(expectedType==null||expectedType.isBlank()) throw new IllegalArgumentException(); } }
private final Map<UUID,Pending> pending=new ConcurrentHashMap<>();
public Pending register(String expectedType){ var p=new Pending(UUID.randomUUID(),expectedType); pending.put(p.correlationId(),p); return p; }
public boolean complete(UUID correlationId,String responseType){ var p=pending.get(correlationId); if(p==null||!p.expectedType().equals(responseType)) return false; return pending.remove(correlationId,p); }
public int pendingCount(){ return pending.size(); }
}
JUnit-Test
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class RequestReplyCorrelationTest { @Test void completes_only_matching_reply(){ var c=new RequestReplyCorrelation(); var p=c.register("PriceReply"); assertFalse(c.complete(p.correlationId(),"Other")); assertTrue(c.complete(p.correlationId(),"PriceReply")); assertEquals(0,c.pendingCount()); } }
Einsatzgrenze: Das Refactoring ist sinnvoll, wenn Integrationszustand, Reihenfolge oder Zustellung eine eigene Semantik und isolierte Tests benötigen. Für eine rein lokale, synchrone Einmaloperation wäre die zusätzliche Infrastruktur unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Asynchrone Antworten werden anhand von Threadzustand, Reihenfolge oder frei gebildeten Strings einem Request zugeordnet. Bei Parallelität und Wiederholung entstehen falsche Zuordnungen.
Erkennungsmerkmale
pending map mit String-Keys; Antwort ohne CorrelationId; Timeout entfernt falschen Eintrag; gleiche BusinessId für mehrere Versuche; Restart verliert wartende Zuordnung.
Ziel und Nutzen
Ein Correlation Identifier Value Object verbindet genau einen Request mit seinen Antworten. Ein Request Registry oder PendingRequest Store verwaltet Lebenszyklus, Timeout und Mehrfachantworten.
Sicherer Refactoring-Ablauf
Request-Identität und Antwortkardinalität definieren.
CorrelationId unabhängig von fachlichen IDs erzeugen.
Identifier in Envelope und Antwortvertrag aufnehmen.
PendingRequestStore mit Ablauf und atomarer Vervollständigung einführen.
Parallel-, Duplicate-, Timeout- und Restart-Szenarien testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei asynchronem Request-Reply und Scatter-Gather. Bei synchronem Methodenaufruf liefert der Call Stack bereits Korrelation.
Risiken und typische Fehlanwendungen
CorrelationId darf nicht mit Authentisierung oder IdempotencyKey verwechselt werden. Unbegrenzte Pending Maps lecken Speicher; verspätete Antworten brauchen eine definierte Behandlung.
Wiederholte Zustellungen lösen dieselbe Fachaktion mehrfach aus.
Sichere Transformation
Kapsle die Deduplizierung in einem Receiver, der eine Message-ID atomar reserviert und nur neue Nachrichten verarbeitet.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer;
// Refactoring: Extract Idempotent Receiver
// Pattern: Idempotent Receiver / Inbox - atomare Reservierung verhindert Doppelverarbeitung.
public final class IdempotentReceiver {
private final Set<String> processed=ConcurrentHashMap.newKeySet();
public boolean receive(String messageId,Runnable action){
if(messageId==null||messageId.isBlank()) throw new IllegalArgumentException("messageId");
if(!processed.add(messageId)) return false;
try { action.run(); return true; } catch(RuntimeException ex){ processed.remove(messageId); throw ex; }
}
public boolean wasProcessed(String messageId){ return processed.contains(messageId); }
}
JUnit-Vertrag
JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test;
class IdempotentReceiverTest { @Test void processes_only_once(){ var c=new AtomicInteger(); var r=new IdempotentReceiver(); assertTrue(r.receive("m1",c::incrementAndGet)); assertFalse(r.receive("m1",c::incrementAndGet)); assertEquals(1,c.get()); } }
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Ein Consumer führt Seiteneffekte bei jeder Zustellung erneut aus, obwohl Broker und Netzwerk Nachrichten mehrfach liefern können.
Erkennungsmerkmale
Doppelte Buchung oder E-Mail; Consumer vertraut exactly once; MessageId wird nur geloggt; dedup check und Side Effect sind nicht atomar; Retries wiederholen teilweise abgeschlossene Arbeit.
Ziel und Nutzen
Ein Idempotent Receiver prüft einen stabilen Message Identifier in einer Inbox und koppelt Deduplizierung mit der fachlichen Transaktion oder einem wiederholbaren Ergebnis.
Sicherer Refactoring-Ablauf
Stabile Nachrichtenidentität und gewünschte Idempotenzdomäne bestimmen.
InboxPort mit atomarem reserve/complete oder transaktionalem insert einführen.
Handlerresultat und Side Effect in dieselbe Konsistenzgrenze bringen.
Duplicate-Entscheidung explizit zurückgeben.
Crashpunkte, Parallelzustellung und Retention testen.
Wann sinnvoll - und wann nicht
Sinnvoll für mindestens-einmal-Zustellung und externe Webhooks. Wenn eine Operation bereits mathematisch idempotent ist, kann zusätzliche Inbox unnötig sein.
Risiken und typische Fehlanwendungen
Check-then-act ohne Transaktion ist racy. Zu kurze Retention lässt alte Duplikate erneut wirken. MessageId eines Providers kann nur innerhalb eines Scopes eindeutig sein.
Abgrenzung
Idempotent Receiver schützt die Verarbeitung. Idempotency Key schützt meist einen API-Befehl; Inbox ist eine typische Persistenztechnik.
PaymentWebhookReceiver reserviert ProviderEventId innerhalb ProviderAccountId, verbucht die Zahlung und markiert den InboxRecord atomar abgeschlossen.
Teststrategie
Paralleltests prüfen eine Wirkung bei vielen gleichen Zustellungen. Crash-Injection testet Reserve, Side Effect und Complete; Retentiontests prüfen definierte Wiederaufnahme.
Payload, Typ, Version, Korrelation und Zeitpunkt werden als lose Header weitergereicht.
Sichere Transformation
Führe einen unveränderlichen Envelope ein, der technische Metadaten und fachliche Payload sauber trennt.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Introduce Event Envelope
// Pattern: Event Envelope - technische Metadaten werden vom fachlichen Payload getrennt.
public record EventEnvelope<T>(UUID eventId,String type,int version,String correlationId,Instant occurredAt,T payload,Map<String,String> headers){
public EventEnvelope { Objects.requireNonNull(eventId); if(type==null||type.isBlank()) throw new IllegalArgumentException("type"); if(version<1) throw new IllegalArgumentException("version"); Objects.requireNonNull(occurredAt); Objects.requireNonNull(payload); headers=Map.copyOf(headers); }
public EventEnvelope<T> withHeader(String key,String value){ var h=new LinkedHashMap<>(headers); h.put(key,value); return new EventEnvelope<>(eventId,type,version,correlationId,occurredAt,payload,h); }
}
JUnit-Vertrag
JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import java.util.*; import org.junit.jupiter.api.Test;
class EventEnvelopeTest { @Test void header_update_is_immutable(){ var a=new EventEnvelope<>(UUID.randomUUID(),"OrderPlaced",1,"c1",Instant.EPOCH,"payload",Map.of()); var b=a.withHeader("tenant","t1"); assertTrue(a.headers().isEmpty()); assertEquals("t1",b.headers().get("tenant")); } }
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fachdaten, Transportheader und Betriebsmetadaten werden als lose Parameter oder untypisierte Map weitergegeben. Jeder Consumer interpretiert Version, Zeit und Korrelation anders.
Erkennungsmerkmale
Map<String,Object> headers; payload ohne EventType; unterschiedliche Timestampfelder; Trace- und Tenantdaten in Fachpayload; keine klare SchemaVersion.
Ziel und Nutzen
Ein Event Envelope trennt unveränderlich Payload von standardisierten Metadaten wie EventId, Type, SchemaVersion, occurredAt, correlationId und causationId.
Sicherer Refactoring-Ablauf
Benötigte gemeinsame Metadaten und Datenschutzgrenzen festlegen.
Typisierten generischen oder domänenspezifischen Envelope entwerfen.
Brokerheader am Adapter in den Envelope übersetzen.
Consumer auf Envelope statt Rohmap umstellen.
Versionierungs-, Serialisierungs- und Kompatibilitätstests ergänzen.
Wann sinnvoll - und wann nicht
Sinnvoll bei mehreren Eventtypen und Integrationskanälen. Für einen lokalen Methodenaufruf ist ein Envelope meist unnötig.
Risiken und typische Fehlanwendungen
Ein überladener Envelope wird zum globalen Kontextcontainer. Fachliche Information gehört in die Payload; sensible Betriebsdaten dürfen nicht unkontrolliert propagiert werden.
At-most-once, at-least-once und Retry-Verhalten sind als verstreute Booleans implementiert.
Sichere Transformation
Kapsle Zustellsemantik und zulässige Wiederholungen in einer typisierten Policy.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Delivery Guarantee Policy
// Pattern: Policy - Zustellsemantik wird explizit und testbar.
public record DeliveryGuaranteePolicy(Mode mode,int maxAttempts){
public enum Mode { AT_MOST_ONCE, AT_LEAST_ONCE }
public DeliveryGuaranteePolicy { if(mode==null) throw new IllegalArgumentException("mode"); if(maxAttempts<1) throw new IllegalArgumentException("maxAttempts"); if(mode==Mode.AT_MOST_ONCE && maxAttempts!=1) throw new IllegalArgumentException("at-most-once requires one attempt"); }
public boolean mayRetry(int completedAttempts){ return mode==Mode.AT_LEAST_ONCE && completedAttempts<maxAttempts; }
}
JUnit-Vertrag
JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class DeliveryGuaranteePolicyTest { @Test void retries_only_for_at_least_once(){ var p=new DeliveryGuaranteePolicy(DeliveryGuaranteePolicy.Mode.AT_LEAST_ONCE,3); assertTrue(p.mayRetry(1)); assertFalse(p.mayRetry(3)); } }
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Zustellgarantien entstehen zufällig aus Brokerkonfiguration, Ack-Zeitpunkt und Retrycode. Teams sprechen von exactly once, obwohl End-to-End-Verhalten nicht definiert ist.
Erkennungsmerkmale
Ack vor oder nach Side Effect ohne Dokumentation; automatische Retries; gemischte at-most/at-least-once-Pfade; keine Idempotenz; DLQ und Timeout widersprechen sich.
Ziel und Nutzen
Eine Delivery Guarantee Policy beschreibt pro Nachrichtenklasse bewusst Ack-Zeitpunkt, Wiederholung, Deduplizierung, Persistenz und zulässigen Verlust.
Sicherer Refactoring-Ablauf
Geschäftliche Folgen von Verlust und Duplikat bestimmen.
Garantie als explizite Policy und Decision Record dokumentieren.
Ack, Inbox/Outbox und Retry entsprechend ausrichten.
Adapterkonfiguration aus der Policy ableiten.
Fehler- und Crashpunkte End-to-End testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei unterschiedlichen Kritikalitäten und mehreren Brokertechnologien. Eine Policy kann physikalische Garantien nicht herbeizaubern; sie muss reale Grenzen benennen.
Risiken und typische Fehlanwendungen
Exactly once wird oft fälschlich versprochen. Deduplizierung hat Retentiongrenzen; externe Side Effects können zusätzliche Idempotenz benötigen. Höhere Garantie erhöht Latenz und Kosten.
Abgrenzung
At-most-once, at-least-once und effectively-once beschreiben Wirkung. Delivery Policy bündelt die dafür nötigen technischen und fachlichen Entscheidungen.
Verwandte Refactorings und Muster
Delivery Semantics, Inbox, Outbox, Idempotent Receiver, Ack Policy und Dead Letter Policy.
Enterprise-Java-Einordnung
LedgerPostingEvent nutzt at-least-once plus Inbox und transaktionale Buchung; TelemetrySample darf nach definiertem Budget at-most-once verarbeitet werden.
Teststrategie
Failure-Injection-Tests unterbrechen vor/nach Ack und Side Effect. Contract-Tests prüfen Adapterkonfiguration; Recoverytests zählen Verlust und Duplikate.
154. Introduce Consumer Offset Checkpoint / Offset Value Object
Ausgangsproblem
Partition und Offset werden als primitive Zahlen verteilt und versehentlich vertauscht.
Sichere Transformation
Führe ein Value Object für Partition und Offset ein und mache Fortschritt nur monoton fortschreibbar.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Consumer Offset
// Pattern: Checkpoint Value Object - Fortschritt wird typisiert und monoton modelliert.
public record ConsumerOffset(int partition,long value){
public ConsumerOffset { if(partition<0||value<0) throw new IllegalArgumentException(); }
public ConsumerOffset advanceTo(long next){ if(next<value) throw new IllegalArgumentException("offset regression"); return new ConsumerOffset(partition,next); }
}
JUnit-Vertrag
JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test;
class ConsumerOffsetTest { @Test void offset_cannot_move_backwards(){ var o=new ConsumerOffset(2,10); assertEquals(12,o.advanceTo(12).value()); assertThrows(IllegalArgumentException.class,()->o.advanceTo(9)); } }
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Fortschritt eines Consumers wird als primitive Zahl, statisches Feld oder impliziter Brokerzustand behandelt. Replay, Neustart und Partitionierung sind dadurch schwer kontrollierbar.
Erkennungsmerkmale
long lastId in Service; Offset wird vor Verarbeitung gespeichert; Partition fehlt; Reset erfolgt manuell; unterschiedliche Consumer teilen unbeabsichtigt denselben Fortschritt.
Ziel und Nutzen
Ein Consumer Offset Value Object identifiziert Stream, Partition, Consumergruppe und Position. Ein Offset Store legt Fortschritt erst gemäß definierter Commit Policy fest.
Sicherer Refactoring-Ablauf
Fortschrittsdomäne und Partitionen explizit bestimmen.
Offset als typisierten unveränderlichen Wert modellieren.
OffsetStorePort mit compare-and-set oder transaktionalem Commit einführen.
Verarbeitungs- und Commitreihenfolge definieren.
Replay-, Gap-, Restart- und Paralleltests durchführen.
Wann sinnvoll - und wann nicht
Sinnvoll bei Streams, Pollingquellen und Projection Rebuilds. Bei brokerverwaltetem einfachen Consumer reicht die native Verwaltung, sofern sie als Infrastrukturdetail gekapselt bleibt.
Risiken und typische Fehlanwendungen
Commit vor Wirkung erzeugt Verlust, Commit danach Duplikate. Offset allein garantiert keine fachliche Idempotenz. Reset und Retention können nicht verfügbare Daten adressieren.
Subscriber werden in statischen Listen verwaltet und kennen Transportdetails.
Sichere Transformation
Extrahiere eine Registry mit typisierten Topics, idempotenter Anmeldung und expliziter Veröffentlichung.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer;
// Refactoring: Extract Subscription Registry
// Pattern: Registry + Observer - Subscriber werden typisiert und entkoppelt verwaltet.
public final class SubscriptionRegistry {
private final Map<String,Map<String,Consumer<EventEnvelope<?>>>> subscribers=new ConcurrentHashMap<>();
public void subscribe(String topic,String subscriberId,Consumer<EventEnvelope<?>> handler){ subscribers.computeIfAbsent(topic,k->new ConcurrentHashMap<>()).put(subscriberId,handler); }
public int publish(String topic,EventEnvelope<?> event){ var map=subscribers.getOrDefault(topic,Map.of()); map.values().forEach(h->h.accept(event)); return map.size(); }
public void unsubscribe(String topic,String subscriberId){ var map=subscribers.get(topic); if(map!=null) map.remove(subscriberId); }
}
JUnit-Vertrag
JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import static org.junit.jupiter.api.Assertions.*; import java.time.Instant; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test;
class SubscriptionRegistryTest { @Test void publishes_to_registered_subscribers(){ var count=new AtomicInteger(); var r=new SubscriptionRegistry(); r.subscribe("orders","s1",e->count.incrementAndGet()); var e=new EventEnvelope<>(UUID.randomUUID(),"OrderPlaced",1,"c",Instant.EPOCH,"x",Map.of()); assertEquals(1,r.publish("orders",e)); assertEquals(1,count.get()); } }
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Abonnenten und Eventtypen sind über if-Ketten, Frameworkannotation und Stringnamen verteilt. Vollständigkeit, Mehrfachregistrierung und Lebenszyklus sind nicht sichtbar.
Erkennungsmerkmale
handler map wird an mehreren Stellen verändert; Klassenname als Eventtyp; unbekannte Events werden ignoriert; Tests booten ganzen Container; dynamisches Entfernen ist unsicher.
Ziel und Nutzen
Eine Subscription Registry verwaltet typisierte Zuordnung zwischen EventType und Handlern sowie Regeln für Eindeutigkeit, Priorität und Lebenszyklus.
Sicherer Refactoring-Ablauf
Eventtypen und Handlervertrag typisieren.
Registrierungsregeln und Duplicate-Verhalten festlegen.
Registry im Composition Root aufbauen.
Dispatcher nur gegen die Registry arbeiten lassen.
Vollständigkeit, Reihenfolge und unbekannte Typen testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei erweiterbaren Handlerlandschaften oder Plugins. Bei wenigen statischen Abhängigkeiten ist direkte Konstruktorverdrahtung einfacher.
Risiken und typische Fehlanwendungen
Dynamische Registrierung zur Laufzeit kann Race Conditions und schwer reproduzierbare Systeme erzeugen. Eine globale Registry wird leicht zum Service Locator.
Subscription Registry, Observer, Dispatcher, Strategy Registry und Composition Root.
Enterprise-Java-Einordnung
IntegrationEventSubscriptionRegistry ordnet EventType<OrderShippedV2> einer geordneten Liste von Handlern zu und wird beim Start unveränderlich abgeschlossen.
Teststrategie
Bootstrapping-Tests prüfen doppelte und fehlende Registrierung. Dispatcher-Tests prüfen Reihenfolge, Unknown-Fall und Fehlerisolation.
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Veraltete Nachrichten werden noch verarbeitet, obwohl ihr fachlicher Wert abgelaufen ist. Broker-TTL, fachliche Gültigkeit und Verarbeitungsdeadline sind nicht unterschieden.
Erkennungsmerkmale
PromotionEvent nach Kampagnenende; Retry läuft stundenlang; TTL als magische Brokerzahl; Consumer kennt occurredAt, aber keine Gültigkeitsregel; abgelaufene Nachricht erzeugt Side Effect.
Ziel und Nutzen
Eine Message Expiration Policy oder ExpiresAt im Envelope macht fachliche Lebensdauer explizit. Consumer trifft vor Verarbeitung eine typisierte Entscheidung über Process, Discard oder Compensate.
Sicherer Refactoring-Ablauf
Fachliche Gültigkeitsdauer je Nachrichtentyp bestimmen.
ExpiresAt oder ExpirationPolicy mit Clock einführen.
Broker-TTL nur als technische Optimierung aus der Policy ableiten.
Consumer vor Side Effects prüfen lassen.
Grenzzeit, Retry und verspätete Zustellung testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei zeitkritischen Befehlen, Angeboten und temporären Daten. Historische Events dürfen oft nicht verfallen, auch wenn ihre Verarbeitung verspätet ist.
Risiken und typische Fehlanwendungen
Broker-TTL kann Nachrichten still entfernen und Audit erschweren. Clockskew und Zeitzonen müssen klar sein. Verwerfen ohne Evidenz kann fachliche Pflichten verletzen.
Abgrenzung
Message Expiration betrifft Lebensdauer einer Nachricht. Deadline begrenzt einen Ablauf; Retention bestimmt Speicherdauer.
Verwandte Refactorings und Muster
Message Expiration, Deadline, Retention Policy, Delivery Attempt und Dead Letter Policy.
Enterprise-Java-Einordnung
ReserveInventoryCommand trägt expiresAt. Nach Ablauf erzeugt der Consumer ExpiredCommandEvidence statt eine späte Reservierung auszuführen.
Teststrategie
Fake-Clock-Tests prüfen exakt vor, bei und nach Ablauf. Integrationstests vergleichen Broker-TTL und fachliche Entscheidung; Audittests sichern Evidenz.
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Wiederholungsabstände werden in Schleifen, Schedulerjobs oder Brokerheadern ad hoc berechnet. Unterschiedliche Consumer verwenden widersprüchliche Backoff- und Jitterregeln.
Erkennungsmerkmale
Thread.sleep; attempt*1000; keine Obergrenze; alle Instanzen retryen gleichzeitig; Retryzeit wird bei Neustart neu berechnet; Fehlerklasse wird ignoriert.
Ziel und Nutzen
Ein Retry Schedule berechnet aus Versuch, Fehlerklasse und Policy den nächsten zulässigen Zeitpunkt. Backoff, Jitter, Maximum und Stop-Entscheidung werden deterministisch modelliert.
Sicherer Refactoring-Ablauf
Retryfähige Fehler und maximale Versuche definieren.
Backoff-Algorithmus und Jitterquelle abstrahieren.
RetryDecision mit nextEligibleAt oder Stop einführen.
Scheduler/Brokeradapter nur noch die Entscheidung ausführen lassen.
Deterministische Sequenzen und Lastverhalten testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei asynchronen Retries und persistenter Wiederaufnahme. Für einen einzelnen kurzen synchronen Retry genügt eine kleine lokale Policy.
Risiken und typische Fehlanwendungen
Unbegrenzte Retries verstecken dauerhafte Fehler. Zufallsjitter ohne injizierbare Quelle macht Tests instabil. Backoff darf die fachliche Deadline nicht überschreiten.
Retry Schedule, Backoff Strategy, Delivery Attempt, Deadline und Circuit Breaker.
Enterprise-Java-Einordnung
ProviderDeliveryRetrySchedule liefert für Attempt 3 bei Timeout einen Zeitpunkt mit exponentiellem Backoff und deterministischem Jitter; bei ValidationError liefert es Stop.
Teststrategie
Fake Clock und feste JitterSource sichern exakte Zeitpunkte. Tabellen prüfen Fehlerklassen, Maximum und Deadline; Lasttests prüfen Entsynchronisierung.
158. Introduce Poison Message Quarantine Dead Letter / Quarantine
Ausgangsproblem
Dauerhaft fehlerhafte Nachrichten blockieren Partitionen oder verschwinden nur im Log.
Sichere Transformation
Führe eine Quarantäne mit Grund, Versuchszahl und Zeitstempel ein.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Introduce Poison Message Quarantine
// Pattern: Dead Letter Channel / Quarantine - dauerhaft fehlerhafte Nachrichten werden nachvollziehbar isoliert.
public final class PoisonMessageQuarantine {
public record Entry(String messageId,String reason,int attempts,Instant quarantinedAt){ public Entry { if(messageId==null||messageId.isBlank()||reason==null||reason.isBlank()||attempts<1||quarantinedAt==null) throw new IllegalArgumentException(); } }
private final List<Entry> entries=new ArrayList<>();
public void add(Entry entry){ entries.add(entry); }
public List<Entry> entries(){ return List.copyOf(entries); }
}
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Eine dauerhaft nicht verarbeitbare Nachricht blockiert Partition oder Queue oder wird ohne ausreichende Evidenz verworfen.
Erkennungsmerkmale
Immer dieselbe Nachricht schlägt fehl; Consumer restartet endlos; DLQ enthält nur Rohpayload; keine Klassifizierung; Support kann Nachricht weder sicher untersuchen noch kontrolliert freigeben.
Ziel und Nutzen
Eine Quarantäne trennt Poison Messages vom normalen Fluss, speichert redigierte Evidenz, Fehlerklasse und Wiederaufnahmezustand und erzwingt einen kontrollierten Freigabeprozess.
Sicherer Refactoring-Ablauf
Poison-Kriterien und maximale Versuche definieren.
QuarantineRecord mit MessageId, Schema, Fehler und Redaction modellieren.
Atomaren Übergang aus normalem Kanal in Quarantäne implementieren.
Analyse-, Korrektur- und Replayworkflow festlegen.
Zugriff, Retention, Replay und Audit testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei dauerhaft fehlerhaften oder sicherheitsrelevanten Nachrichten. Transiente Fehler gehören zunächst in Retry, nicht sofort in Quarantäne.
Risiken und typische Fehlanwendungen
Quarantäne darf kein unbeobachteter Friedhof werden. Rohpayload kann sensible Daten enthalten. Replay ohne Idempotenz oder korrigierte Ursache erzeugt erneute Schäden.
Abgrenzung
Dead Letter Channel ist ein Zielkanal. Quarantäne umfasst zusätzlich Evidenz, Zugriffsschutz und kontrollierten Recoveryprozess.
Verwandte Refactorings und Muster
Poison Message, Dead Letter Policy, Quarantine, Retry Schedule und Idempotent Receiver.
Enterprise-Java-Einordnung
UnknownSchemaEvent wird nach klassifiziertem PermanentFailure als QuarantineRecord gespeichert; ein Operator kann nach Schemafix einen signierten ReplayCommand auslösen.
Teststrategie
Integrationstests prüfen atomaren Transfer und Redaction. Authorization-Tests sichern Zugriff; Replaytests prüfen Idempotenz und vollständigen Audittrail.
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Schemaänderungen werden anhand von Entwicklerintuition oder erst durch fehlschlagende Consumer erkannt. Kompatibilitätsregeln sind nicht automatisiert.
Erkennungsmerkmale
Feld wird umbenannt oder Pflichtfeld hinzugefügt; alte Fixtures fehlen; Registry prüft nur Syntax; Producer und Consumer deployen unabhängig; Breaking Change fällt in Produktion auf.
Ziel und Nutzen
Ein Schema Compatibility Checker bewertet alte und neue Vertragsversion nach expliziten Regeln wie backward, forward oder full compatibility und liefert nachvollziehbare Verstöße.
Sicherer Refactoring-Ablauf
Unterstützte Kompatibilitätsrichtung je Vertrag definieren.
Schema- und Semantikregeln trennen.
CheckerPort und CompatibilityReport einführen.
CI/Registry-Gate vor Veröffentlichung integrieren.
Historische Versionen und bekannte Breaking Changes testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei versionierten Events und unabhängigen Deployments. Bei gemeinsam veröffentlichtem internem Modul kann Compile-Time-Kompatibilität ausreichen.
Risiken und typische Fehlanwendungen
Reine Schemakompatibilität erkennt keine semantische Bedeutungsänderung. Defaultwerte können fachlich falsch sein. Zu viele historische Versionen erhöhen Prüfkosten.
Abgrenzung
Schema Checker prüft strukturelle Verträge. Event Version Translator ermöglicht Verarbeitung alter Versionen; Consumer-Driven Contract prüft konkrete Erwartungen.
Verwandte Refactorings und Muster
Schema Compatibility, Event Versioning, Contract Test, Upcaster und Published Language.
Enterprise-Java-Einordnung
OrderEventCompatibilityChecker meldet, dass removal von currency backward-incompatible ist und dokumentiert betroffene ConsumerCapabilities.
Teststrategie
Fixturetests enthalten erlaubte und verbotene Evolutionen. CI-Tests prüfen alle veröffentlichten Versionen; semantische Contract-Tests ergänzen Strukturprüfung.
Domänenereignisse werden direkt serialisiert und veröffentlichen interne Modellstrukturen.
Sichere Transformation
Führe einen Mapper ein, der interne Domain Events in versionierte externe Verträge übersetzt.
Java-21-Zielcode
Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Introduce Integration Event Mapper
// Pattern: Domain Event to Integration Event Mapper / Anti-Corruption Layer.
public final class IntegrationEventMapper {
public record OrderPlaced(String orderId,long totalCents) { public OrderPlaced { if(orderId==null||orderId.isBlank()||totalCents<0) throw new IllegalArgumentException(); } }
public EventEnvelope<Map<String,Object>> map(OrderPlaced event,String correlationId){
var payload=Map.<String,Object>of("orderId",event.orderId(),"totalCents",event.totalCents());
return new EventEnvelope<>(UUID.randomUUID(),"order.placed",1,correlationId,Instant.now(),payload,Map.of("source","orders"));
}
}
Einsatzgrenze: Das Refactoring lohnt sich, wenn die jeweilige Integrationsregel mehrfach vorkommt, fachliche Risiken trägt oder separat getestet werden muss. Für einen einmaligen, trivialen Datenfluss wäre die zusätzliche Abstraktion unnötig.
Ausführliche fachliche Vertiefung
Fachliches Ausgangsproblem
Domain Events werden unverändert nach außen veröffentlicht oder Integrationsverträge direkt im Aggregat erzeugt. Interne Modelländerungen brechen dadurch externe Consumer.
Erkennungsmerkmale
Aggregate serialisiert sich selbst; interne Entityreferenzen im Event; vertrauliche Felder werden veröffentlicht; externe Schemanamen stehen im Domainmodul; jede Änderung zwingt Consumerupdate.
Ziel und Nutzen
Ein Integration Event Mapper übersetzt interne Domain Events in bewusst veröffentlichte, versionierte Integration Events. Er filtert, normalisiert und ergänzt nur erlaubte Kontextdaten.
Sicherer Refactoring-Ablauf
Interne Tatsache und extern benötigten Vertrag getrennt beschreiben.
Published Integration Event mit Ownership und Version definieren.
Mapper an Application-/Outbox-Grenze einführen.
Datenschutz, Normalisierung und IDs explizit behandeln.
Domain- und Integrationsevolution unabhängig testen.
Wann sinnvoll - und wann nicht
Sinnvoll bei externen oder modulübergreifenden Consumerverträgen. Innerhalb eines kleinen Moduls kann das Domain Event direkt genügen.
Risiken und typische Fehlanwendungen
Der Mapper darf keine neue Fachentscheidung treffen oder Daten nachladen, die den Eventzeitpunkt verfälschen. Ein universeller Mapper koppelt alle Domänen erneut zentral.
Abgrenzung
Domain Event ist internes Fachsignal. Integration Event ist veröffentlichter Vertrag; Message Translator übersetzt meist fremde Formate.
Verwandte Refactorings und Muster
Domain Event, Integration Event, Outbox, Mapper, Published Language und Anti-Corruption Layer.
Enterprise-Java-Einordnung
PolicyIssuedDomainEvent wird zu PolicyIssuedV2 mit PolicyId, productCode, premium und occurredAt; interne UnderwritingDetails bleiben unveröffentlicht.
Teststrategie
Mappingtests prüfen erlaubte Felder, Version und deterministische Ausgabe. Contract-Tests sichern Consumervertrag; Datenschutztests verhindern interne Datenlecks.
Ereignisse werden in beliebigen Tabellen, Logdateien oder technischen Queues abgelegt. Schreiben, Stream-Lesen, Versionierung und Konfliktprüfung sind über Services verteilt. Ein expliziter Event Store bildet Append-only-Schreiben, geordnetes Lesen und erwartete Version als eine konsistente Persistenzgrenze ab.
Erkennungsmerkmale
mehrere Persistenzpfade für dieselbe Ereignisfolge
Aggregate werden aus ungeordneten Datensätzen rekonstruiert
Versionskonflikte werden erst durch Datenfehler sichtbar
Sicherer Ablauf
bestehende Ereignisreihenfolge und Identität durch Charakterisierungstests sichern
Event Envelope mit Aggregate-ID, Version, Typ und Nutzdaten definieren
append(streamId, expectedVersion, events) als atomaren Vertrag einführen
Lesen ausschließlich über geordnete Streams führen
Konflikt-, Replay- und Wiederanlauftests ergänzen
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn Ereignisse die maßgebliche Historie eines Aggregats bilden und Auditierbarkeit oder Replay benötigt werden. Für gewöhnliches CRUD ohne fachliche Historie wäre der Aufwand meist überzogen.
Risiken und Fehlanwendungen
Ein Event Store ist keine allgemeine Logablage. Unveränderlichkeit, Reihenfolge, Schemaentwicklung und Zugriffsschutz müssen verbindlich sein; spätere Korrekturen erfolgen durch neue Ereignisse, nicht durch stilles Überschreiben.
Enterprise-Java-Beispiel
Ein Order-Aggregat wird aus OrderCreated, ItemAdded und OrderConfirmed rekonstruiert. Der erwartete Versionswert verhindert, dass zwei parallele Bestätigungen denselben Stand überschreiben.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Event Store
// Pattern: Event Store / Repository - Append und Stream-Lesen bilden eine klare Persistenzgrenze.
public final class EventStore {
public record StoredEvent(String aggregateId,long version,String type,String payload){ public StoredEvent { if(aggregateId==null||aggregateId.isBlank()||version<1||type==null||type.isBlank()) throw new IllegalArgumentException(); } }
private final Map<String,List<StoredEvent>> streams=new HashMap<>();
public synchronized void append(String id,long expectedVersion,List<StoredEvent> events){ var current=streams.computeIfAbsent(id,k->new ArrayList<>()); if(current.size()!=expectedVersion) throw new IllegalStateException("version conflict"); long v=expectedVersion; for(var e:events) current.add(new StoredEvent(id,++v,e.type(),e.payload())); }
public synchronized List<StoredEvent> load(String id){ return List.copyOf(streams.getOrDefault(id,List.of())); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class EventStoreTest { @Test void contract(){ var s=new EventStore(); s.append("o1",0,java.util.List.of(new EventStore.StoredEvent("o1",1,"CREATED","{}"))); org.junit.jupiter.api.Assertions.assertEquals(1,s.load("o1").size()); org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,()->s.append("o1",0,java.util.List.of())); } }
Teststrategie: Vertragstests prüfen atomisches Append, Versionskonflikt, stabile Reihenfolge, unveränderte Historie und deterministisches Replay.
Parallele Befehle arbeiten auf demselben Aggregatzustand, ohne dass der veraltete Stand beim Speichern erkennbar ist. Eine explizite Aggregate-Version wird Bestandteil des Lade- und Speichervertrags und dient als Optimistic-Concurrency-Token.
Erkennungsmerkmale
Last-write-wins überschreibt fachliche Änderungen
Konflikte treten nur unter Last auf
Versionswerte sind technische Zufallsfelder ohne Domänenvertrag
Sicherer Ablauf
Version beim Laden gemeinsam mit dem Aggregat zurückgeben
Befehl gegen genau diese Version ausführen
beim Speichern expectedVersion vergleichen
bei Konflikt einen typisierten Fehler statt stiller Wiederholung liefern
fachlich entscheiden, ob neu laden, zusammenführen oder ablehnen
Wann sinnvoll - und wann nicht?
Sinnvoll bei konkurrierenden Änderungen mit niedriger bis mittlerer Konfliktrate. Bei exklusiver Bearbeitung oder sehr hoher Konfliktdichte kann ein anderer Koordinationsmechanismus geeigneter sein.
Risiken und Fehlanwendungen
Automatisches Retry kann eine fachlich nicht mehr gültige Entscheidung erneut ausführen. Die Version schützt nur den technischen Stand; Konfliktauflösung bleibt eine Fachentscheidung.
Enterprise-Java-Beispiel
Zwei Sachbearbeiter ändern denselben Schadenfall. Die zweite Speicherung erkennt, dass die Freigabe inzwischen erfolgt ist, und verlangt eine neue Bewertung.
Abgrenzung und verwandte Muster
Optimistic Locking, Event Store, Conflict Resolver, Unit of Work.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Aggregate Version
// Pattern: Optimistic Concurrency Token - konkurrierende Commits werden sichtbar.
public record AggregateVersion(long value){
public AggregateVersion { if(value<0) throw new IllegalArgumentException("value"); }
public AggregateVersion next(){ return new AggregateVersion(value+1); }
public void requireExpected(AggregateVersion expected){ if(!equals(expected)) throw new IllegalStateException("stale aggregate"); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class AggregateVersionTest { @Test void contract(){ var v=new AggregateVersion(2); org.junit.jupiter.api.Assertions.assertEquals(3,v.next().value()); org.junit.jupiter.api.Assertions.assertThrows(IllegalStateException.class,()->v.requireExpected(new AggregateVersion(1))); } }
Teststrategie: Paralleltests erzeugen zwei Befehle auf derselben Version und erwarten genau einen Erfolg sowie einen klaren Konflikt.
Das Wiederherstellen eines Aggregats erfordert das Abspielen einer sehr langen Ereignishistorie und belastet Latenz sowie Infrastruktur. Ein getrenntes Snapshot Store speichert einen überprüfbaren Zustandsstand samt Event-Version; danach werden nur neuere Ereignisse nachgespielt.
Erkennungsmerkmale
Replay-Zeit wächst kontinuierlich
häufig gelesene Aggregate besitzen tausende Ereignisse
Snapshot-Logik liegt verstreut in Repositories
Sicherer Ablauf
Snapshot als abgeleitete Optimierung definieren, nicht als neue Wahrheit
Schema, Aggregate-ID und abgedeckte Version speichern
beim Laden jüngsten kompatiblen Snapshot wählen
Ereignisse nach Snapshot-Version nachspielen
Snapshots jederzeit lösch- und reproduzierbar halten
Wann sinnvoll - und wann nicht?
Sinnvoll bei langen Streams und messbarer Replay-Latenz. Bei kurzen Historien erhöht Snapshotting nur Komplexität.
Risiken und Fehlanwendungen
Veraltete oder inkompatible Snapshots dürfen den Event Stream nicht verfälschen. Snapshot-Migration muss entweder explizit erfolgen oder durch Neuaufbau ersetzt werden.
Enterprise-Java-Beispiel
Ein Vertragskonto mit mehrjähriger Historie wird aus einem monatlichen Snapshot plus den Ereignissen des laufenden Monats rekonstruiert.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Snapshot Store
// Pattern: Snapshot / Repository - Event-Replay wird durch versionierte Snapshots begrenzt.
public final class SnapshotStore {
public record Snapshot(String aggregateId,long version,Map<String,String> state){ public Snapshot { state=Map.copyOf(state); } }
private final Map<String,Snapshot> data=new HashMap<>();
public synchronized void save(Snapshot snapshot){ var old=data.get(snapshot.aggregateId()); if(old!=null&&old.version()>snapshot.version()) throw new IllegalStateException("older snapshot"); data.put(snapshot.aggregateId(),snapshot); }
public synchronized Optional<Snapshot> find(String id){ return Optional.ofNullable(data.get(id)); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class SnapshotStoreTest { @Test void contract(){ var s=new SnapshotStore(); s.save(new SnapshotStore.Snapshot("a",3,java.util.Map.of("status","OPEN"))); org.junit.jupiter.api.Assertions.assertEquals(3,s.find("a").orElseThrow().version()); } }
Teststrategie: Tests vergleichen Zustand aus vollständigem Replay mit Zustand aus Snapshot plus Restereignissen und prüfen beschädigte oder alte Snapshot-Versionen.
Fachereignisse werden direkt in Controller- oder Reportcode ausgewertet; jede Abfrage rekonstruiert erneut denselben Lesestand. Eine Projection übersetzt Ereignisse deterministisch in ein für einen konkreten Lesezweck optimiertes Modell.
Erkennungsmerkmale
UI-Abfragen spielen Events zur Laufzeit nach
mehrere Leser duplizieren Transformationslogik
Lesemodelle sind eng an Schreibaggregate gekoppelt
Sicherer Ablauf
eine konkrete Abfrage und ihr benötigtes Schema festlegen
Projektor als reine oder idempotente Ereignisverarbeitung extrahieren
Checkpoint beziehungsweise Offset speichern
Rebuild-Pfad aus vollständiger Historie vorsehen
Projektionsverzögerung sichtbar messen
Wann sinnvoll - und wann nicht?
Sinnvoll bei eventbasierten Schreibmodellen oder teuren zusammengesetzten Abfragen. Für einfache transaktionale Tabellen ist eine zusätzliche Projektion oft unnötig.
Risiken und Fehlanwendungen
Projektionen sind abgeleitete Daten und dürfen nicht zur konkurrierenden Schreibwahrheit werden. Reihenfolge, Duplikate und Schemaänderungen müssen behandelt werden.
Enterprise-Java-Beispiel
Aus Bestellereignissen entsteht ein OrderOverview-Lesemodell mit Betrag, Status und Lieferadresse für die Kundenansicht.
Dasselbe Objektmodell soll gleichzeitig komplexe Geschäftsregeln schützen und flexible, schnelle Abfragen bedienen. Ein separates Read Model wird auf die Bedürfnisse einer Abfrage zugeschnitten und unabhängig vom fachlichen Schreibmodell gepflegt.
Erkennungsmerkmale
DTOs greifen auf interne Aggregatstrukturen zu
Lesen erfordert zahlreiche Joins oder Lazy Loads
Schreibmodell wird für Reporting mit technischen Feldern aufgebläht
Sicherer Ablauf
Query-Anwendungsfälle und erforderliche Felder erfassen
eigenes unveränderliches Leseschema definieren
Aktualisierung über Projektion oder kontrollierte Synchronisation einführen
Query-Port vom Schreibrepository trennen
Staleness und Aktualisierungszeitpunkt dokumentieren
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn Lese- und Schreibanforderungen deutlich auseinandergehen. Bei kleinen CRUD-Modellen kann die Trennung unnötige Synchronisationsarbeit erzeugen.
Risiken und Fehlanwendungen
Ein Read Model kann zeitverzögert sein. Benutzeroberflächen und Fachprozesse müssen wissen, wann starke Aktualität erforderlich ist.
Enterprise-Java-Beispiel
Das Zahlungsaggregat schützt Autorisierung und Capture; ein PaymentSearchView liefert dagegen flache Suchdaten für den Support.
Bei parallelen oder replizierten Änderungen wird an vielen Stellen ad hoc entschieden, welcher Wert gewinnt. Ein typisierter Conflict Resolver kapselt Vergleich, Merge oder Eskalation als explizite fachliche Strategie.
Erkennungsmerkmale
Last-write-wins ist implizit verteilt
Konfliktfälle werden manuell in Datenbankskripten bereinigt
verschiedene Felder benötigen unterschiedliche Merge-Regeln
Sicherer Ablauf
Konfliktarten und unverlierbare Informationen klassifizieren
Versionen und Metadaten vollständig an den Resolver übergeben
deterministische Merge-Regeln pro Datentyp definieren
nicht automatisch lösbare Fälle als Decision/Escalation zurückgeben
jede Entscheidung auditierbar protokollieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei Multi-Writer-Replikation, Offline-Änderungen oder parallelen Fachentscheidungen. Vermeidbar, wenn ein klarer Single Writer garantiert ist.
Risiken und Fehlanwendungen
Technische Zeitstempel allein bilden selten fachliche Priorität ab. Ein falscher automatischer Merge kann Daten still verlieren.
Enterprise-Java-Beispiel
Zwei Regionen ändern Kontakt- und Risikodaten eines Kunden. Kontaktfelder lassen sich zusammenführen, eine widersprüchliche Risikoklasse wird zur Prüfung eskaliert.
Unklar ist, welche Änderungen gemeinsam atomar sein müssen und welche nur irgendwann konsistent werden dürfen. Eine benannte Consistency Boundary macht fachliche Invarianten, atomaren Umfang und asynchrone Folgeaktionen explizit.
Erkennungsmerkmale
große Transaktionen über mehrere Dienste
Teilupdates ohne dokumentierte Invariante
Anwendungsservice mischt lokale Transaktion und externe Aufrufe
Sicherer Ablauf
fachliche Invarianten und betroffene Daten identifizieren
kleinste notwendige atomare Grenze bestimmen
externe Systeme aus der lokalen Transaktion herauslösen
Folgeaktionen über Ereignis/Outbox modellieren
Kompensation und sichtbaren Zwischenzustand definieren
Wann sinnvoll - und wann nicht?
Sinnvoll, sobald ein Vorgang mehrere Aggregate, Datenbanken oder Dienste berührt. Innerhalb eines einfachen Aggregats genügt häufig die bestehende Transaktionsgrenze.
Risiken und Fehlanwendungen
Eine zu große Grenze senkt Verfügbarkeit und Skalierbarkeit; eine zu kleine verletzt Invarianten. Eventual Consistency darf nicht als fehlende Fehlerbehandlung missverstanden werden.
Enterprise-Java-Beispiel
Die Reservierung einer Bestellung ist lokal atomar. Versand und Benachrichtigung folgen zuverlässig über Outbox-Ereignisse.
Abgrenzung und verwandte Muster
Aggregate, Transaction Boundary, Outbox, Saga, Unit of Work.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Consistency Boundary
// Pattern: Unit of Work - nur explizit registrierte Änderungen werden gemeinsam committed.
public final class ConsistencyBoundary {
private final List<Runnable> changes=new ArrayList<>(); private boolean committed;
public void register(Runnable change){ if(committed) throw new IllegalStateException("already committed"); changes.add(Objects.requireNonNull(change)); }
public void commit(){ if(committed) throw new IllegalStateException("already committed"); changes.forEach(Runnable::run); committed=true; }
public int pending(){ return committed?0:changes.size(); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class ConsistencyBoundaryTest { @Test void contract(){ var x=new java.util.concurrent.atomic.AtomicInteger(); var b=new ConsistencyBoundary(); b.register(x::incrementAndGet); org.junit.jupiter.api.Assertions.assertEquals(1,b.pending()); b.commit(); org.junit.jupiter.api.Assertions.assertEquals(1,x.get()); } }
Teststrategie: Invarianten-, Crash- und Wiederanlauftests prüfen, dass lokale Zustände atomar und externe Folgen wiederholbar sind.
Nachgelagerte Systeme lesen Tabellen direkt oder periodisch vollständig aus, um Änderungen zu erkennen. Ein CDC-Adapter übersetzt das technische Änderungsprotokoll der Datenbank in stabile, typisierte Integrationsereignisse.
Erkennungsmerkmale
Polling mit Zeitstempeln übersieht oder dupliziert Änderungen
Reporting kennt interne Tabellenschemata
Datenbanktrigger enthalten Integrationslogik
Sicherer Ablauf
Quelle und garantierte Reihenfolge des Änderungslogs bestimmen
technisches CDC-Format hinter einem Adapter kapseln
Schema- und Operationsarten in interne Change-Objekte übersetzen
Checkpoint und Wiederaufnahme definieren
fachliche Events nur dann ableiten, wenn Semantik eindeutig ist
Wann sinnvoll - und wann nicht?
Sinnvoll bei Legacy-Datenbanken oder Replikations-/Analysepfaden ohne direkte Ereignispublikation. Für neue Fachprozesse ist eine explizite Outbox oft klarer.
Risiken und Fehlanwendungen
Eine Tabellenänderung ist nicht automatisch ein Domain Event. CDC koppelt an physische Datenmodelle und benötigt Schemaevolution, Datenschutz und Rebuild-Strategie.
Enterprise-Java-Beispiel
Änderungen an einer alten Vertragsdatenbank werden durch den Adapter in ContractChanged-Integrationsnachrichten für das Data Warehouse übersetzt.
package com.aydinsude.workbench.catalog;
import java.util.Map;
// Refactoring: Introduce Change Data Capture Adapter
// Pattern: Adapter / Anti-Corruption Layer - technische Change-Records werden in Fachänderungen übersetzt.
public final class ChangeDataCaptureAdapter {
public record Change(String aggregateId,String operation,Map<String,String> fields){ public Change { fields=Map.copyOf(fields); } }
public Change translate(Map<String,Object> record){ String id=String.valueOf(record.get("id")); String op=String.valueOf(record.get("op")); @SuppressWarnings("unchecked") var fields=(Map<String,String>)record.getOrDefault("after",Map.of()); return new Change(id,op,fields); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class ChangeDataCaptureAdapterTest { @Test void contract(){ var a=new ChangeDataCaptureAdapter(); var c=a.translate(java.util.Map.of("id","o1","op","u","after",java.util.Map.of("status","PAID"))); org.junit.jupiter.api.Assertions.assertEquals("PAID",c.fields().get("status")); } }
Teststrategie: Fixtures verschiedener CDC-Versionen, Checkpoint-Recovery, Delete/Tombstone und Schemaänderungen werden geprüft.
Replikationsfaktor, Synchronität und Zielknoten werden als verstreute Infrastrukturparameter ohne fachlichen Kontext konfiguriert. Eine Replication Policy beschreibt pro Datenklasse Replikationsfaktor, Bestätigungsanforderung, Regionen und zulässige Verzögerung.
Erkennungsmerkmale
jede Komponente nutzt andere Replikationsannahmen
kritische und unkritische Daten werden gleich behandelt
Failover-Verhalten ist nicht testbar
Sicherer Ablauf
Datenklassen nach Verlust- und Verfügbarkeitsrisiko ordnen
Replikationsziele und Bestätigungsregeln explizit modellieren
Policy vom konkreten Datenbanktreiber trennen
Entscheidung und aktive Konfiguration beobachtbar machen
Fehler- und Degradationsmodi testen
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn mehrere Speicherorte oder Datenklassen unterschiedliche Ziele besitzen. Bei einer einzigen verwalteten Datenbank genügt oft die Plattformkonfiguration.
Risiken und Fehlanwendungen
Eine Policy kann physikalische Grenzen nicht wegabstrahieren. Höhere Replikation erhöht Kosten und kann Schreiblatenz beziehungsweise Konfliktrisiko steigern.
Enterprise-Java-Beispiel
Zahlungsbuchungen benötigen synchrone lokale Mehrfachbestätigung; Produktbilder dürfen asynchron in entfernte Regionen repliziert werden.
Abgrenzung und verwandte Muster
Quorum Policy, Consistency Level, Geo Replication, Data Sovereignty.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Replication Policy
// Pattern: Policy / Strategy - Zielauswahl und Quorum sind unabhängig von Transportcode.
public record ReplicationPolicy(List<String> orderedTargets,int quorum){
public ReplicationPolicy { orderedTargets=List.copyOf(orderedTargets); if(orderedTargets.isEmpty()||quorum<1||quorum>orderedTargets.size()) throw new IllegalArgumentException(); }
public boolean successful(Set<String> acknowledged){ return orderedTargets.stream().filter(acknowledged::contains).count()>=quorum; }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class ReplicationPolicyTest { @Test void contract(){ var p=new ReplicationPolicy(java.util.List.of("a","b","c"),2); org.junit.jupiter.api.Assertions.assertTrue(p.successful(java.util.Set.of("a","c"))); org.junit.jupiter.api.Assertions.assertFalse(p.successful(java.util.Set.of("a"))); } }
Teststrategie: Entscheidungstabellen prüfen Datenklassen, Regionen, Knotenausfälle und Degradationsreaktionen.
Gelöschte Datensätze verschwinden lokal, werden aber bei Replikation oder Offline-Synchronisation aus älteren Kopien wiederhergestellt. Ein Tombstone ist ein versionierter Löschmarker, der die Löschentscheidung replizierbar und zeitlich geordnet macht.
Erkennungsmerkmale
Hard Delete ohne verteilte Löschinformation
gelöschte Einträge tauchen nach Recovery erneut auf
Caches und Projektionen kennen keine Löschung
Sicherer Ablauf
Löschung als eigenes Ereignis beziehungsweise Marker modellieren
Identität, Version, Zeitpunkt und Grund aufnehmen
Leser so anpassen, dass Tombstones fachlich unsichtbar sind
Weitergabe an Replikate und Projektionen sicherstellen
Garbage-Collection erst nach bestätigter Verteilung ausführen
Wann sinnvoll - und wann nicht?
Sinnvoll bei replizierten, eventbasierten oder offlinefähigen Datensystemen. In einer lokalen, streng transaktionalen Tabelle kann Soft Delete genügen oder unnötig sein.
Risiken und Fehlanwendungen
Tombstones wachsen ohne Retention unbegrenzt. Zu frühes Entfernen kann gelöschte Daten wiederbeleben; Datenschutzanforderungen müssen mit technischer Replikation abgestimmt werden.
Enterprise-Java-Beispiel
Ein widerrufener API-Schlüssel bleibt als Tombstone erhalten, bis alle Regionen und Caches die Sperre verarbeitet haben.
Die Verteilung von Daten erfolgt durch spontane Hashes oder technische IDs, obwohl Zugriffsmuster und Hotspots fachlich variieren. Eine Shard Key Strategy kapselt die nachvollziehbare Zuordnung eines Datensatzes zu einer Partition und macht ihre Versionierung explizit.
Erkennungsmerkmale
einzelne Shards werden dauerhaft überlastet
zusammengehörige Daten liegen weit verteilt
Shard-Berechnung wird in vielen Repositories dupliziert
Sicherer Ablauf
Zugriffs-, Schreib- und Wachstumsprofile messen
Kandidaten nach Kardinalität, Stabilität und Lokalität bewerten
Strategie als reinen deterministischen Vertrag extrahieren
Version und Migrationspfad für Schlüsseländerungen vorsehen
Hot-Partition- und Rebalancing-Szenarien testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei horizontal partitionierten Datenbeständen. Ohne echte Skalierungs- oder Isolationsanforderung ist Sharding eine unnötige Betriebsbelastung.
Risiken und Fehlanwendungen
Ein falscher Schlüssel lässt sich später nur teuer ändern. Monotone Schlüssel, große Mandanten und globale Abfragen erzeugen Hotspots oder Scatter-Gather.
Enterprise-Java-Beispiel
Bestellungen werden nach TenantId und einem stabilen Bucket verteilt, damit sehr große Mandanten nicht eine einzelne Partition blockieren.
Abgrenzung und verwandte Muster
Sharding, Data Locality Policy, Partition Key Strategy, Rebalancing.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.nio.charset.StandardCharsets;
// Refactoring: Extract Shard Key Strategy
// Pattern: Strategy - Partitionierung wird deterministisch, testbar und austauschbar.
public final class ShardKeyStrategy {
private final int shardCount;
public ShardKeyStrategy(int shardCount){ if(shardCount < 1) throw new IllegalArgumentException("shardCount"); this.shardCount=shardCount; }
public int shardFor(String tenantId,String aggregateId){
if(tenantId==null||tenantId.isBlank()||aggregateId==null||aggregateId.isBlank()) throw new IllegalArgumentException("key");
int hash=java.util.Arrays.hashCode((tenantId+":"+aggregateId).getBytes(StandardCharsets.UTF_8));
return Math.floorMod(hash,shardCount);
}
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class ShardKeyStrategyTest { @Test void contract(){ var s=new ShardKeyStrategy(8); int a=s.shardFor("t1","o1"); org.junit.jupiter.api.Assertions.assertEquals(a,s.shardFor("t1","o1")); org.junit.jupiter.api.Assertions.assertTrue(a>=0&&a<8); } }
Teststrategie: Verteilungstests prüfen Determinismus, Balance, Datenlokalität und Verhalten bei neuen Strategieversionen.
Lesen aus mehreren Replikaten erkennt unterschiedliche Versionen, korrigiert veraltete Kopien jedoch nicht systematisch. Read Repair nutzt einen ohnehin erfolgenden Lesezugriff, um veraltete Replikate kontrolliert auf den gewählten Stand zu bringen.
Erkennungsmerkmale
stale reads bleiben dauerhaft bestehen
Reparaturjobs müssen komplette Bestände scannen
Leselogik wählt zwar neueste Version, verwirft aber Konfliktwissen
Sicherer Ablauf
Versionen oder kausale Metadaten jeder Antwort vergleichen
Antwort an den Client von der Reparatur entkoppeln
Reparatur idempotent und begrenzt ausführen
Fehler, Latenz und Reparaturquote messen
Wann sinnvoll - und wann nicht?
Sinnvoll in eventually-consistent replizierten Speichern mit Mehrfachlesen. Bei stark konsistenten Datenbanken ist es Aufgabe der Plattform.
Risiken und Fehlanwendungen
Read Repair erhöht Lesekosten und darf keine ungelösten Konflikte still überschreiben. Selten gelesene Daten benötigen zusätzlich Anti-Entropy-Prozesse.
Enterprise-Java-Beispiel
Ein Kundenprofil wird von drei Replikaten gelesen; zwei liefern Version 12, eines Version 10. Version 12 wird zurückgegeben und die alte Kopie asynchron aktualisiert.
Abgrenzung und verwandte Muster
Quorum Read, Vector Clock, Conflict Resolver, Anti-Entropy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Read Repair
// Pattern: Read Repair - Lesen erkennt und repariert veraltete Replikate.
public final class ReadRepairService {
public record ReplicaValue(String replica,long version,String value){ public ReplicaValue { if(replica==null||replica.isBlank()||version<0) throw new IllegalArgumentException(); } }
public record RepairPlan(ReplicaValue winner,List<String> staleReplicas){ public RepairPlan { staleReplicas=List.copyOf(staleReplicas); } }
public RepairPlan plan(List<ReplicaValue> values){
if(values==null||values.isEmpty()) throw new IllegalArgumentException("values");
var winner=values.stream().max(Comparator.comparingLong(ReplicaValue::version)).orElseThrow();
var stale=values.stream().filter(v->v.version()<winner.version()).map(ReplicaValue::replica).sorted().toList();
return new RepairPlan(winner,stale);
}
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class ReadRepairServiceTest { @Test void contract(){ var p=new ReadRepairService().plan(java.util.List.of(new ReadRepairService.ReplicaValue("a",1,"old"),new ReadRepairService.ReplicaValue("b",2,"new"))); org.junit.jupiter.api.Assertions.assertEquals("new",p.winner().value()); org.junit.jupiter.api.Assertions.assertEquals(java.util.List.of("a"),p.staleReplicas()); } }
Teststrategie: Tests simulieren veraltete, konkurrierende und nicht erreichbare Replikate sowie idempotente Wiederholung.
Wie viele Replikate Lesen oder Schreiben bestätigen müssen, ist als magische Zahl in Clients und Treibern verstreut. Eine Quorum Policy macht Replikationsfaktor, Lese- und Schreibquorum sowie Degradationsregeln als testbare Entscheidung sichtbar.
Erkennungsmerkmale
unterschiedliche R/W-Werte ohne Begründung
Verfügbarkeit und Konsistenz ändern sich unbemerkt
Timeouts werden mit fachlicher Ablehnung verwechselt
Sicherer Ablauf
Replikationsfaktor und zulässige Ausfälle erfassen
Konsistenzanforderung pro Operation klassifizieren
R- und W-Werte samt Bedingung R+W>N festlegen, wenn aktuelle Überschneidung benötigt wird
Timeout und Teilantwort als eigene Ergebnisse modellieren
Policy in Last- und Partitionstests validieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei Systemen, in denen die Anwendung Quorumparameter beeinflusst. Bei vollständig verwalteter starker Konsistenz bleibt dies Plattformverantwortung.
Risiken und Fehlanwendungen
Quorum garantiert nicht automatisch semantische Konfliktfreiheit. Hohe Quoren erhöhen Latenz und senken Verfügbarkeit während Partitionen.
Enterprise-Java-Beispiel
Kontostandsbuchungen verlangen W=2 von N=3; Produktkatalog-Lesen darf R=1 nutzen und akzeptiert kurze Verzögerung.
Einfache Versionsnummern oder Zeitstempel können nicht unterscheiden, ob zwei Änderungen kausal aufeinander folgen oder unabhängig entstanden sind. Eine Vector Clock hält pro beteiligtem Ursprung einen logischen Zähler und ermöglicht die Erkennung von Vorgänger-, Nachfolger- und nebenläufigen Versionen.
stabile Knoten- oder Writer-Identitäten definieren
Zähler beim lokalen Schreiben erhöhen
beim Empfang komponentenweise Maximum bilden
partielle Ordnung und Concurrent-Fall als API modellieren
Wachstum, Writer-Austritt und Kompaktierung regeln
Wann sinnvoll - und wann nicht?
Sinnvoll bei dezentralen Multi-Writer-Daten mit relevanter Kausalität. Für Single Writer oder zentrale Sequenzierung ist eine einfache Version leichter.
Risiken und Fehlanwendungen
Vektoren wachsen mit Writern und lösen Konflikte nicht selbst; sie erkennen sie nur. Dynamische Identitäten und Datenschutz können die Metadaten erschweren.
Enterprise-Java-Beispiel
Zwei offline arbeitende Außendienstgeräte ändern dieselbe Kundenakte. Die Vector Clock erkennt echte Nebenläufigkeit und übergibt beide Versionen an den Resolver.
Zusammenführung replizierter Daten basiert auf verzweigten Sonderfällen und ist nicht deterministisch oder reihenfolgeunabhängig. Eine klar definierte Merge-Strategie nutzt algebraische Eigenschaften wie Idempotenz, Kommutativität und Assoziativität, um konfliktfreie Zusammenführung zu ermöglichen.
Erkennungsmerkmale
Merge-Ergebnis hängt von Ankunftsreihenfolge ab
gleiches Ereignisset erzeugt verschiedene Zustände
manuelle Konfliktbereinigung bei additiven Daten
Sicherer Ablauf
Datentyp und erlaubte Fachoperationen analysieren
Merge-Invarianten ausdrücklich formulieren
geeignete monotone Repräsentation wählen
Merge als reine Funktion implementieren
Löschung und nicht monotone Regeln separat behandeln
Wann sinnvoll - und wann nicht?
Sinnvoll für Mengen, Zähler, Versionen oder andere Daten mit geeigneter Merge-Semantik. Nicht jede Fachentscheidung ist konfliktfrei automatisierbar.
Risiken und Fehlanwendungen
Ein technisch konfliktfreier Merge kann fachlich falsch sein. CRDT-artige Modelle erhöhen Metadaten und verlangen klare Lösch- beziehungsweise Kompaktierungsregeln.
Enterprise-Java-Beispiel
Tags eines Dokuments werden als add-wins Set zusammengeführt; eine Kreditentscheidung bleibt dagegen eskalationspflichtig.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Conflict-Free Merge
// Pattern: Grow-only CRDT - parallele Ergänzungen werden verlustfrei zusammengeführt.
public final class ConflictFreeTagSet {
private final Set<String> tags;
public ConflictFreeTagSet(Set<String> tags){ this.tags=Set.copyOf(tags); }
public ConflictFreeTagSet add(String tag){ if(tag==null||tag.isBlank()) throw new IllegalArgumentException("tag"); var copy=new HashSet<>(tags); copy.add(tag); return new ConflictFreeTagSet(copy); }
public ConflictFreeTagSet merge(ConflictFreeTagSet other){ var copy=new HashSet<>(tags); copy.addAll(other.tags); return new ConflictFreeTagSet(copy); }
public Set<String> values(){ return tags; }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class ConflictFreeTagSetTest { @Test void contract(){ var a=new ConflictFreeTagSet(java.util.Set.of()).add("vip"); var b=new ConflictFreeTagSet(java.util.Set.of()).add("risk"); var merged=a.merge(b); org.junit.jupiter.api.Assertions.assertEquals(java.util.Set.of("vip","risk"),merged.values()); org.junit.jupiter.api.Assertions.assertEquals(merged.values(),b.merge(a).values()); } }
Replikationsverzögerung wird nur als technische Metrik betrachtet, obwohl unterschiedliche Fachprozesse verschiedene Aktualitätsgrenzen besitzen. Ein Replication Lag Budget definiert pro Anwendungsfall maximal akzeptable Verzögerung und die Reaktion bei Überschreitung.
Erkennungsmerkmale
stale reads verursachen sporadische Fachfehler
alle Abfragen nutzen beliebige Replikate
kein klarer Schwellenwert für Routing oder Alarmierung
Sicherer Ablauf
fachliche Aktualitätsanforderung pro Query bestimmen
messbare Lag-Metrik und Zeitbezug festlegen
Budget als Value Object oder Policy modellieren
bei Überschreitung auf Primary routen, warten oder ablehnen
Budgetverletzungen getrennt überwachen
Wann sinnvoll - und wann nicht?
Sinnvoll bei asynchroner Replikation und read-after-write-Anforderungen. Bei starker Konsistenz ohne Replikatwahl ist es nicht erforderlich.
Risiken und Fehlanwendungen
Lag-Metriken können ungenau sein. Blindes Fallback auf Primary kann einen Ausfall verstärken; die Reaktion braucht Kapazitäts- und Fehlergrenzen.
Enterprise-Java-Beispiel
Die Auftragsübersicht akzeptiert 30 Sekunden Verzögerung; direkt nach einer Zahlung verlangt die Bestätigungsseite maximal zwei Sekunden und routet sonst zum Primary.
Bei Ausfall einer Instanz oder Region entscheiden Infrastrukturcode und Anwendung uneinheitlich über Umschaltung und Rückfall. Eine Failover Policy bewertet Gesundheit, Datenaktualität, Kapazität und Sperrzeiten und liefert eine explizite Umschaltentscheidung.
Erkennungsmerkmale
automatische Umschaltung ohne Datenstandprüfung
manuelle Runbooks widersprechen dem Code
Failover und Retry verstärken sich gegenseitig
Sicherer Ablauf
zulässige Ziele und Voraussetzungen definieren
Health-, Lag- und Quorumdaten in ein Decision-Objekt bündeln
Umschaltung, Abbruch und Degradation unterscheiden
Cooldown und Flapping-Schutz ergänzen
Failover und späteres Failback getrennt modellieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren aktiven oder bereiten Standorten. Bei Single-Instance-Systemen ist zunächst Redundanz statt Policy nötig.
Risiken und Fehlanwendungen
Automatisches Failover kann Split Brain oder Datenverlust auslösen. Gesundheit allein reicht nicht; Konsistenz und Kapazität müssen mitbewertet werden.
Enterprise-Java-Beispiel
Der Zahlungslesedienst wechselt nur auf Region B, wenn Health grün, Replikationslag unter fünf Sekunden und ausreichende Kapazität vorhanden sind.
Abgrenzung und verwandte Muster
Region Health Model, Leader Election, Failback Policy, Circuit Breaker.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Failover Policy
// Pattern: Policy - Failover-Entscheidungen werden deterministisch und zentral.
public record FailoverPolicy(Duration failureThreshold,Duration cooldown){
public FailoverPolicy { if(failureThreshold==null||cooldown==null||failureThreshold.isNegative()||cooldown.isNegative()) throw new IllegalArgumentException(); }
public boolean shouldFailover(Instant failedSince,boolean targetHealthy,Instant lastFailover,Instant now){
if(failedSince==null||now==null) return false;
boolean failedLongEnough=!now.isBefore(failedSince.plus(failureThreshold));
boolean cooledDown=lastFailover==null||!now.isBefore(lastFailover.plus(cooldown));
return failedLongEnough&&targetHealthy&&cooledDown;
}
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class FailoverPolicyTest { @Test void contract(){ var p=new FailoverPolicy(java.time.Duration.ofSeconds(10),java.time.Duration.ofMinutes(1)); var now=java.time.Instant.parse("2026-01-01T00:02:00Z"); org.junit.jupiter.api.Assertions.assertTrue(p.shouldFailover(now.minusSeconds(20),true,now.minusSeconds(70),now)); org.junit.jupiter.api.Assertions.assertFalse(p.shouldFailover(now.minusSeconds(5),true,null,now)); } }
Teststrategie: Game-Day-Szenarien prüfen Ausfall, Teilstörung, Flapping, veraltete Replikate und Rückkehr des Primärstandorts.
Mehrere Knoten führen gleichzeitig eine Aufgabe aus oder ein alter Leader arbeitet nach Netzwerkverlust weiter. Ein Leader Lease gewährt Führung nur bis zu einem klaren Ablaufzeitpunkt und verlangt regelmäßige, atomare Erneuerung.
Erkennungsmerkmale
doppelte Scheduler-Ausführung
Lock ohne Ablauf blockiert dauerhaft
Leader besitzt keinen nachweisbaren Gültigkeitszeitraum
Sicherer Ablauf
Lease-ID, Besitzer, Gültigkeitsende und Fencing Token definieren
Acquire und Renew atomar umsetzen
jede geschützte Schreiboperation mit Fencing Token absichern
bei abgelaufenem Lease sofort stoppen
Uhrdrift und Netzwerkpartitionen testen
Wann sinnvoll - und wann nicht?
Sinnvoll für genau-einen aktiven Koordinator, Scheduler oder Partitionseigentümer. Bei idempotenten parallelisierbaren Arbeiten sind Competing Consumers einfacher.
Risiken und Fehlanwendungen
Ein Lease ohne Fencing verhindert alte Leader nicht sicher. Lokale Uhren, lange Pausen und verzögerte Nachrichten müssen berücksichtigt werden.
Enterprise-Java-Beispiel
Nur ein Knoten erzeugt Abrechnungsbatches. Jeder Schreibzugriff trägt ein monoton steigendes Fencing Token, sodass ein früherer Leader abgewiesen wird.
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Leader Lease
// Pattern: Lease - Führungsrecht ist zeitlich begrenzt und gegen alte Epochen geschützt.
public record LeaderLease(String owner,long epoch,Instant validUntil){
public LeaderLease { if(owner==null||owner.isBlank()||epoch<1||validUntil==null) throw new IllegalArgumentException(); }
public boolean permits(String node,long observedEpoch,Instant now){ return owner.equals(node)&&epoch==observedEpoch&&now.isBefore(validUntil); }
public LeaderLease renew(String node,Instant newUntil){ if(!owner.equals(node)||!newUntil.isAfter(validUntil)) throw new IllegalStateException("invalid renewal"); return new LeaderLease(owner,epoch,newUntil); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class LeaderLeaseTest { @Test void contract(){ var l=new LeaderLease("n1",4,java.time.Instant.parse("2026-01-01T00:01:00Z")); org.junit.jupiter.api.Assertions.assertTrue(l.permits("n1",4,java.time.Instant.parse("2026-01-01T00:00:30Z"))); org.junit.jupiter.api.Assertions.assertFalse(l.permits("n1",3,java.time.Instant.parse("2026-01-01T00:00:30Z"))); } }
Teststrategie: Tests simulieren Ablauf, verspätete Erneuerung, zwei Bewerber, Uhrsprung und alten Fencing Token.
Daten und Verarbeitung werden ohne Rücksicht auf Latenz, Mandantenzuordnung, Rechtsraum oder Kosten verteilt. Eine Data Locality Policy entscheidet nachvollziehbar, wo Daten gespeichert, gelesen und verarbeitet werden dürfen beziehungsweise sollen.
Erkennungsmerkmale
häufige Cross-Region-Abfragen
personenbezogene Daten verlassen ungewollt eine Region
Rechenjobs laufen weit entfernt von ihren Daten
Sicherer Ablauf
Datenklassifikation, Mandant, Rechtsraum und Zugriffsmuster erfassen
harte Verbote von Optimierungspräferenzen trennen
zulässige Regionen und bevorzugte Verarbeitung modellieren
Routing und Platzierung hinter Ports anwenden
Verstöße und Ausnahmen auditieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei Multi-Region-, Multi-Tenant- oder regulierten Systemen. In einer einzigen Region kann die Policy zunächst einfach bleiben.
Risiken und Fehlanwendungen
Lokalität kann Verfügbarkeit und globale Performance einschränken. Caches, Backups, Logs und Supportzugriffe müssen ebenfalls erfasst werden.
Enterprise-Java-Beispiel
Gesundheitsdaten eines österreichischen Mandanten bleiben in einer EU-Region; anonymisierte Aggregationen dürfen zentral verarbeitet werden.
Abgrenzung und verwandte Muster
Data Sovereignty, Shard Key Strategy, Geo Replication, Routing Policy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Data Locality Policy
// Pattern: Policy - Residenz- und Verarbeitungsregeln werden als Fachgrenze modelliert.
public final class DataLocalityPolicy {
private final Map<String,Set<String>> allowedRegionsByClassification;
public DataLocalityPolicy(Map<String,Set<String>> rules){ var copy=new HashMap<String,Set<String>>(); rules.forEach((k,v)->copy.put(k,Set.copyOf(v))); this.allowedRegionsByClassification=Map.copyOf(copy); }
public boolean permits(String classification,String storageRegion,String processingRegion){ var allowed=allowedRegionsByClassification.getOrDefault(classification,Set.of()); return allowed.contains(storageRegion)&&allowed.contains(processingRegion); }
public void requireAllowed(String classification,String storageRegion,String processingRegion){ if(!permits(classification,storageRegion,processingRegion)) throw new SecurityException("data locality violation"); }
}
Vorhandener JUnit-Vertrag
Vorhandener JUnit-Vertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class DataLocalityPolicyTest { @Test void contract(){ var p=new DataLocalityPolicy(java.util.Map.of("PERSONAL",java.util.Set.of("eu-central","eu-west"))); org.junit.jupiter.api.Assertions.assertTrue(p.permits("PERSONAL","eu-central","eu-west")); org.junit.jupiter.api.Assertions.assertThrows(SecurityException.class,()->p.requireAllowed("PERSONAL","eu-central","us-east")); } }
Teststrategie: Entscheidungstabellen prüfen Datenklasse, Mandant, Region, Notfallausnahme und unzulässige Ziele.
Clients erwarten implizit „aktuelle“ Daten, obwohl der Speicher verschiedene Konsistenzmodelle und Kosten anbietet. Ein typisiertes Consistency Level macht die gewünschte Sichtbarkeit und Reihenfolge pro Anwendungsfall explizit und vom Datenbanktreiber unabhängig.
Erkennungsmerkmale
read-after-write funktioniert nur manchmal
jede Abfrage nutzt dieselbe teure Konsistenz
Begriffe wie strong oder eventual bleiben Treiberstrings
Sicherer Ablauf
fachliche Lesegarantie pro Use Case bestimmen
unterstützte Stufen als Domänentyp modellieren
Mapping zum konkreten Datenspeicher in Adapter verschieben
nicht erfüllbare Anforderungen explizit ablehnen
Latenz, Verfügbarkeit und Kosten je Stufe messen
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Datenspeichern mit wählbarer Konsistenz. In einer lokalen ACID-Datenbank darf kein künstlicher Abstraktionsapparat entstehen.
Risiken und Fehlanwendungen
Bezeichnungen unterscheiden sich zwischen Produkten. Stärkere Konsistenz kann Latenz und Verfügbarkeit reduzieren; schwächere verlangt sichtbare Fachsemantik.
Enterprise-Java-Beispiel
Produktlisten verwenden eventual consistency, der eigene Warenkorb session consistency und eine unmittelbar folgende Zahlungsprüfung eine starke beziehungsweise primäre Lesegarantie.
Abgrenzung und verwandte Muster
CAP, Replication Lag Budget, Read Preference, Quorum Policy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Consistency Level
// Pattern: Explicit Policy Value - Konsistenz wird Teil des Aufrufvertrags.
public enum ConsistencyLevel {
ONE, QUORUM, ALL;
public int requiredAcknowledgements(int replicas){
if(replicas<1) throw new IllegalArgumentException("replicas");
return switch(this){ case ONE -> 1; case QUORUM -> replicas/2+1; case ALL -> replicas; };
}
public boolean satisfied(int acknowledgements,int replicas){ return acknowledgements>=requiredAcknowledgements(replicas); }
}
Replikationsziele, Mindestkopien und Schreibpräferenzen sind über Infrastrukturdateien, Datenbankadapter und Betriebswissen verteilt. Dadurch bleibt unklar, welche Daten in welcher Region liegen müssen und welche Ausfallannahmen tatsächlich gelten.
Erkennungsmerkmale
Regionen werden als freie Strings in mehreren Komponenten verwendet
kritische und unkritische Daten teilen dieselben Replikationsregeln
Failover- und Datenresidenzannahmen sind nur in Runbooks dokumentiert
Sicherer Ablauf
Datenklassen, Schutzbedarf und zulässige Regionen erfassen
harte Residenzregeln von Verfügbarkeitspräferenzen trennen
Replikationsziele und Mindestbestätigungen als unveränderliche Policy modellieren
Policy hinter einem Port mit der Plattformkonfiguration verbinden
Regionsausfall, verzögerte Replikation und unzulässige Zielregionen testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Multi-Region-Betrieb, regulierten Daten und expliziten Wiederanlaufzielen. Für eine kleine Einzelregionsanwendung genügt meist eine klar dokumentierte Plattformkonfiguration.
Risiken und Fehlanwendungen
Eine Policy erzeugt keine physische Replikation. Sie muss mit tatsächlichen Plattformfähigkeiten, Verschlüsselung, Backups und Betriebsprozessen übereinstimmen.
Enterprise-Java-Beispiel
Ein Zahlungsjournal verlangt zwei bestätigte Kopien innerhalb der EU; Produktbilder dürfen asynchron in weitere Regionen repliziert werden.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Geo Replication Policy
// Pattern: Policy - regionale Replikationsregeln werden explizit und testbar.
public record GeoReplicationPolicy(Set<String> regions,int minimumCopies,boolean preferLocalWrite){
public GeoReplicationPolicy { regions=Set.copyOf(regions); if(regions.isEmpty()||minimumCopies<1||minimumCopies>regions.size()) throw new IllegalArgumentException(); }
public boolean durable(Set<String> acknowledgedRegions){ return acknowledgedRegions!=null && regions.containsAll(acknowledgedRegions) && acknowledgedRegions.size()>=minimumCopies; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Lesezugriffe wählen Primär- oder Replikaknoten implizit. Fachlich unterschiedliche Anforderungen an Aktualität, Latenz und Ausfallsicherheit werden dadurch vermischt.
Erkennungsmerkmale
jeder Adapter entscheidet selbst zwischen Primary und Replica
stale reads werden erst im Produktionsbetrieb bemerkt
Read-after-write-Anforderungen sind nicht explizit testbar
Sicherer Ablauf
Leseanwendungsfälle nach Aktualitätsanforderung klassifizieren
starke, gebundene und eventual konsistente Reads benennen
Read Preference als typisierten Vertrag einführen
Routingadapter auf diese Präferenz abbilden
Replikationsverzug und Fallback-Verhalten in Vertragstests simulieren
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn mehrere Replikate existieren und nicht jeder Lesevorgang dieselbe Aktualität benötigt. Ohne Replikation wäre die zusätzliche Abstraktion nur Ballast.
Risiken und Fehlanwendungen
Eine zu aggressive Replica-Präferenz kann fachlich falsche Entscheidungen erzeugen. Für schreibnahe Prozesse müssen Session- oder Read-after-write-Garantien ausdrücklich berücksichtigt werden.
Enterprise-Java-Beispiel
Die Produktübersicht liest aus der nächstgelegenen Replica; die Bestellbestätigung liest nach dem Schreiben vom Primary.
Abgrenzung und verwandte Muster
Consistency Level, Replication Lag Budget, Query Port, Data Locality Policy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Read Preference
// Pattern: Strategy - die Wahl eines Lesereplikats wird austauschbar.
public final class ReadPreference {
public enum Mode { LOCAL, FRESHEST, ANY }
public record Replica(String id,String region,long version,boolean healthy){}
private final Mode mode; private final String localRegion;
public ReadPreference(Mode mode,String localRegion){ this.mode=Objects.requireNonNull(mode); this.localRegion=localRegion; }
public Replica choose(List<Replica> replicas){ var healthy=replicas.stream().filter(Replica::healthy); return switch(mode){ case LOCAL -> healthy.filter(r->Objects.equals(r.region(),localRegion)).max(Comparator.comparingLong(Replica::version)).orElseThrow(); case FRESHEST -> healthy.max(Comparator.comparingLong(Replica::version)).orElseThrow(); case ANY -> healthy.findFirst().orElseThrow(); }; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Schreibziele werden durch technische Standortabfragen oder hart codierte Regionstabellen bestimmt. Damit sind Ownership, Datenresidenz und Failover nicht als fachliche Entscheidung sichtbar.
Erkennungsmerkmale
mehrere Services enthalten eigene Region-if-Ketten
Writes werden bei Störungen manuell umgeleitet
Mandanten- oder Datenklassifikation beeinflusst Routing ohne zentralen Vertrag
Sicherer Ablauf
Schreib-Ownership und zulässige Zielregionen erheben
Normalbetrieb und Notfallrouting getrennt modellieren
deterministische Routingentscheidung als Policy extrahieren
Fencing- und Idempotenzanforderungen ergänzen
Split-Brain-, Regionsausfall- und Failback-Szenarien testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei aktiv-aktiv, aktiv-passiv oder mandantenbezogener Regionalisierung. Bei genau einem Schreibstandort reicht ein einfacher Adapter.
Risiken und Fehlanwendungen
Schreibumleitung ohne Fencing kann Doppelverarbeitung und widersprüchliche Daten erzeugen. Routing darf nicht allein auf Netzwerk-Erreichbarkeit beruhen.
Enterprise-Java-Beispiel
Vertragsänderungen werden in der Heimatregion des Mandanten geschrieben; bei freigegebenem Notfallmodus übernimmt eine vorbereitete Sekundärregion.
Abgrenzung und verwandte Muster
Leader Lease, Idempotency Key, Failover Policy, Data Sovereignty Policy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Write Routing Policy
// Pattern: Policy - Schreibautorität und Fallback werden explizit.
public record WriteRoutingPolicy(Map<String,String> tenantPrimaryRegion,List<String> fallbackOrder){
public WriteRoutingPolicy { tenantPrimaryRegion=Map.copyOf(tenantPrimaryRegion); fallbackOrder=List.copyOf(fallbackOrder); }
public String route(String tenant,Set<String> healthy){ String primary=tenantPrimaryRegion.get(tenant); if(primary!=null&&healthy.contains(primary)) return primary; return fallbackOrder.stream().filter(healthy::contains).findFirst().orElseThrow(); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Regionen gelten nur als erreichbar oder nicht erreichbar. Degradierte Datenbanken, hohe Replikationsverzögerung oder teilweise abhängige Dienste werden dadurch nicht angemessen bewertet.
Erkennungsmerkmale
Healthchecks liefern nur boolean
Failover wird wegen einzelner transienter Fehler ausgelöst
Anwendungs- und Datenebene melden widersprüchliche Zustände
Sicherer Ablauf
relevante Gesundheitsdimensionen definieren
Messwerte in Healthy, Degraded, Unavailable und Unknown übersetzen
Zeitfenster und Mindestbeobachtungen modellieren
Entscheidungen von einzelnen Rohmetriken entkoppeln
Flapping, Teilstörung und veraltete Messwerte testen
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn Routing oder Failover von mehreren Signalen abhängt. Für rein lokale Prozesse genügt ein einfacher Readiness-Check.
Risiken und Fehlanwendungen
Ein zu komplexes Modell kann Störungen verschleiern. Rohsignale, Alter der Messung und Entscheidungsgrund müssen beobachtbar bleiben.
Enterprise-Java-Beispiel
Eine Region ist erreichbar, aber wegen hoher Datenbanklatenz nur für statische Lesezugriffe freigegeben.
Abgrenzung und verwandte Muster
Health Probe Port, Consistency Monitor, Failover Policy, SLO Evaluation.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Introduce Region Health Model
// Pattern: Decision Object - mehrere Signale ergeben einen nachvollziehbaren Zustand.
public record RegionHealthModel(Duration latency,double errorRate,Duration replicationLag){
public enum Status { HEALTHY, DEGRADED, UNAVAILABLE }
public RegionHealthModel { if(latency.isNegative()||replicationLag.isNegative()||errorRate<0||errorRate>1) throw new IllegalArgumentException(); }
public Status status(){ if(errorRate>=0.5||latency.compareTo(Duration.ofSeconds(5))>0) return Status.UNAVAILABLE; if(errorRate>=0.1||latency.compareTo(Duration.ofSeconds(1))>0||replicationLag.compareTo(Duration.ofSeconds(3))>0) return Status.DEGRADED; return Status.HEALTHY; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Nach einem Failover wird die Rückkehr zur Primärregion ad hoc durchgeführt. Datenabgleich, Mindeststabilität und Verkehrsverlagerung sind nicht als überprüfbarer Ablauf modelliert.
Erkennungsmerkmale
Failback erfolgt per manueller Checkliste ohne Entscheidungskriterien
Verkehr wird sofort vollständig zurückgeschaltet
Datenkonflikte werden erst nach der Rückkehr sichtbar
Sicherer Ablauf
Vorbedingungen für Datenabgleich und Stabilität definieren
Mindestbeobachtungszeit und Freigaben modellieren
Traffic schrittweise statt abrupt zurückführen
Rollback des Failbacks vorsehen
Störung während Synchronisation und Teilrückkehr testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei vorbereitetem Multi-Region-Failover. Ohne sekundären Betriebsstandort existiert kein eigenständiger Failback-Prozess.
Risiken und Fehlanwendungen
Zu früher Failback erzeugt einen zweiten Ausfall; zu später Failback verlängert teuren oder eingeschränkten Notbetrieb.
Enterprise-Java-Beispiel
Nach 30 Minuten stabiler Primärregion und bestätigtem Replikationsgleichstand werden zunächst interne Nutzer und danach Kundentraffic zurückgeführt.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Failback Policy
// Pattern: Policy - Rückschaltung wird stabilitäts- und zeitgesteuert.
public record FailbackPolicy(Duration stableFor,Duration cooldown){
public FailbackPolicy { if(stableFor.isNegative()||cooldown.isNegative()) throw new IllegalArgumentException(); }
public boolean allowed(Instant now,Instant healthySince,Instant lastSwitch,boolean approved){ return approved && !healthySince.plus(stableFor).isAfter(now) && !lastSwitch.plus(cooldown).isAfter(now); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Wiederanlaufwissen liegt in verstreuten Runbooks, Tickets und Köpfen. Abhängigkeiten, Reihenfolge, Verantwortlichkeiten sowie RTO und RPO sind nicht als ausführbarer Plan sichtbar.
Erkennungsmerkmale
Notfallübungen entdecken immer neue manuelle Schritte
RTO/RPO werden genannt, aber nicht gemessen
Wiederherstellung von Daten und Anwendung wird getrennt geplant
Sicherer Ablauf
kritische Geschäftsfähigkeiten und Abhängigkeiten erfassen
RTO, RPO und zulässige Degradationen pro Fähigkeit definieren
Wiederanlaufschritte als Zustände und Commands modellieren
manuelle Freigaben und Automatisierung klar trennen
regelmäßige Übungen mit gemessenen Zeiten durchführen
Wann sinnvoll - und wann nicht?
Sinnvoll für geschäftskritische Systeme und regulatorisch verlangte Wiederanlaufverfahren. Der Plan muss zur tatsächlichen Kritikalität passen.
Risiken und Fehlanwendungen
Ein Dokument ohne Übungen ist keine belastbare Wiederherstellungsfähigkeit. Abhängigkeiten wie Identität, DNS, Schlüssel und externe Partner dürfen nicht fehlen.
Enterprise-Java-Beispiel
Der Plan stellt zuerst Identität und Schlüsselzugriff, danach Datenbank, Messaging, Kern-API und zuletzt nichtkritische Reports wieder her.
Abgrenzung und verwandte Muster
Restore Point, Backup Retention Policy, Region Health Model, Recovery Scenario Harness.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*; import java.util.*;
// Refactoring: Introduce Disaster Recovery Plan
// Pattern: Plan Object - RTO, RPO und Schritte bilden einen prüfbaren Vertrag.
public record DisasterRecoveryPlan(Duration rto,Duration rpo,List<String> recoverySteps){
public DisasterRecoveryPlan { recoverySteps=List.copyOf(recoverySteps); if(rto.isNegative()||rpo.isNegative()||recoverySteps.isEmpty()) throw new IllegalArgumentException(); }
public boolean meets(Duration outage,Duration dataLoss){ return outage.compareTo(rto)<=0&&dataLoss.compareTo(rpo)<=0; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Datenresidenz, Zugriffsort und zulässige Verarbeitung werden als technische Regionseinstellungen behandelt. Fachliche Klassifikation und Rechtsraum bleiben unsichtbar.
Erkennungsmerkmale
personenbezogene Daten werden in generische globale Speicher geschrieben
Backups und Logs folgen anderen Regionen als Primärdaten
Ausnahmen werden informell genehmigt
Sicherer Ablauf
Datenklassen und Rechtsräume explizit erfassen
Speicherung, Verarbeitung, Supportzugriff und Backup getrennt bewerten
harte Verbote als Policy modellieren
Entscheidung in Routing- und Speicherports erzwingen
Ausnahmen zeitlich begrenzen und auditieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei personenbezogenen, behördlichen oder vertraglich regional gebundenen Daten. Für nicht regulierte öffentliche Daten kann eine einfache Lokalitätspräferenz reichen.
Risiken und Fehlanwendungen
Nur den Primärspeicher zu prüfen ist unzureichend. Caches, Telemetrie, Suchindizes, Backups und Supportwerkzeuge müssen dieselbe Policy beachten.
Enterprise-Java-Beispiel
Schadendokumente österreichischer Kunden bleiben in EU-Regionen; anonymisierte Kennzahlen dürfen zentral aggregiert werden.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Data Sovereignty Policy
// Pattern: Policy - Datenresidenz wird als fachlich-rechtliche Regel modelliert.
public final class DataSovereigntyPolicy {
public enum Classification { PUBLIC, INTERNAL, PERSONAL, RESTRICTED }
private final Map<Classification,Set<String>> allowed;
public DataSovereigntyPolicy(Map<Classification,Set<String>> allowed){ var copy=new EnumMap<Classification,Set<String>>(Classification.class); allowed.forEach((k,v)->copy.put(k,Set.copyOf(v))); this.allowed=Map.copyOf(copy); }
public boolean permits(Classification c,String region,boolean legalBasis){ return allowed.getOrDefault(c,Set.of()).contains(region) && (c.ordinal()<Classification.PERSONAL.ordinal()||legalBasis); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Aufbewahrungsfristen und Backupklassen sind als Zahlen in Skripten verteilt. Fachliche Wiederherstellungsziele, rechtliche Pflichten und Löschanforderungen werden vermischt.
Erkennungsmerkmale
alle Daten erhalten dieselbe Retention
Backups überleben Lösch- oder Mandantenregeln unbegrenzt
niemand kann erklären, welche Sicherung für welches Szenario gedacht ist
Sicherer Ablauf
Backupklassen nach Wiederherstellungszweck definieren
Fristen, Unveränderlichkeit und geografische Ablage modellieren
Retention von operativer Datenhaltung trennen
automatische Ablauf- und Löschentscheidungen einführen
Wiederherstellung aus jeder relevanten Backupklasse testen
Wann sinnvoll - und wann nicht?
Sinnvoll, sobald mehrere Datenklassen oder Wiederherstellungspunkte existieren. Ein einfaches Entwicklungssystem benötigt keine umfangreiche Policy.
Risiken und Fehlanwendungen
Lange Retention erhöht Kosten und Datenschutzrisiken; kurze Retention kann regulatorische und betriebliche Anforderungen verletzen.
Enterprise-Java-Beispiel
Transaktionsjournale werden sieben Jahre unveränderlich aufbewahrt, temporäre Importdateien nur 30 Tage.
Abgrenzung und verwandte Muster
Restore Point, Data Retention Decision, Legal Hold, Disaster Recovery Plan.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Backup Retention Policy
// Pattern: Policy - Aufbewahrung und Löschschutz werden deterministisch.
public record BackupRetentionPolicy(int dailyDays,int monthlyMonths){
public BackupRetentionPolicy { if(dailyDays<1||monthlyMonths<1) throw new IllegalArgumentException(); }
public boolean retain(LocalDate backupDate,LocalDate today,boolean monthly,boolean legalHold){ if(legalHold) return true; return monthly ? !backupDate.plusMonths(monthlyMonths).isBefore(today) : !backupDate.plusDays(dailyDays).isBefore(today); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Wiederherstellung wird über Dateinamen, Zeitstempel und Provider-IDs gesteuert. Gültigkeit, Datenstand und Abhängigkeiten eines Wiederherstellungspunkts sind nicht typisiert.
Erkennungsmerkmale
Restore-Skripte akzeptieren beliebige Strings
Anwendungs- und Datenbanksnapshots passen zeitlich nicht zusammen
RPO wird nur geschätzt
Sicherer Ablauf
Identität, Zeitpunkt, Datenquellen und Vollständigkeit modellieren
Restore Point als unveränderliches Value Object einführen
Kompatibilität abhängiger Komponenten prüfen
Auswahl und Freigabe protokollieren
Restore in isolierter Umgebung regelmäßig verifizieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei zeitpunktbezogenen Backups, Snapshots oder Event-Store-Wiederherstellung. Ohne mehrere Wiederherstellungsstände genügt ein einfacher Provider-Aufruf.
Risiken und Fehlanwendungen
Ein vorhandener Snapshot ist nicht automatisch wiederherstellbar. Schlüssel, Schema, externe Dateien und Event-Offsets müssen zusammenpassen.
Enterprise-Java-Beispiel
Ein Restore Point bündelt Datenbank-LSN, Object-Storage-Snapshot, Event-Offset und Schema-Version für einen konsistenten Wiederanlauf.
package com.aydinsude.workbench.catalog;
import java.time.*; import java.util.*;
// Refactoring: Introduce Restore Point
// Pattern: Value Object - ein Wiederherstellungspunkt wird unveränderlich und eindeutig.
public record RestorePoint(String snapshotId,long logPosition,Instant createdAt){
public RestorePoint { if(snapshotId==null||snapshotId.isBlank()||logPosition<0||createdAt==null) throw new IllegalArgumentException(); }
public boolean before(RestorePoint other){ return logPosition<other.logPosition; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Replikationsverzug und Datenabweichungen werden nur durch Infrastrukturmetriken beobachtet. Fachliche Konsistenzverletzungen bleiben bis zu Kundenmeldungen unentdeckt.
Erkennungsmerkmale
nur technische Lag-Metriken vorhanden
Checksummen oder fachliche Summen unterscheiden sich unbemerkt
Alarmierung kennt keine Kritikalität der betroffenen Daten
Stichproben, Checksummen und Versionsstände kombinieren
Toleranzfenster je Datenklasse modellieren
Fehlalarm, echte Divergenz und Reconciliation testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei asynchroner Replikation, CDC und Materialized Views. Bei synchroner Einzelinstanz ist ein separater Monitor meist unnötig.
Risiken und Fehlanwendungen
Vollständige Vergleiche können teuer sein. Stichproben und Hashes müssen so gewählt werden, dass kritische Abweichungen nicht systematisch verborgen bleiben.
Enterprise-Java-Beispiel
Der Monitor vergleicht Anzahl und Gesamtsumme gebuchter Zahlungen pro Zeitraum zwischen Primary und Reporting-Region.
Abgrenzung und verwandte Muster
Replication Lag Budget, Conflict Resolver, Read Repair, Configuration Drift Detector.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Consistency Monitor
// Pattern: Monitor - Replikatabweichungen werden als expliziter Bericht sichtbar.
public final class ConsistencyMonitor {
public record Sample(String region,long version,String checksum){}
public record Report(long highestVersion,Set<String> staleRegions,boolean checksumConflict){ public Report { staleRegions=Set.copyOf(staleRegions); } }
public Report inspect(List<Sample> samples){ if(samples==null||samples.isEmpty()) throw new IllegalArgumentException(); long max=samples.stream().mapToLong(Sample::version).max().orElseThrow(); var stale=samples.stream().filter(s->s.version()<max).map(Sample::region).collect(java.util.stream.Collectors.toSet()); boolean conflict=samples.stream().filter(s->s.version()==max).map(Sample::checksum).distinct().count()>1; return new Report(max,stale,conflict); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Routinggewichte, Wartungszustände und Zielgruppen werden direkt in Gatewayregeln verändert. Die geschäftliche Absicht einer Verkehrsverlagerung ist nicht testbar.
Erkennungsmerkmale
Gewichte werden manuell ohne Versionierung geändert
interne, Beta- und Standardnutzer teilen dieselbe Routinglogik
Notfallrouting ist mit Rolloutlogik vermischt
Sicherer Ablauf
Zielgruppen, Ziele und Gewichte als Domainmodell erfassen
Normalbetrieb, Wartung und Notfall getrennt modellieren
deterministische Policy extrahieren
Änderungen versionieren und auditieren
Gewichtsgrenzen, leere Ziele und Rollback testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren aktiven Versionen, Regionen oder Kapazitätspools. Bei einem einzigen Ziel ist keine Shaping-Policy erforderlich.
Risiken und Fehlanwendungen
Fehlerhafte Gewichte können Überlast oder vollständigen Ausfall erzeugen. Die Summe, Mindestkapazität und Health-Signale müssen validiert werden.
Enterprise-Java-Beispiel
Zahlungsverkehr wird zu 90 Prozent auf Version A und zu 10 Prozent auf eine geprüfte Version B verteilt.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Traffic Shaping Policy
// Pattern: Policy - Routinggewichte und Wartungsregeln werden explizit und testbar.
public record TrafficShapingPolicy(Map<String,Integer> weights, Set<String> disabledTargets) {
public TrafficShapingPolicy { weights=Map.copyOf(weights); disabledTargets=Set.copyOf(disabledTargets); if(weights.values().stream().anyMatch(v->v<0)) throw new IllegalArgumentException(); }
public String route(int bucket){ var active=weights.entrySet().stream().filter(e->!disabledTargets.contains(e.getKey())&&e.getValue()>0).sorted(Map.Entry.comparingByKey()).toList(); int total=active.stream().mapToInt(Map.Entry::getValue).sum(); if(total==0) throw new IllegalStateException("no target"); int point=Math.floorMod(bucket,total); int sum=0; for(var e:active){ sum+=e.getValue(); if(point<sum) return e.getKey(); } throw new IllegalStateException(); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Canary-Nutzer werden durch zufällige Prozentwerte oder uneinheitliche Header ausgewählt. Dieselben Nutzer landen dadurch nicht stabil auf derselben Version.
Erkennungsmerkmale
Fehler lassen sich nicht einer stabilen Kohorte zuordnen
Nutzer wechseln zwischen alter und neuer Version
interne Tester und echte Kunden sind nicht getrennt
Sicherer Ablauf
Kohortenkriterien und Ausschlüsse definieren
stabile Hash- oder Attributzuordnung einführen
Kohortenversion und Zeitraum modellieren
Beobachtbarkeit nach Kohorte ergänzen
Stabilität, Datenschutz und Rücknahme testen
Wann sinnvoll - und wann nicht?
Sinnvoll für kontrollierte progressive Releases. Bei rein internen Änderungen ohne Nutzerwirkung reicht eine technische Staging-Umgebung.
Risiken und Fehlanwendungen
Eine nicht repräsentative Kohorte liefert falsche Sicherheit. Sensible Attribute dürfen nicht unnötig für Routing verwendet werden.
Enterprise-Java-Beispiel
Zunächst interne Konten, danach ein stabil gehashter Prozentanteil geeigneter Geschäftskunden erhalten die neue Preisberechnung.
package com.aydinsude.workbench.catalog;
import java.util.Objects;
// Refactoring: Introduce Canary Cohort
// Pattern: Value Object - eine stabile Rollout-Kohorte ersetzt flüchtige Zufallsentscheidungen.
public record CanaryCohort(int percentage, String salt) {
public CanaryCohort { if(percentage<0||percentage>100) throw new IllegalArgumentException(); Objects.requireNonNull(salt); }
public boolean contains(String subject){ Objects.requireNonNull(subject); return Math.floorMod(Objects.hash(salt,subject),100)<percentage; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Rollouts werden anhand einzelner Dashboards oder manueller Einschätzung fortgesetzt. Abbruchkriterien und Mindestdaten sind nicht automatisiert.
Erkennungsmerkmale
jedes Team nutzt andere Grenzwerte
Rollout wird trotz fehlender Messdaten fortgesetzt
Rollback erfolgt erst nach deutlicher Kundenauswirkung
Sicherer Ablauf
erforderliche Signale und Mindeststichprobe festlegen
harte Stopkriterien von Warnungen trennen
Guard als reinen Decision Service extrahieren
Entscheidungsbegründung protokollieren
fehlende Daten, Grenzwertnähe und Rücknahme testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei gestuften Releases mit messbaren Qualitäts- oder Geschäftsindikatoren. Für kleine, sofort reversible Änderungen kann eine einfache Checkliste genügen.
Risiken und Fehlanwendungen
Ein Guard ist nur so gut wie seine Signale. Falsch gewählte Metriken können sichere Releases blockieren oder gefährliche freigeben.
Enterprise-Java-Beispiel
Der Rollout stoppt, wenn Fehlerrate, p95-Latenz oder fachliche Ablehnungen gegenüber der Kontrollgruppe deutlich steigen.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Rollout Guard
// Pattern: Decision Object - Rollout-Freigabe und Abbruchgrund werden gemeinsam geliefert.
public record RolloutGuard(double maxErrorRate,long maxP95Millis,long minimumSamples){
public record Metrics(long samples,long failures,long p95Millis){}
public record Decision(boolean continueRollout,String reason){}
public Decision evaluate(Metrics m){ if(m.samples()<minimumSamples) return new Decision(false,"insufficient-samples"); double rate=m.samples()==0?1d:(double)m.failures()/m.samples(); if(rate>maxErrorRate) return new Decision(false,"error-rate"); if(m.p95Millis()>maxP95Millis) return new Decision(false,"latency"); return new Decision(true,"healthy"); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Verfügbarkeit wird als abstraktes Ziel genannt, ohne zu bestimmen, wie viel Unzuverlässigkeit im Zeitraum toleriert wird. Produktentwicklung und Stabilitätsarbeit konkurrieren ohne gemeinsame Größe.
Erkennungsmerkmale
SLO-Verletzungen haben keine Auswirkung auf Releaseentscheidungen
Teams diskutieren über einzelne Ausfälle statt über Zeitfenster
jede Störung wird gleich gewichtet
Sicherer Ablauf
SLI und SLO für eine Nutzerfähigkeit definieren
zulässige Fehlerereignisse oder Ausfallzeit berechnen
Verbrauch und Restbudget als Value Object modellieren
Entscheidungsregeln für Releases und Stabilitätsarbeit ableiten
Fensterwechsel, fehlende Daten und Budgetüberschreitung testen
Wann sinnvoll - und wann nicht?
Sinnvoll für betriebene Dienste mit messbarem SLO. Für nicht produktive Werkzeuge ohne Verfügbarkeitsversprechen wäre es unnötig.
Risiken und Fehlanwendungen
Ein Error Budget darf nicht zur Rechtfertigung vermeidbarer Ausfälle werden. Falsche SLI-Auswahl verschleiert tatsächliche Nutzerprobleme.
Enterprise-Java-Beispiel
Ein Dienst mit 99,9 Prozent Monats-SLO darf rund 43 Minuten Unverfügbarkeit verbrauchen; bei ausgeschöpftem Budget werden riskante Releases pausiert.
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Error Budget
// Pattern: Value Object - tolerierte und verbrauchte Ausfallzeit werden fachlich benannt.
public record ErrorBudget(long allowedDowntimeMillis,long consumedDowntimeMillis){
public ErrorBudget { if(allowedDowntimeMillis<0||consumedDowntimeMillis<0) throw new IllegalArgumentException(); }
public long remainingMillis(){ return Math.max(0,allowedDowntimeMillis-consumedDowntimeMillis); }
public double consumedRatio(){ return allowedDowntimeMillis==0 ? (consumedDowntimeMillis==0?0d:1d) : Math.min(1d,(double)consumedDowntimeMillis/allowedDowntimeMillis); }
public boolean permitsRiskyChange(){ return consumedRatio()<0.8d; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
SLO-Berechnung ist in Dashboardabfragen und Alarmregeln verteilt. Unterschiedliche Teams erhalten für denselben Zeitraum abweichende Ergebnisse.
Erkennungsmerkmale
SLI-Formeln werden mehrfach kopiert
Zeitzonen und Wartungsfenster werden uneinheitlich behandelt
Alarm und Release-Gate nutzen verschiedene Berechnungen
Sicherer Ablauf
SLI, Ziel, Fenster und Ausschlüsse typisieren
Messdatenzugriff über Port trennen
Evaluation als deterministische Funktion extrahieren
Ergebnis mit Datenvollständigkeit und Begründung versehen
Fenstergrenzen, Missing Data und Wartungszeiten testen
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn SLOs operative oder Releaseentscheidungen steuern. Bei rein informellen Dashboards genügt zunächst eine dokumentierte Abfrage.
Risiken und Fehlanwendungen
Zu viele oder unverständliche SLOs führen zu Alarmmüdigkeit. Die Evaluation muss die Nutzerperspektive abbilden.
Enterprise-Java-Beispiel
Die Evaluation berechnet Erfolgsquote und Latenzziel für Checkout-Anfragen über ein rollierendes 28-Tage-Fenster.
Abgrenzung und verwandte Muster
Error Budget, Rollout Guard, Health Model, Observability Context.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Extract SLO Evaluation
// Pattern: Specification - mehrere Serviceziele werden als kombinierbare Kriterien bewertet.
public final class SloEvaluator {
public record Target(double minimumAvailability,long maximumP95Millis){}
public record Measurement(double availability,long p95Millis){}
public record Result(boolean availabilityMet,boolean latencyMet){ public boolean met(){ return availabilityMet&&latencyMet; } }
public Result evaluate(Target t,Measurement m){ return new Result(m.availability()>=t.minimumAvailability(),m.p95Millis()<=t.maximumP95Millis()); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Kritische Arbeitslasten teilen Ressourcen mit optionalen Jobs. Bei Lastspitzen sind keine Kapazitäten für priorisierte Vorgänge reserviert.
Erkennungsmerkmale
Batchjobs verdrängen Online-Transaktionen
Kapazitätszusagen existieren nur in Tabellen
Autoscaling reagiert langsamer als Lastanstieg
Sicherer Ablauf
kritische Klassen und Mindestkapazität definieren
Reservation als typisierten Anspruch modellieren
Zuteilung und Rückgabe explizit machen
Überbuchung und Priorität regeln
Spitzenlast, Ausfall und verwaiste Reservierungen testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei vorhersehbaren kritischen Lasten oder knappen Ressourcen. Bei vollständig elastischen, billigen Ressourcen kann reine Skalierung genügen.
Risiken und Fehlanwendungen
Zu hohe Reservierung verschwendet Ressourcen; zu niedrige gibt falsche Sicherheit. Reservation und tatsächliche Plattformkapazität müssen abgestimmt sein.
Enterprise-Java-Beispiel
Für regulatorische Tagesabschlüsse werden feste Worker-Slots reserviert, die normale Reportjobs nicht belegen dürfen.
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Capacity Reservation
// Pattern: Value Object - zugesicherte Kapazität wird atomar geprüft statt implizit über Zähler.
public record CapacityReservation(String owner,int reservedUnits,int usedUnits){
public CapacityReservation { if(reservedUnits<0||usedUnits<0||usedUnits>reservedUnits) throw new IllegalArgumentException(); }
public int availableUnits(){ return reservedUnits-usedUnits; }
public CapacityReservation allocate(int units){ if(units<0||units>availableUnits()) throw new IllegalStateException("capacity exceeded"); return new CapacityReservation(owner,reservedUnits,usedUnits+units); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
ablehnbare Arbeit und Degradationsoptionen benennen
Policy als frühe Entscheidung extrahieren
Retry-After und beobachtbare Ablehnungsgründe liefern
Überlast, Erholung und Prioritätsinversion testen
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn Überlast realistisch ist und priorisierte Dienste geschützt werden müssen. Ohne messbare Kapazitätsgrenze kann die Policy zu willkürlichen Ablehnungen führen.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Load Shedding Policy
// Pattern: Policy - Überlastablehnung wird nach Priorität und Auslastung reproduzierbar.
public record LoadSheddingPolicy(double softLimit,double hardLimit){
public enum Priority { CRITICAL, NORMAL, BULK }
public boolean accept(Priority priority,double utilization){ if(utilization>=hardLimit) return priority==Priority.CRITICAL; if(utilization>=softLimit) return priority!=Priority.BULK; return true; }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Neue Arbeit wird angenommen, obwohl Ressourcen oder Abhängigkeiten die Bearbeitung nicht zuverlässig schaffen. Überlast wird dadurch nur in Queues verschoben.
Erkennungsmerkmale
Annahmen erfolgen ohne Kostenabschätzung
Queues wachsen nach Lastspitzen über lange Zeit weiter
Systemzustand verschlechtert sich nach jeder zusätzlichen Anfrage
Sicherer Ablauf
Eintrittskriterien und Arbeitskosten definieren
Admission Decision mit Grund und Gültigkeit modellieren
Prüfung an der Systemgrenze platzieren
Reservations- oder Tokenmechanismus anbinden
Grenzlast, Fairness und Freigabe testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei begrenzten Ressourcen, langen Jobs oder teuren Abhängigkeiten. Für sehr kurze, elastisch skalierbare Requests reicht oft Rate Limiting.
Risiken und Fehlanwendungen
Admission Control kann unfair werden oder Kapazität ungenutzt lassen. Entscheidungen benötigen aktuelle, aber nicht flackernde Signale.
Enterprise-Java-Beispiel
Ein Import wird nur angenommen, wenn ausreichend Speicher, Worker-Kapazität und ein freier Mandanten-Slot verfügbar sind.
package com.aydinsude.workbench.catalog;
import java.util.concurrent.atomic.AtomicInteger;
// Refactoring: Introduce Admission Control
// Pattern: Guard - ein Permit begrenzt parallele Arbeit an der Systemgrenze.
public final class AdmissionControl {
private final int limit; private final AtomicInteger active=new AtomicInteger();
public AdmissionControl(int limit){ if(limit<1) throw new IllegalArgumentException(); this.limit=limit; }
public Permit tryAcquire(){ while(true){ int now=active.get(); if(now>=limit) return null; if(active.compareAndSet(now,now+1)) return new Permit(); } }
public final class Permit implements AutoCloseable { private boolean closed; private Permit(){} public void close(){ if(!closed){ closed=true; active.decrementAndGet(); } } }
public int active(){ return active.get(); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Cloud- und Plattformkosten werden nachträglich mit unvollständigen Tags verteilt. Fachliche Nutzung und Kostenverursacher sind im Code nicht sichtbar.
Erkennungsmerkmale
gemeinsame Ressourcen landen pauschal bei einem Team
fehlende Tags verhindern belastbare Berichte
Kostenentscheidungen sind von technischen Ressourcennamen abhängig
Sicherer Ablauf
Kostenobjekte, Mandanten und Verteilungsregeln definieren
direkte und gemeinsame Kosten trennen
Policy als deterministische Zuordnung extrahieren
unbekannte Zuordnungen explizit behandeln
Summenerhalt, Rundung und Regelversionen testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Multi-Tenant-Plattformen oder interner Leistungsverrechnung. Für kleine Einzelteam-Systeme kann eine einfache Kostenstelle genügen.
Risiken und Fehlanwendungen
Scheingenauigkeit führt zu falschen Anreizen. Gemeinsame Grundkosten und Spitzenkapazität müssen transparent behandelt werden.
Enterprise-Java-Beispiel
Messagingkosten werden nach Nachrichtenvolumen verteilt, reservierte Basisinfrastruktur nach festen Plattformanteilen.
Abgrenzung und verwandte Muster
Cost Center Value Object, Capacity Reservation, Tenant Policy, Usage Metering.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.math.*;
// Refactoring: Extract Cost Allocation Policy
// Pattern: Policy - fixe und variable Plattformkosten werden reproduzierbar zugeordnet.
public record CostAllocationPolicy(BigDecimal fixedSharePerService,BigDecimal pricePerUnit){
public CostAllocationPolicy { if(fixedSharePerService.signum()<0||pricePerUnit.signum()<0) throw new IllegalArgumentException(); }
public BigDecimal allocate(long consumedUnits){ if(consumedUnits<0) throw new IllegalArgumentException(); return fixedSharePerService.add(pricePerUnit.multiply(BigDecimal.valueOf(consumedUnits))).setScale(2,RoundingMode.HALF_UP); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Laufzeitkonfiguration wird während eines Vorgangs mehrfach aus veränderlichen Quellen gelesen. Ein einzelner Geschäftsprozess kann dadurch mit widersprüchlichen Werten laufen.
Erkennungsmerkmale
ein Request liest Featureflag und Grenzwert zu verschiedenen Zeitpunkten
Konfigurationsänderungen erzeugen nicht reproduzierbare Fehler
Tests müssen globale Konfiguration mutieren
Sicherer Ablauf
für einen Vorgang benötigte Werte identifizieren
validierten unveränderlichen Snapshot erzeugen
Snapshot am Use-Case-Eingang binden
Version und Erstellungszeit mitführen
parallele Aktualisierung und alte Snapshot-Versionen testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei dynamisch änderbarer Konfiguration und längeren Vorgängen. Für unveränderliche Startkonfiguration reicht ein normaler Konfigurationswert.
Risiken und Fehlanwendungen
Snapshots können veralten. Sicherheitskritische Widerrufe und Kill Switches benötigen gegebenenfalls einen bewusst unmittelbaren Pfad.
Enterprise-Java-Beispiel
Ein Preisberechnungslauf verwendet genau eine Version von Rundung, Gebühren und Featureflags, auch wenn sich die zentrale Konfiguration währenddessen ändert.
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.*;
// Refactoring: Introduce Runtime Configuration Snapshot
// Pattern: Immutable Snapshot - ein Verarbeitungslauf sieht eine konsistente Konfigurationsversion.
public record RuntimeConfigurationSnapshot(long version,Instant loadedAt,Map<String,String> values){
public RuntimeConfigurationSnapshot { if(version<0) throw new IllegalArgumentException(); Objects.requireNonNull(loadedAt); values=Map.copyOf(values); }
public String required(String key){ var value=values.get(key); if(value==null||value.isBlank()) throw new IllegalStateException("missing "+key); return value; }
public int requiredInt(String key){ return Integer.parseInt(required(key)); }
}
Teststrategie: Vertragstests sichern die fachliche Entscheidung; Szenariotests prüfen Normalbetrieb, Grenzwerte, Degradation, Ausfall und Rückkehr. Zeit, Regionen und Messwerte werden über deterministische Fakes eingebracht.
Geheimnisse werden direkt aus Umgebungsvariablen, Properties oder statischen Hilfsklassen gelesen. Dadurch vermischt sich Geschäftslogik mit Speicherort, Authentifizierung und Fehlerbehandlung des Secret-Stores.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Zahlungsadapter bezieht API-Schlüssel über einen SecretProvider-Port; Tests verwenden einen In-Memory-Provider ohne echte Zugangsdaten.
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Ablauf, Übergangsfrist und Aktivierung neuer Geheimnisversionen sind implizit. Rotation wird dadurch zu einer riskanten Betriebsaktion statt zu einer kontrollierten Policy.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Zwei Schlüsselversionen sind während eines kurzen Überlappungsfensters gültig; danach wird die alte Version gesperrt.
Abgrenzung und verwandte Muster
Secret Provider, Key Rotation Policy, Rollback Plan
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Secret Rotation Policy
// Pattern: Policy - Rotationsregeln werden deterministisch testbar.
public record SecretRotationPolicy(Duration maximumAge, Duration gracePeriod){ public SecretRotationPolicy{ if(maximumAge.isNegative()||maximumAge.isZero()||gracePeriod.isNegative()) throw new IllegalArgumentException(); } public boolean due(Instant createdAt,Instant now){ return !now.isBefore(createdAt.plus(maximumAge)); } public boolean expired(Instant createdAt,Instant now){ return now.isAfter(createdAt.plus(maximumAge).plus(gracePeriod)); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Konfiguration wird erst dort geprüft, wo sie verwendet wird. Fehler erscheinen spät, uneinheitlich und oft erst unter Last.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Validator prüft Endpunkte, Grenzwerte und Kombinationen beim Laden und liefert ein typisiertes Fehlerergebnis.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Configuration Validator
// Pattern: Specification - mehrere Konfigurationsregeln werden gemeinsam ausgewertet.
public final class ConfigurationValidator { public record Config(String endpoint,int timeoutMillis,boolean tls){} public List<String> validate(Config c){ var e=new ArrayList<String>(); if(c.endpoint()==null||c.endpoint().isBlank()) e.add("endpoint"); if(c.timeoutMillis()<=0) e.add("timeout"); if(c.endpoint()!=null&&c.endpoint().startsWith("http:")&&c.tls()) e.add("tls-endpoint-mismatch"); return List.copyOf(e); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Ein Vorgang liest Konfiguration mehrfach aus veränderlichen Quellen. Innerhalb desselben Ablaufs können dadurch widersprüchliche Werte gelten.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Preisberechnungslauf erhält genau einen unveränderlichen Snapshot mit Version und Zeitstempel.
package com.aydinsude.workbench.catalog;
import java.time.*; import java.util.*;
// Refactoring: Introduce Configuration Snapshot
// Pattern: Immutable Snapshot - Leser sehen konsistente Versionen.
public record ConfigurationSnapshot(long version,Instant loadedAt,Map<String,String> values){ public ConfigurationSnapshot{ values=Map.copyOf(values); if(version<0) throw new IllegalArgumentException(); } public String require(String key){ var v=values.get(key); if(v==null) throw new IllegalArgumentException("missing "+key); return v; } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Mehrere Komponenten laden Konfiguration unabhängig neu. Reihenfolge, Validierung und Fehlerbehandlung sind nicht koordiniert.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Der Coordinator lädt, validiert und veröffentlicht einen neuen Snapshot atomar; bei Fehlern bleibt der alte aktiv.
Abgrenzung und verwandte Muster
Configuration Snapshot, Reconciliation Loop, Change Set
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.concurrent.atomic.*;
// Refactoring: Extract Dynamic Refresh Coordinator
// Pattern: Coordinator - Refresh-Schritte werden explizit sequenziert.
public final class DynamicRefreshCoordinator { public interface Loader{ ConfigurationSnapshot load(); } public interface Validator{ boolean valid(ConfigurationSnapshot s); } private final Loader loader; private final Validator validator; private final AtomicReference<ConfigurationSnapshot> current; public DynamicRefreshCoordinator(Loader l,Validator v,ConfigurationSnapshot initial){loader=l;validator=v;current=new AtomicReference<>(initial);} public boolean refresh(){ var next=loader.load(); return validator.valid(next)&¤t.compareAndSet(current.get(),next); } public ConfigurationSnapshot current(){return current.get();} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Mandantenspezifische Abweichungen liegen als verstreute if-Abfragen und Schlüsselpräfixe vor.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Eine Policy entscheidet transparent, welche globalen Werte überschrieben werden dürfen und welche unveränderlich bleiben.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Tenant Override Policy
// Pattern: Policy - Tenant-Overrides bleiben begrenzt und nachvollziehbar.
public record TenantOverridePolicy(Set<String> allowedKeys){ public TenantOverridePolicy{allowedKeys=Set.copyOf(allowedKeys);} public Map<String,String> merge(Map<String,String> base,Map<String,String> override){ var out=new HashMap<>(base); override.forEach((k,v)->{if(!allowedKeys.contains(k)) throw new IllegalArgumentException("forbidden override: "+k); out.put(k,v);}); return Map.copyOf(out); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Notabschaltungen sind als beliebige Feature-Flags implementiert und besitzen keine besondere Priorität oder Auditierbarkeit.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein KillSwitch-Port ermöglicht eine sofortige, nachvollziehbare Abschaltung kritischer Folgeaufrufe.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Kill Switch
// Pattern: Guard - riskante Verarbeitung wird früh und erklärbar blockiert.
public record KillSwitch(boolean enabled,String reason,Instant until){ public boolean blocks(Instant now){ return enabled&&(until==null||now.isBefore(until)); } public void check(Instant now){ if(blocks(now)) throw new IllegalStateException("disabled: "+reason); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Einzelne Schlüsseländerungen werden ohne fachlichen Zusammenhang verteilt. Teiländerungen können inkonsistente Zustände erzeugen.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Zusammengehörige Änderungen werden als versioniertes Change Set validiert, genehmigt und atomar aktiviert.
Abgrenzung und verwandte Muster
Approval Workflow, Policy Version, Rollback Plan
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*; import java.util.*;
// Refactoring: Introduce Configuration Change Set
// Pattern: Command/Audit Record - Änderung und Motivation reisen gemeinsam.
public record ConfigurationChangeSet(String actor,String reason,Instant changedAt,Map<String,String> before,Map<String,String> after){ public ConfigurationChangeSet{before=Map.copyOf(before);after=Map.copyOf(after);} public Set<String> changedKeys(){ var keys=new HashSet<String>(); keys.addAll(before.keySet()); keys.addAll(after.keySet()); keys.removeIf(k->Objects.equals(before.get(k),after.get(k))); return Set.copyOf(keys); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Entscheidungen lassen sich später nicht der damals gültigen Regelversion zuordnen.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Jede Policy-Auswertung trägt eine stabile Version, die Audit, Reproduktion und schrittweise Migration ermöglicht.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Policy Version
// Pattern: Value Object - Regelstände werden reproduzierbar referenziert.
public record PolicyVersion(String name,int major,int minor,Instant activeFrom) implements Comparable<PolicyVersion>{ public PolicyVersion{if(major<0||minor<0) throw new IllegalArgumentException();} public boolean activeAt(Instant t){return !t.isBefore(activeFrom);} public int compareTo(PolicyVersion o){int n=name.compareTo(o.name); if(n!=0)return n; int m=Integer.compare(major,o.major); return m!=0?m:Integer.compare(minor,o.minor);} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Soll- und Ist-Konfiguration weichen unbemerkt voneinander ab. Manuelle Änderungen bleiben bis zum Fehlerfall verborgen.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Der DriftDetector vergleicht normalisierte Snapshots und erzeugt strukturierte Abweichungsbefunde.
Abgrenzung und verwandte Muster
Reconciliation Loop, Checksum, Access Log
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Configuration Drift Detector
// Pattern: Differ - Soll/Ist-Abweichungen werden als typisiertes Ergebnis geliefert.
public final class ConfigurationDriftDetector { public record Drift(Set<String> missing,Set<String> unexpected,Set<String> changed){ public boolean clean(){return missing.isEmpty()&&unexpected.isEmpty()&&changed.isEmpty();} } public Drift compare(Map<String,String> desired,Map<String,String> actual){ var missing=new HashSet<>(desired.keySet());missing.removeAll(actual.keySet()); var unexpected=new HashSet<>(actual.keySet());unexpected.removeAll(desired.keySet()); var changed=new HashSet<String>(); desired.forEach((k,v)->{if(actual.containsKey(k)&&!Objects.equals(v,actual.get(k)))changed.add(k);}); return new Drift(Set.copyOf(missing),Set.copyOf(unexpected),Set.copyOf(changed)); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Fehlende oder ungültige Konfiguration führt zu zufälligen Standardwerten oder Null-Verhalten.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Eine SafeDefaultPolicy definiert bewusst fail-safe oder fail-closed Verhalten je Konfigurationsklasse.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Safe Default Policy
// Pattern: Policy - Defaults werden explizit, begrenzt und testbar.
public record SafeDefaultPolicy(Map<String,String> defaults, Set<String> requiredKeys){ public SafeDefaultPolicy{defaults=Map.copyOf(defaults);requiredKeys=Set.copyOf(requiredKeys);} public String resolve(String key,Map<String,String> values){ var value=values.get(key); if(value!=null)return value; if(requiredKeys.contains(key)&&!defaults.containsKey(key))throw new IllegalStateException("missing required: "+key); return defaults.get(key); } }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Kritische Änderungen werden ohne klaren Genehmigungs- und Vier-Augen-Prozess aktiviert.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Workflow modelliert Antrag, Prüfung, Freigabe, Aktivierung und Ablehnung mit Auditspur.
Abgrenzung und verwandte Muster
Change Set, Change Window Policy, Access Log
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Configuration Approval Workflow
// Pattern: State Machine - Änderung durchläuft nachvollziehbare Freigabestufen.
public final class ConfigurationApprovalWorkflow { public enum State{DRAFT,APPROVED,ACTIVE,REJECTED} public record Change(String id,State state,String approver,Instant activatedAt){} public Change approve(Change c,String approver){if(c.state()!=State.DRAFT)throw new IllegalStateException();return new Change(c.id(),State.APPROVED,approver,null);} public Change activate(Change c,Instant now){if(c.state()!=State.APPROVED)throw new IllegalStateException();return new Change(c.id(),State.ACTIVE,c.approver(),now);} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Rollback besteht nur aus der Hoffnung, alte Werte manuell wiederherzustellen.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Der Plan hält Vorgängerversion, Rückkehrkriterien und ausführbare Rollback-Schritte bereit.
Abgrenzung und verwandte Muster
Snapshot Reference, Policy Version, Change Set
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Configuration Rollback Plan
// Pattern: Command/Memento - Rückkehrziel und Aktion werden gemeinsam modelliert.
public record ConfigurationRollbackPlan(ConfigurationSnapshot previous,List<String> checks){ public ConfigurationRollbackPlan{checks=List.copyOf(checks);} public ConfigurationSnapshot execute(java.util.function.Consumer<ConfigurationSnapshot> activator){activator.accept(previous);return previous;} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Zeitliche Einschränkungen für riskante Änderungen sind in Kalendern und Betriebshandbüchern versteckt.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Eine Policy bewertet Zeitpunkt, Kritikalität, Bereitschaft und Ausnahmefreigabe.
package com.aydinsude.workbench.catalog;
import java.time.*; import java.util.*;
// Refactoring: Extract Change Window Policy
// Pattern: Policy - Betriebszeitfenster werden zentral bewertet.
public record ChangeWindowPolicy(Set<DayOfWeek> days,LocalTime from,LocalTime until,ZoneId zone){ public ChangeWindowPolicy{days=Set.copyOf(days);} public boolean allows(Instant instant){var z=instant.atZone(zone);return days.contains(z.getDayOfWeek())&&!z.toLocalTime().isBefore(from)&&z.toLocalTime().isBefore(until);} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Konfiguration wird je Umgebung separat gepflegt und driftet auseinander.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Promotion-Objekt überführt ein geprüftes Change Set nachvollziehbar von Test nach Produktion.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Environment Promotion
// Pattern: Command - Promotion wird als auditable Aktion modelliert.
public record EnvironmentPromotion(String source,String target,long version,Map<String,String> payload){ public EnvironmentPromotion{payload=Map.copyOf(payload);if(source.equals(target))throw new IllegalArgumentException();} public EnvironmentPromotion to(String next){return new EnvironmentPromotion(target,next,version,payload);} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Datei, Umgebung, Remote-Store und Mandantenwerte überschreiben sich nach impliziter Reihenfolge.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Eine explizite Prioritätsregel macht Herkunft, Überschreibung und verbotene Konflikte sichtbar.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Extract Configuration Source Priority
// Pattern: Strategy - Quellen werden nach expliziter Priorität zusammengeführt.
public record ConfigurationSourcePriority(List<String> order){ public ConfigurationSourcePriority{order=List.copyOf(order);} public Map<String,String> merge(Map<String,Map<String,String>> sources){var result=new LinkedHashMap<String,String>(); for(String source:order){result.putAll(sources.getOrDefault(source,Map.of()));} return Map.copyOf(result);} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Änderungen und Übertragungsfehler sind nur durch kompletten Inhaltsvergleich erkennbar.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Eine kanonische Prüfsumme identifiziert exakt den aktivierten Konfigurationsstand.
Abgrenzung und verwandte Muster
Snapshot, Drift Detector, Environment Promotion
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.nio.charset.*; import java.security.*; import java.util.*;
// Refactoring: Introduce Configuration Checksum
// Pattern: Value Object - Snapshot-Identität wird inhaltlich bestimmt.
public record ConfigurationChecksum(String value){ public static ConfigurationChecksum of(Map<String,String> values){try{var md=MessageDigest.getInstance("SHA-256");values.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(e->md.update((e.getKey()+"="+e.getValue()+"\n").getBytes(StandardCharsets.UTF_8)));return new ConfigurationChecksum(HexFormat.of().formatHex(md.digest()));}catch(NoSuchAlgorithmException e){throw new IllegalStateException(e);}} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Lese- und Änderungszugriffe auf sensible Konfiguration sind nicht nachvollziehbar.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Audit-Port protokolliert wer welche Version gelesen, geändert oder aktiviert hat, ohne Geheimniswerte mitzuschreiben.
Abgrenzung und verwandte Muster
Secret Provider, Approval Workflow, Policy Version
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Configuration Access Log
// Pattern: Audit Port - Zugriff wird entkoppelt und nachvollziehbar.
public interface ConfigurationAccessLog { record Access(String actor,String key,String purpose,Instant at){} void record(Access access); default String read(String actor,String key,String purpose,java.util.function.Supplier<String> supplier,Clock clock){record(new Access(actor,key,purpose,clock.instant()));return supplier.get();} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Schlüssel sind global, uneinheitlich benannt und kollidieren zwischen Modulen oder Mandanten.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Ein Namespace strukturiert Ownership, Sichtbarkeit und Lebenszyklus der Konfiguration.
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Configuration Namespace
// Pattern: Value Object - Schlüsselräume werden explizit und kollisionsfrei.
public record ConfigurationNamespace(String application,String module,String tenant){ public ConfigurationNamespace{if(application.isBlank()||module.isBlank())throw new IllegalArgumentException();} public String qualify(String key){if(key.isBlank())throw new IllegalArgumentException();return application+"."+module+(tenant==null||tenant.isBlank()?"":"."+tenant)+"."+key;} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Startkritische Werte und dynamische Laufzeitkonfiguration sind vermischt. Ohne Remote-Store kann die Anwendung nicht kontrolliert starten.
Erkennungsmerkmale
Die Entscheidung oder technische Abhängigkeit ist über mehrere Klassen, Properties oder Betriebsanweisungen verteilt.
Fehler werden spät erkannt und sind nur schwer reproduzierbar.
Tests müssen globale Zustände, Umgebungsvariablen oder externe Systeme manipulieren.
Sicherer Ablauf
Beobachtbares Verhalten und bestehende Konfigurationsquellen durch Characterization Tests sichern.
Fachliche Entscheidung von Speicherort, Transport und Framework-API trennen.
Einen kleinen typisierten Port oder ein unveränderliches Value Object einführen.
Validierung, Versionierung und Fehlerverhalten explizit machen.
Altpfad schrittweise ablösen und Rollback beziehungsweise Fallback testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Anwendungen, dynamischer Konfiguration, mehreren Umgebungen oder erhöhten Sicherheits- und Audit-Anforderungen. Für eine kleine Anwendung mit unveränderlichen Startwerten kann eine zusätzliche Abstraktionsschicht unnötig sein.
Risiken und Fehlanwendungen
Die neue Abstraktion darf keine Scheinsicherheit erzeugen. Geheimnisse dürfen weder geloggt noch in Snapshots oder Prüfsummen offengelegt werden. Dynamische Änderungen benötigen atomare Aktivierung, nachvollziehbare Versionen und ein bewusstes Fehlerverhalten.
Enterprise-Java-Beispiel
Eine kleine Bootstrap-Konfiguration enthält nur Identität, Vertrauensanker und Endpunkte zum Laden des validierten Hauptsnapshots.
package com.aydinsude.workbench.catalog;
import java.net.*; import java.time.*;
// Refactoring: Extract Bootstrap Configuration
// Pattern: Bootstrap Object - minimale Startabhängigkeiten bleiben stabil.
public record BootstrapConfiguration(URI configurationEndpoint,Duration connectTimeout,String identity){ public BootstrapConfiguration{if(connectTimeout.isNegative()||connectTimeout.isZero()||identity.isBlank())throw new IllegalArgumentException();} public boolean secure(){return "https".equalsIgnoreCase(configurationEndpoint.getScheme());} }
Teststrategie: Vertragstests prüfen den Port. Szenariotests decken fehlende Werte, ungültige Kombinationen, Versionswechsel, parallele Aktualisierung, Rollback und sicheren Ausfall ab. Geheimnisse werden ausschließlich durch Fakes oder Platzhalter repräsentiert.
Konfiguration besitzt häufig keinen klaren fachlichen und technischen Verantwortungsbereich. Mehrere Teams ändern dieselben Schlüssel, während niemand verbindlich für Bedeutung, Freigabe, Lebenszyklus und Rückbau zuständig ist. Das führt zu widersprüchlichen Annahmen, verwaisten Schaltern und riskanten Änderungen ohne belastbare Entscheidungskette.
Erkennungsmerkmale
Schlüssel werden von mehreren Modulen oder Teams geschrieben
für Störungen existiert kein eindeutig zuständiger Owner
fachliche Bedeutung und technische Ablage werden getrennt dokumentiert
veraltete Konfiguration bleibt bestehen, weil niemand den Rückbau verantwortet
Sicherer Refactoring-Ablauf
Alle Konfigurationsbereiche nach fachlicher Wirkung und technischer Nutzung inventarisieren.
Für jeden Bereich einen verantwortlichen Owner, Stellvertretung und Eskalationsweg festlegen.
Ownership als typisierten Vertrag mit Geltungsbereich, Änderungsrecht und Review-Pflicht modellieren.
Freigabe-, Audit- und Deprecation-Prozesse an die Ownership-Information koppeln.
Verwaiste, widersprüchliche und unzulässig geänderte Konfiguration durch automatisierte Regeln erkennen.
Wann sinnvoll - und wann nicht?
Sinnvoll in größeren Systemlandschaften, bei mehreren Teams oder geteilten Plattformdiensten. In einem kleinen, allein betreuten Modul reicht eine klar dokumentierte Zuständigkeit; ein eigenes Laufzeitmodell wäre dort unnötig.
Risiken und typische Fehlanwendungen
Ownership darf nicht zu einer organisatorischen Sperre für jede Kleinigkeit werden. Verantwortlichkeit, Änderungsrecht und technische Betriebszuständigkeit müssen unterscheidbar bleiben. Ein bloßes Namensfeld ohne Review- und Eskalationsprozess erzeugt Scheinsicherheit.
Enterprise-Java-Beispiel
Die Preisplattform ordnet Rabattschwellen dem Pricing-Team, Sicherheitsgrenzen dem Security-Team und technische Timeouts dem Plattformteam zu. Jede Änderung wird mit Owner, Grund, Version und Review-Frist gespeichert.
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Configuration Ownership
// Pattern: Value Object / Ownership Policy - Verantwortung wird explizit und prüfbar.
public record ConfigurationOwner(String team,String contact,Set<String> namespaces){
public ConfigurationOwner{if(team.isBlank()||contact.isBlank())throw new IllegalArgumentException();namespaces=Set.copyOf(namespaces);}
public boolean owns(String namespace){return namespaces.stream().anyMatch(namespace::startsWith);}
}
Teststrategie: Vertragstests sichern Ownership, Versionierung und Entscheidungsgrenzen. Szenariotests decken konkurrierende Änderungen, veraltete Snapshots, Teilaktivierung, Rollback, Ablauf und nicht automatisch lösbare Konflikte ab.
Alle Konfigurationsänderungen durchlaufen denselben Prozess, obwohl ein Textlabel ein anderes Risiko besitzt als ein Authentisierungsmodus, ein Zahlungsgrenzwert oder ein Kill Switch. Dadurch werden harmlose Änderungen unnötig langsam und kritische Änderungen zu schwach kontrolliert.
Erkennungsmerkmale
Freigaben richten sich nur nach Umgebung oder Dateipfad
Ausfallwirkung, Sicherheitsrelevanz und Datenrisiko werden nicht bewertet
Notfalländerungen und normale Pflege verwenden denselben Prozess
Tests und Rollback-Anforderungen sind nicht vom Risiko abhängig
Sicherer Refactoring-Ablauf
Auswirkungen auf Verfügbarkeit, Sicherheit, Datenintegrität und Compliance erfassen.
Wenige verständliche Risikoklassen mit nachvollziehbaren Kriterien definieren.
Die Klassifikation als reine, testbare Policy extrahieren.
Freigabe, Change Window, Testtiefe und Rollback-Vorgaben aus der Risikoklasse ableiten.
Grenzfälle und falsch klassifizierte Änderungen regelmäßig anhand realer Vorfälle überprüfen.
Wann sinnvoll - und wann nicht?
Sinnvoll, sobald Konfiguration unterschiedliche geschäftliche oder sicherheitsrelevante Auswirkungen besitzt. Bei ausschließlich kosmetischen, statischen Einstellungen genügt meist ein einfacher Review-Prozess.
Risiken und typische Fehlanwendungen
Zu viele Klassen machen die Entscheidung unverständlich. Die Klassifikation darf nicht allein auf Schlüsselpräfixen beruhen und muss bei unbekannten Änderungen konservativ reagieren.
Enterprise-Java-Beispiel
Eine Änderung der E-Mail-Betreffzeile wird als niedriges Risiko eingestuft; eine neue Token-Gültigkeit oder Zahlungsgrenze verlangt Vier-Augen-Freigabe, Canary-Rollout und getesteten Rollback.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Configuration Risk Classifier
// Pattern: Strategy - Risikoentscheidungen werden zentral und austauschbar.
public final class ConfigurationRiskClassifier{
public enum Risk{LOW,MEDIUM,HIGH,CRITICAL}
public record Change(int affectedServices,boolean securitySensitive,boolean reversible){}
public Risk classify(Change c){if(c.securitySensitive()&&!c.reversible())return Risk.CRITICAL;if(c.securitySensitive()||c.affectedServices()>20)return Risk.HIGH;if(c.affectedServices()>5||!c.reversible())return Risk.MEDIUM;return Risk.LOW;}
}
Teststrategie: Vertragstests sichern Ownership, Versionierung und Entscheidungsgrenzen. Szenariotests decken konkurrierende Änderungen, veraltete Snapshots, Teilaktivierung, Rollback, Ablauf und nicht automatisch lösbare Konflikte ab.
Konfigurationswerte werden einzeln betrachtet, obwohl sie fachlich und technisch voneinander abhängen. Wird ein Endpunkt aktiviert, ohne Credentials, Timeout, Featurefreigabe und Schema-Version gemeinsam zu berücksichtigen, entsteht ein formal gültiger, aber nicht betriebsfähiger Zustand.
Erkennungsmerkmale
Änderungen müssen in einer geheimen Reihenfolge erfolgen
Validierung prüft nur einzelne Schlüssel
ein Wert ist ohne mehrere andere Werte bedeutungslos
Rollback eines Schlüssels lässt abhängige Werte inkonsistent zurück
Sicherer Refactoring-Ablauf
Direkte und indirekte Abhängigkeiten zwischen Konfigurationswerten erfassen.
Abhängigkeiten als gerichteten Graphen mit Pflicht-, Konflikt- und Reihenfolgebeziehungen modellieren.
Zyklen und unvollständige Teiländerungen vor Aktivierung erkennen.
Change Sets topologisch planen und atomar beziehungsweise stufenweise ausführen.
Migration, Rollback und Ausfall einzelner Abhängigkeiten mit Szenariotests absichern.
Wann sinnvoll - und wann nicht?
Sinnvoll bei modularen Plattformen, Integrationen und mehrstufigen Rollouts. Für wenige unabhängige Startparameter wäre ein Graph überdimensioniert.
Risiken und typische Fehlanwendungen
Der Graph darf keine versteckten Laufzeitabhängigkeiten nachträglich legitimieren. Zu feingranulare Knoten erhöhen Pflegekosten; relevante fachliche Beziehungen müssen im Vordergrund stehen.
Enterprise-Java-Beispiel
Die Aktivierung eines neuen Fraud-Providers hängt von Endpoint, Credential-Version, Timeout-Policy, Circuit-Breaker-Profil und Event-Schema ab. Der Graph verhindert eine Teilaktivierung.
Beim Umbenennen, Aufteilen oder Bedeutungswechsel von Konfigurationswerten werden alte und neue Formate ad hoc in Geschäftslogik ausgewertet. Übergangscode verteilt sich und bleibt oft dauerhaft erhalten.
Erkennungsmerkmale
mehrere Schlüsselvarianten werden parallel geprüft
Fallbacks auf alte Namen sind über viele Klassen verteilt
eine neue Version kann alte Werte nicht eindeutig interpretieren
Rollback und Vorwärtsmigration sind nicht getrennt beschrieben
Sicherer Refactoring-Ablauf
Ausgangs- und Zielformat samt Semantik explizit beschreiben.
Lesen, Transformieren, Validieren und Schreiben in eine Migration kapseln.
Migration versionieren und idempotent ausführbar machen.
Kompatibilitätszeitraum sowie Rückbau des Altformats festlegen.
Beispiele aus realen Beständen, Wiederholung und Abbruch mit Tests absichern.
Wann sinnvoll - und wann nicht?
Sinnvoll bei produktiven Installationen mit langlebiger Konfiguration und mehreren Softwareversionen. Für eine noch nicht veröffentlichte Anwendung kann das alte Format meist direkt entfernt werden.
Risiken und typische Fehlanwendungen
Eine Migration darf Bedeutungsänderungen nicht als bloße Umbenennung behandeln. Ohne Idempotenz, Sicherung und Beobachtbarkeit kann ein Wiederanlauf Werte doppelt oder unvollständig transformieren.
Enterprise-Java-Beispiel
Aus `payment.timeoutSeconds` und `payment.retryCount` entsteht ein versioniertes Resilience-Profil. Die Migration erzeugt genau einen neuen Datensatz und markiert den Altbestand für späteren Rückbau.
Alte Schlüssel, Feature-Schalter und Kompatibilitätswerte bleiben unbegrenzt bestehen. Niemand weiß, ob sie noch verwendet werden, wann Warnungen beginnen und unter welchen Bedingungen sie entfernt werden dürfen.
Erkennungsmerkmale
alte und neue Namen werden dauerhaft parallel unterstützt
Verwendung veralteter Werte wird nicht gemessen
Entfernung erfolgt überraschend mit einem Release
Defaultwerte maskieren noch vorhandene Alt-Konfiguration
Sicherer Refactoring-Ablauf
Veralteten Vertrag, Ersatz und betroffene Versionen dokumentieren.
Warn-, Migrations- und Abschaltdatum als Policy modellieren.
Nutzung des Altwerts messen, ohne sensible Inhalte zu protokollieren.
Betreiber frühzeitig mit konkretem Migrationshinweis informieren.
Entfernung erst nach nachgewiesener Nichtnutzung und getestetem Rollback durchführen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei öffentlichen Plattformen, mehreren Deployments oder langen Upgrade-Zyklen. Bei internem, atomar ausgeliefertem Code kann eine direkte Migration ausreichen.
Risiken und typische Fehlanwendungen
Eine Deprecation ohne Telemetrie bleibt eine Absichtserklärung. Zu lange Übergangszeiten erhöhen Komplexität, zu kurze Fristen gefährden Bestandsinstallationen.
Enterprise-Java-Beispiel
Der alte Schlüssel `auth.roleMapping` wird drei Releases lang akzeptiert. Jede Nutzung erzeugt eine strukturierte Warnung; danach verweigert die Validierung das Altformat mit einem klaren Migrationshinweis.
Ob eine Konfigurationsänderung erfolgreich ist, wird nur durch allgemeine Systemmetriken oder subjektive Beobachtung bewertet. Es fehlt die Verbindung zwischen konkreter Version, Zielgruppe und Auswirkung.
Erkennungsmerkmale
Fehleranstieg lässt sich keiner Konfigurationsversion zuordnen
Canary und Kontrollgruppe werden nicht getrennt ausgewertet
Rollback-Kriterien sind nicht messbar
Metriknamen und Schwellenwerte liegen im Änderungsskript
Sicherer Refactoring-Ablauf
Erfolg, Fehler, Latenz und fachliche Ergebnisqualität je Änderung definieren.
Konfigurationsversion, Kohorte und Zeitpunkt als Dimensionen erfassen.
Messung über einen kleinen Metrics-Port vom Rollout entkoppeln.
Guard-Regeln aus stabilen, fachlich bestätigten Schwellenwerten ableiten.
Metrikausfall, verspätete Daten und falsche Korrelation explizit testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei dynamischen Änderungen, Canary-Rollouts und geschäftskritischen Policies. Für rein lokale Entwicklungswerte reicht Beobachtung über Tests.
Risiken und typische Fehlanwendungen
Zu viele Dimensionen können Kosten und Kardinalität explodieren lassen. Metriken dürfen keine personenbezogenen Daten oder Geheimnisse enthalten und müssen fachliche Wirkung statt nur technische Aktivität zeigen.
Enterprise-Java-Beispiel
Version 42 einer Betrugsschwelle wird für fünf Prozent des Verkehrs aktiviert. Der Metrics-Port liefert Ablehnungsquote, manuelle Nachprüfung und Fehlerrate getrennt nach Kontroll- und Canary-Gruppe.
Nach einer Änderung wird angenommen, dass alle Instanzen den gewünschten Zustand erreicht haben. Teilausfälle, manuelle Eingriffe oder verlorene Benachrichtigungen erzeugen dauerhafte Abweichungen.
Erkennungsmerkmale
Soll- und Ist-Zustand werden nur einmal beim Deployment verglichen
Fehlerhafte Instanzen bleiben auf alter Konfiguration
manuelle Reparatur ist der normale Betriebsweg
Aktivierung und tatsächliche Anwendung werden verwechselt
Sicherer Refactoring-Ablauf
Gewünschten und beobachteten Zustand getrennt modellieren.
Abweichungen regelmäßig, idempotent und mit begrenzter Rate ermitteln.
Erlaubte Reparaturaktionen und nicht automatisch lösbare Konflikte unterscheiden.
Jeden Lauf mit Version, Ergebnis und nächstem Versuch protokollieren.
Netzpartition, veraltete Beobachtung und konkurrierende Änderungen testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Systemen, vielen Instanzen und eventual-consistenter Verteilung. Bei einem einzelnen Prozess mit atomarem Startsnapshot reicht eine Startvalidierung.
Risiken und typische Fehlanwendungen
Eine aggressive Schleife kann Fehler verstärken oder manuelle Notmaßnahmen überschreiben. Sie braucht Ownership, Backoff, Abbruchgrenzen und eine eindeutige Quelle des Sollzustands.
Enterprise-Java-Beispiel
Der Coordinator erkennt, dass zwei von zwanzig Instanzen noch Version 41 verwenden. Er stößt einen begrenzten Refresh an und eskaliert nach wiederholtem Fehlschlag statt endlos zu schreiben.
Mehrere Quellen oder Administratoren ändern denselben Konfigurationsbereich gleichzeitig. Die zuletzt geschriebene Version gewinnt, ohne Priorität, Semantik oder fachliche Absicht zu berücksichtigen.
Erkennungsmerkmale
Last-write-wins ist die einzige Konfliktregel
Tenant-, Plattform- und Notfallwerte überschreiben sich unklar
Konflikte werden erst durch Laufzeitfehler sichtbar
manuelle Zusammenführung besitzt keine nachvollziehbare Begründung
Sicherer Refactoring-Ablauf
Konfliktarten und beteiligte Quellen typisieren.
Priorität, Merge-Fähigkeit und nicht automatisch lösbare Fälle definieren.
Entscheidung als reinen Resolver mit Evidenz und Begründung extrahieren.
Bei kritischen Konflikten Aktivierung stoppen und menschliche Freigabe verlangen.
Gleichzeitige Updates, veraltete Versionen und Notfallübersteuerung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Konfigurationsquellen, Mandanten-Overrides oder offline arbeitenden Verwaltungswerkzeugen. Bei einer strikt zentralen Schreibstelle genügt optimistische Versionsprüfung.
Risiken und typische Fehlanwendungen
Automatisches Mergen ist gefährlich, wenn Werte gemeinsam eine Invariante bilden. Notfallpriorität darf nicht dauerhaft normale Governance umgehen.
Enterprise-Java-Beispiel
Ein globales Sicherheitsminimum kollidiert mit einem niedrigeren Mandantenwert. Der Resolver lehnt die Absenkung ab, dokumentiert die Policy-Version und bietet keinen stillen Last-write-wins-Fallback.
Lang laufende Verwaltungs- oder Rolloutprozesse gehen davon aus, dass ihre gelesene Konfiguration bis zum Abschluss gültig bleibt. Paralleländerungen führen zu Operationen auf veralteter Grundlage.
Erkennungsmerkmale
ein Prozess liest Version A und schreibt später ohne Versionsprüfung
globale Sperren werden für lange Änderungen verwendet
abgebrochene Besitzer blockieren weitere Arbeiten
Gültigkeit einer Reservierung ist nicht zeitlich begrenzt
Sicherer Refactoring-Ablauf
Die zu schützende Ressource und erwartete Version definieren.
Eine zeitlich begrenzte Lease mit Besitzer, Token und Ablaufzeit einführen.
Verlängerung und Freigabe explizit, idempotent und auditierbar machen.
Jede schreibende Operation gegen Lease und Version prüfen.
Ablauf, Besitzerverlust, doppelte Freigabe und Uhrabweichung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten, länger dauernden Konfigurationsänderungen oder Migrationen. Für kurze atomare Datenbankupdates ist optimistisches Locking meist einfacher.
Risiken und typische Fehlanwendungen
Eine Lease ist keine dauerhafte Sperre. Falsch gewählte Laufzeiten, nicht monotone Zeit und fehlendes Fencing können weiterhin konkurrierende Schreiber zulassen.
Enterprise-Java-Beispiel
Eine mehrstufige Mandantenmigration erhält eine Lease auf Namespace und Version. Nach Ablauf darf ein neuer Prozess übernehmen; alte Schreibversuche werden anhand des Fencing-Tokens abgewiesen.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Configuration Lease
// Pattern: Lease Object - exklusiver Schreibzugriff hat eine klare zeitliche Grenze.
public record ConfigurationLease(String resource,String holder,Instant expiresAt){
public ConfigurationLease{if(resource.isBlank()||holder.isBlank())throw new IllegalArgumentException();}
public boolean validFor(String candidate,Instant now){return holder.equals(candidate)&&now.isBefore(expiresAt);}
public ConfigurationLease renew(String candidate,Instant now,Duration duration){if(!validFor(candidate,now)||duration.isNegative()||duration.isZero())throw new IllegalStateException();return new ConfigurationLease(resource,holder,now.plus(duration));}
}
Teststrategie: Vertragstests sichern Ownership, Versionierung und Entscheidungsgrenzen. Szenariotests decken konkurrierende Änderungen, veraltete Snapshots, Teilaktivierung, Rollback, Ablauf und nicht automatisch lösbare Konflikte ab.
In Störungen werden Werte direkt in Datenbanken, Umgebungsvariablen oder Feature-Stores überschrieben. Solche Notfalländerungen umgehen Validierung, Zeitbegrenzung und Rückführung.
Notfallwerte sind nicht von normalen Overrides unterscheidbar
nach dem Vorfall bleibt der Ausnahmezustand aktiv
Berechtigung und fachliche Zulässigkeit werden gemeinsam in Skripten geprüft
Sicherer Refactoring-Ablauf
Zulässige Notfallfälle, Berechtigungen und maximale Dauer definieren.
Override als typisierte Policy mit Grund, Genehmiger und Ablauf modellieren.
Normale Validierung nur gezielt und nachvollziehbar übersteuern.
Automatische Erinnerung, Rücknahme und nachträgliche Prüfung vorsehen.
Missbrauch, Ablauf, parallele Normaländerung und Wiederherstellung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll für geschäftskritische Systeme mit dokumentiertem Notfallverfahren. Ohne echte betriebliche Notwendigkeit sollte kein zusätzlicher Bypass existieren.
Risiken und typische Fehlanwendungen
Ein bequemer Notfallschalter wird schnell zum normalen Betriebsweg. Die Policy muss minimal, zeitlich begrenzt, streng autorisiert und vollständig auditiert sein.
Enterprise-Java-Beispiel
Bei einem fehlerhaften externen Provider darf ein Incident Commander für maximal 30 Minuten einen Fallback aktivieren. Danach wird automatisch auf den genehmigten Zustand zurückgestellt.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Extract Emergency Override Policy
// Pattern: Policy / Expiring Command - Notfallzugriff ist begrenzt, begründet und testbar.
public record EmergencyOverridePolicy(Duration maximumDuration){
public record Request(String actor,String reason,Instant requestedAt,Duration duration){}
public record Permit(String actor,String reason,Instant expiresAt){}
public Permit authorize(Request r,boolean authorized){if(!authorized||r.reason().isBlank()||r.duration().isNegative()||r.duration().isZero()||r.duration().compareTo(maximumDuration)>0)throw new IllegalArgumentException();return new Permit(r.actor(),r.reason(),r.requestedAt().plus(r.duration()));}
}
Teststrategie: Vertragstests sichern Ownership, Versionierung und Entscheidungsgrenzen. Szenariotests decken konkurrierende Änderungen, veraltete Snapshots, Teilaktivierung, Rollback, Ablauf und nicht automatisch lösbare Konflikte ab.
Kapitel 24 Authentication-Ports und Identitätsgrenzen10 Refactorings
Refactorings 231–240 lösen versteckte Sicherheitsabhängigkeiten auf und führen Ports, Value Objects, Policies, Decision Objects, Snapshots, Events und Cache-Regeln ein.
Refactoring-Katalog · Abschnitt 9
Identity, Authentication und Authorization explizit machen
Refactorings 231–240 lösen versteckte Sicherheitsabhängigkeiten auf und führen Ports, Value Objects, Policies, Decision Objects, Snapshots, Events und Cache-Regeln ein.
240 von 300 Refactorings abgeschlossen
60 Refactorings offen.
231. Extract Authentication Port
Fachliche Einordnung
Anwendungscode spricht direkt mit Framework-, Token- oder Verzeichnis-APIs. Damit werden Identitätsprüfung, Protokolldetails und fachliche Use Cases eng gekoppelt und schwer testbar.
Erkennungsmerkmale
Controller und Services parsen Token oder Sessions selbst
Framework-Sicherheitsobjekte wandern in den Anwendungskern
Tests benötigen echte Identity Provider oder komplexe Mocks
mehrere Authentisierungsverfahren erzeugen duplizierte Verzweigungen
Sicherer Refactoring-Ablauf
Benötigte Authentisierungsergebnisse aus Sicht des Use Cases bestimmen.
Einen kleinen Port für Identitätsnachweis und Fehlerfälle definieren.
Protokoll-, Framework- und Providerdetails in Adapter verschieben.
Aus externen Claims einen typisierten Principal erzeugen.
ungültige, abgelaufene, fehlende und technisch nicht erreichbare Nachweise testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei austauschbaren Identity Providern, mehreren Zugangskanälen oder einer hexagonalen Architektur. In einer kleinen Randanwendung kann die Frameworkgrenze bereits ausreichend sein.
Risiken und typische Fehlanwendungen
Der Port darf keine Sicherheitsdetails verschlucken, die für die Entscheidung relevant sind. Authentisierung und Autorisierung müssen getrennt bleiben; ein gültiger Nachweis erteilt noch keine Fachberechtigung.
Enterprise-Java-Beispiel
Ein REST-Adapter validiert ein OAuth-Zugriffstoken und liefert dem Use Case nur einen Principal mit stabiler Subjekt-ID, Authentisierungsstärke und Mandant.
Abgrenzung und verwandte Muster
Principal Value Object, Token Issuer, Credential Validator, Anti-Corruption Layer.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Optional;
// Refactoring: Extract Authentication Port
// Pattern: Port / Adapter - Identitätsprüfung bleibt außerhalb der Fachlogik.
public interface AuthenticationPort {
record Credentials(String scheme,String value){public Credentials{if(scheme.isBlank()||value.isBlank())throw new IllegalArgumentException();}}
record AuthenticatedIdentity(String subject){public AuthenticatedIdentity{if(subject.isBlank())throw new IllegalArgumentException();}}
Optional<AuthenticatedIdentity> authenticate(Credentials credentials);
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Identitäten werden als Benutzername, E-Mail, Map oder Frameworkobjekt weitergereicht. Dadurch sind Mandant, Subjekt-ID, Authentisierungsart und Vertrauensniveau uneinheitlich oder veränderbar.
Erkennungsmerkmale
Autorisierung nutzt wechselweise Name, E-Mail und Token-Claim
null oder leere Identitäten sind möglich
Frameworkobjekte gelangen tief in Domain- und Application-Code
Gleichheit einer Identität ist nicht eindeutig definiert
Sicherer Refactoring-Ablauf
Stabile Identitätsmerkmale und ihre Invarianten festlegen.
Ein unveränderliches Value Object mit klarer Gleichheit einführen.
Optionale Anzeigenamen von sicherheitsrelevanter Subjekt-ID trennen.
Mapping aus externen Claims ausschließlich am Adapter durchführen.
Mandantenwechsel, Serviceidentität und anonyme Zugriffe als getrennte Fälle testen.
Wann sinnvoll - und wann nicht?
Sinnvoll, sobald Identität mehrere Eigenschaften besitzt oder an vielen fachlichen Entscheidungen beteiligt ist. Bei einem rein technischen Einzweckprozess kann eine stabile Service-ID genügen.
Risiken und typische Fehlanwendungen
E-Mail und Anzeigename sind meist keine dauerhaften Identifikatoren. Sensible Claims sollten nicht unnötig im Principal gehalten oder geloggt werden.
Enterprise-Java-Beispiel
Der Principal enthält `subjectId`, `tenantId`, `actorType` und Authentisierungsstärke. Rollen und Berechtigungen werden bewusst nicht als unveränderliche Identitätseigenschaften modelliert.
Abgrenzung und verwandte Muster
Authentication Port, Security Context Snapshot, Access Decision, Value Object.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Introduce Principal Value Object
// Pattern: Value Object - Identität erhält Invarianten und fachliche Semantik.
public record Principal(String subject,String tenant,Set<String> factors){
public Principal{if(subject.isBlank()||tenant.isBlank())throw new IllegalArgumentException();factors=Set.copyOf(factors);}
public boolean strong(){return factors.size()>=2;}
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Berechtigungslogik liegt als verstreute Rollenabfrage in Controllern, Services und UI. Fachliche Bedingungen wie Eigentümerschaft, Betrag, Mandant und Prozesszustand bleiben unvollständig oder widersprüchlich.
Erkennungsmerkmale
`hasRole` und Stringvergleiche dominieren Fachcode
derselbe Vorgang wird an mehreren Stellen unterschiedlich geprüft
Rollen werden direkt mit fachlichen Aktionen gleichgesetzt
Ablehnung besitzt keine nachvollziehbare Begründung
Sicherer Refactoring-Ablauf
Fachliche Aktion, Ressource, Principal und Kontext explizit benennen.
Regeln in eine reine, kombinierbare Authorization Policy extrahieren.
Ein typisiertes Access Decision mit Grund und Evidenz zurückgeben.
Frameworkrollen am Rand in fachliche Fähigkeiten übersetzen.
positive, negative, mandantenfremde und zeitabhängige Fälle als Entscheidungsmatrix testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei nichttrivialen Fachrechten, mehreren Kanälen oder auditpflichtigen Entscheidungen. Für eine einfache administrative Route kann deklarative Methodensicherheit ausreichen.
Risiken und typische Fehlanwendungen
Eine zentrale Policy darf nicht zum allwissenden God Object werden. Regeln sollten nach Ressourcen oder Fähigkeiten geschnitten und standardmäßig restriktiv sein.
Enterprise-Java-Beispiel
Die Freigabe eines Schadenfalls verlangt nicht nur die Rolle Sachbearbeitung, sondern passenden Mandanten, Freigabelimit, Vier-Augen-Regel und einen zulässigen Fallzustand.
package com.aydinsude.workbench.catalog;
import java.util.Map;
// Refactoring: Extract Authorization Policy
// Pattern: Policy / Specification - Zugriff wird zentral und testbar entschieden.
public interface AuthorizationPolicy {
record Request(Principal principal,String action,Map<String,String> resource){public Request{resource=Map.copyOf(resource);}}
AccessDecision decide(Request request);
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Autorisierung liefert nur `true` oder `false`. Für Audit, Fehlermeldung, Support und Tests fehlen Grund, geprüfte Policy-Version und relevante Evidenz.
Erkennungsmerkmale
Ablehnungen werden pauschal als 403 protokolliert
Tests prüfen nur einen Boolean
mehrere Gründe werden durch frühes Return verborgen
Entscheidungen lassen sich später nicht reproduzieren
Sicherer Refactoring-Ablauf
Erlaubt, abgelehnt und technisch nicht entscheidbar unterscheiden.
Grundcode, Policy-Version und minimale Evidenz im Decision Object modellieren.
Sensible Details von benutzergeeigneten Meldungen trennen.
Enforcement, Audit und UI auf dasselbe Ergebnis zugreifen lassen.
kombinierte Regeln, Priorität von Ablehnungen und fehlende Evidenz testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei auditierbaren, erklärbaren oder komplexen Zugriffsentscheidungen. Für eine interne, triviale Guard-Clause kann ein Boolean ausreichend bleiben.
Risiken und typische Fehlanwendungen
Das Objekt darf keine geheimen Regelparameter oder personenbezogenen Daten offenlegen. Eine ausführliche Begründung darf einem Angreifer nicht unnötig die Sicherheitslogik erklären.
Enterprise-Java-Beispiel
Die Entscheidung enthält `DENIED_LIMIT_EXCEEDED`, Policy-Version 12 und die Information, dass eine zweite Freigabe erforderlich ist; interne Details bleiben im Auditkanal.
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Access Decision
// Pattern: Decision Object - Ergebnis und Begründung reisen gemeinsam.
public record AccessDecision(boolean allowed,String reason){
public AccessDecision{if(reason==null||reason.isBlank())throw new IllegalArgumentException();}
public static AccessDecision allow(String reason){return new AccessDecision(true,reason);}
public static AccessDecision deny(String reason){return new AccessDecision(false,reason);}
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Rollen werden im Fachcode direkt geprüft, obwohl Rollen organisatorische Bündel und keine stabilen Fachaktionen sind. Neue Rollen oder Ausnahmen erfordern Änderungen in vielen Use Cases.
Erkennungsmerkmale
Code enthält zahlreiche Vergleiche wie `role == ADMIN`
eine Rolle darf nur unter zusätzlichen Bedingungen handeln
mehrere Rollen teilen einzelne Fähigkeiten
Umbenennung oder Aufteilung einer Rolle verändert Fachcode
Sicherer Refactoring-Ablauf
Konkrete fachliche Aktionen als Fähigkeiten benennen.
Rollen, Gruppen und Claims am Sicherheitsrand auf Fähigkeiten abbilden.
Use Cases ausschließlich nach benötigter Fähigkeit fragen lassen.
Ressourcen- und Kontextbedingungen zusätzlich über eine Policy prüfen.
neue Rollen, kombinierte Fähigkeiten und Entzug ohne Fachcodeänderung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei wachsenden Berechtigungsmodellen und mehreren organisatorischen Rollen. Bei einem kleinen System mit einer einzigen stabilen Administrationsrolle kann direkte deklarative Prüfung genügen.
Risiken und typische Fehlanwendungen
Fähigkeiten dürfen nicht zu Tausenden unstrukturierten Strings werden. Capability Query ersetzt keine ressourcenbezogene Autorisierung und keine Mandantentrennung.
Enterprise-Java-Beispiel
Der Use Case verlangt `CLAIM_APPROVE`. Ob diese Fähigkeit aus Rolle, Delegation oder zeitlich begrenzter Freigabe stammt, entscheidet der Adapter beziehungsweise die Policy.
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Replace Role Checks with Capability Query
// Pattern: Capability Model - Fachcode kennt Fähigkeiten statt Rollenbezeichnungen.
public record CapabilitySet(Set<String> values){
public CapabilitySet{values=Set.copyOf(values);}
public boolean allows(String capability){return values.contains(capability);}
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Passwort-, Zertifikats-, API-Key- oder Signaturprüfung liegt in einem großen Authentisierungsservice. Algorithmen, Formate, Rotation und Fehlerbehandlung sind miteinander vermischt.
Erkennungsmerkmale
ein `switch` wählt Credentialtypen
kryptografische Bibliotheksaufrufe liegen im Use Case
Rotation verlangt Änderungen an mehreren Stellen
Tests können einzelne Prüfverfahren nicht isolieren
Sicherer Refactoring-Ablauf
Credentialtypen und gemeinsame Ergebnissemantik definieren.
Je Verfahren einen Validator hinter einem kleinen Strategy-Vertrag kapseln.
Auswahl anhand eines typisierten Credentialtyps oder Headers treffen.
Bibliotheks- und Schlüsselzugriffe in Infrastrukturadapter verschieben.
ungültig, abgelaufen, widerrufen, unbekannt und technisch fehlerhaft getrennt testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Nachweisverfahren oder wechselnden kryptografischen Implementierungen. Bei genau einem stabilen Verfahren genügt eine einzelne klar abgegrenzte Komponente.
Risiken und typische Fehlanwendungen
Validatoren dürfen keine detaillierten Unterschiede nach außen leaken, die Enumeration erleichtern. Zeitvergleich, Schlüsselrotation und sichere Fehlerbehandlung müssen erhalten bleiben.
Enterprise-Java-Beispiel
Ein API-Gateway unterstützt mTLS-Zertifikate und signierte Service-Tokens. Eine Registry wählt den Validator; beide liefern dasselbe typisierte Validierungsergebnis.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Credential Validator
// Pattern: Strategy - Credential-Arten teilen einen stabilen Vertrag.
public interface CredentialValidator {
record Result(boolean valid,String subject,String reason){}
Result validate(String credential);
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Während eines Vorgangs werden Identität, Mandant, Delegation und Berechtigungsclaims mehrfach aus veränderlichen Thread-, Session- oder Request-Kontexten gelesen. Asynchrone Ausführung und parallele Requests können dadurch falsche oder inkonsistente Werte verwenden.
Erkennungsmerkmale
ThreadLocal oder Frameworkkontext wird tief im Fachcode gelesen
asynchrone Aufgaben verlieren oder übernehmen fremden Kontext
Claims ändern sich innerhalb eines langen Ablaufs unkontrolliert
Für den Use Case benötigte Sicherheitsinformationen minimal bestimmen.
Beim Eintritt einen unveränderlichen Snapshot mit Principal, Mandant, Authentisierungsstärke und Delegation erzeugen.
Snapshot explizit an Application Services und asynchrone Commands übergeben.
Lebensdauer und Aktualitätsgrenzen definieren.
Threadwechsel, parallele Requests, Ablauf und widerrufene Berechtigungen testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei asynchronen, parallelen oder längeren Abläufen. Für einen kurzen synchronen Adapter kann der lokale Frameworkkontext ausreichen, solange er nicht in den Kern gelangt.
Risiken und typische Fehlanwendungen
Ein Snapshot kann veralten und darf keine unbegrenzte Autorisierung garantieren. Hochkritische Entscheidungen benötigen gegebenenfalls eine erneute aktuelle Policy-Auswertung.
Enterprise-Java-Beispiel
Ein Exportauftrag speichert den auslösenden Principal und Mandantenkontext im Command. Die spätere Worker-Ausführung übernimmt keinen zufälligen ThreadLocal-Kontext.
Abgrenzung und verwandte Muster
Principal Value Object, Immutable Snapshot, Context Propagation, Authorization Policy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Security Context Snapshot
// Pattern: Immutable Snapshot - Sicherheitskontext ist explizit und threadsicher.
public record SecurityContextSnapshot(Principal principal,String correlationId,Instant authenticatedAt){
public SecurityContextSnapshot{if(correlationId.isBlank())throw new IllegalArgumentException();}
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Token-Erzeugung, Claim-Auswahl, Signatur, Schlüsselzugriff und Ablaufzeit sind in Login- oder Servicecode eingebettet. Fachlogik kennt Bibliotheksdetails und kann Sicherheitsparameter unbeabsichtigt variieren.
Erkennungsmerkmale
Services bauen JWT-Maps direkt auf
Signaturalgorithmus und Key-ID werden im Use Case gewählt
verschiedene Tokenarten besitzen inkonsistente Claims
Tests benötigen echte private Schlüssel
Sicherer Refactoring-Ablauf
Tokenzweck, Empfänger, Lebensdauer und notwendige Claims als typisierten Auftrag modellieren.
Einen TokenIssuer-Port mit eindeutigem Ergebnis und Fehlern extrahieren.
Signatur, Schlüsselwahl und Bibliothek in einen Adapter kapseln.
Claim-Minimierung, Audience und Policy-Version zentral validieren.
Ablauf, falsche Audience, Rotation, Uhrabweichung und Signaturfehler testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei eigenen Token, mehreren Tokenarten oder austauschbarer Identity-Infrastruktur. Wenn ausschließlich ein externer Identity Provider Token ausstellt, sollte die Anwendung nicht selbst zum Issuer werden.
Risiken und typische Fehlanwendungen
Selbst ausgestellte Token erhöhen Sicherheitsverantwortung erheblich. Der Port darf schwache Algorithmen, beliebige Claims oder unkontrollierte Laufzeiten nicht ermöglichen.
Enterprise-Java-Beispiel
Der Application Service fordert ein kurzlebiges Download-Token für genau eine Datei an. Der Adapter setzt Audience, Subject, Scope, Ablauf und Key-ID nach zentraler Policy.
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Extract Token Issuer
// Pattern: Port / Factory - Tokenformat und Kryptografie bleiben Infrastrukturdetails.
public interface TokenIssuer {
record Token(String value,Duration validFor){public Token{if(value.isBlank()||validFor.isNegative()||validFor.isZero())throw new IllegalArgumentException();}}
Token issue(Principal principal,Duration validity);
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Sicherheitsrelevante Vorgänge werden als freie Logzeilen geschrieben. Struktur, Korrelation, Akteur, Ressource, Entscheidung und Policy-Version sind uneinheitlich; sensible Daten können versehentlich im Log landen.
Erkennungsmerkmale
Audit nutzt Stringverkettung
Erfolg und Ablehnung besitzen unterschiedliche Formate
Korrelation zu Request, Principal oder Ressource fehlt
Passwörter, Token oder vollständige Claims erscheinen in Logs
Sicherer Refactoring-Ablauf
Auditpflichtige Ereignistypen und minimale Felder definieren.
Ein unveränderliches SecurityAuditEvent mit Akteur, Aktion, Ressource, Ergebnis, Zeit und Korrelation einführen.
Sensible Inhalte explizit ausschließen oder maskieren.
Publikation hinter einen Audit-Port legen und Ausfallverhalten festlegen.
Schema, Redaction, Reihenfolge und nicht blockierende Zustellung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll für Authentisierung, Autorisierung, Privilegienänderung, Notfallzugriff und sensible Datenzugriffe. Reines Debug-Logging ist kein Ersatz.
Risiken und typische Fehlanwendungen
Auditdaten selbst sind schützenswert. Zu viele Ereignisse erschweren Auswertung; zu wenige verhindern Nachvollziehbarkeit. Kritische Aktionen dürfen bei Audit-Ausfall nicht unbemerkt fortgesetzt werden.
Enterprise-Java-Beispiel
Eine abgelehnte Freigabe erzeugt ein Event mit Principal-ID, Aktion, Fall-ID, Reason-Code, Policy-Version und Correlation-ID, jedoch ohne Token oder medizinische Falldaten.
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.Map;
// Refactoring: Introduce Security Audit Event
// Pattern: Domain Event - Auditdaten erhalten ein stabiles Schema.
public record SecurityAuditEvent(String subject,String action,boolean allowed,String reason,Instant occurredAt,Map<String,String> attributes){
public SecurityAuditEvent{if(subject.isBlank()||action.isBlank()||reason.isBlank())throw new IllegalArgumentException();attributes=Map.copyOf(attributes);}
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Autorisierungsentscheidungen werden pauschal oder ad hoc gecacht. Rollenänderungen, Widerruf, Ressourcenstatus und Policy-Version können dadurch verspätet wirksam werden.
Erkennungsmerkmale
ein globales TTL gilt für alle Berechtigungen
Cache-Key enthält weder Ressource noch Policy-Version
Ablehnungen und Erlaubnisse werden gleich lang gespeichert
Widerruf kann den Cache nicht gezielt invalidieren
Sicherer Refactoring-Ablauf
Stabilität und Widerrufsanforderung je Entscheidungstyp analysieren.
Cache-Key aus Principal, Mandant, Fähigkeit, Ressource und Policy-Version ableiten.
TTL, negative Caches und nicht cachebare Entscheidungen in einer Policy definieren.
Invalidierung an Rollen-, Delegations- und Policyänderungen koppeln.
Widerruf, Mandantentrennung, Versionswechsel und Cache-Ausfall testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei teuren, häufig wiederholten Policy-Auswertungen mit klaren Aktualitätsgrenzen. Hochkritische oder stark kontextabhängige Entscheidungen sollten häufig gar nicht gecacht werden.
Risiken und typische Fehlanwendungen
Ein schneller Cache darf niemals wichtiger als zeitnaher Entzug sein. Unvollständige Keys können Berechtigungen zwischen Ressourcen oder Mandanten vermischen.
Enterprise-Java-Beispiel
Leserechte auf einen öffentlichen Katalog werden kurz gecacht; Zahlungsfreigaben und Break-Glass-Zugriffe werden stets aktuell ausgewertet. Jede Policy-Version erzeugt einen neuen Cache-Namensraum.
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Extract Access Decision Cache Policy
// Pattern: Cache Policy - Cachebarkeit ist eine bewusste Sicherheitsentscheidung.
public final class AccessDecisionCachePolicy {
public record CacheDirective(boolean cacheable,Duration ttl){}
public CacheDirective directive(AccessDecision decision,boolean sensitive){
if(sensitive||!decision.allowed())return new CacheDirective(false,Duration.ZERO);
return new CacheDirective(true,Duration.ofSeconds(30));
}
}
Teststrategie: Vertragstests prüfen Port und Decision Objects. Sicherheitsfälle decken fehlende oder ungültige Nachweise, Mandantentrennung, Widerruf, Policywechsel, Kontextverlust, sensible Auditdaten und restriktives Fehlerverhalten ab.
Kapitel 25 Permissions, Policies und Autorisierungsentscheidungen10 Refactorings
Refactorings 241–250 führen explizite Berechtigungen, MFA-, Session-, Schlüssel-, Maskierungs- und Funktionstrennungsregeln sowie stabile Security-Ports ein.
Refactoring-Katalog · Abschnitt 10
Security Policies, Entscheidungen und Infrastrukturgrenzen
Refactorings 241–250 führen explizite Berechtigungen, MFA-, Session-, Schlüssel-, Maskierungs- und Funktionstrennungsregeln sowie stabile Security-Ports ein.
250 von 300 Refactorings abgeschlossen
50 Refactorings offen.
241. Introduce Permission Object
Fachliche Einordnung
Berechtigungen erscheinen in gewachsenen Java-Systemen häufig als freie Zeichenketten wie `invoice.read`, `READ_INVOICE` oder `invoice:read`. Dadurch bleiben Namensraum, Ressourcentyp und Aktion implizit; Tippfehler werden erst zur Laufzeit sichtbar und fachlich verschiedene Rechte können versehentlich gleich behandelt werden. Ein Permission Object macht die Berechtigung zu einem unveränderlichen Fachwert mit kanonischer Darstellung und klaren Invarianten.
Erkennungsmerkmale
Rollen- oder Rechteprüfungen vergleichen String-Literale in mehreren Modulen
Groß-/Kleinschreibung und Trennzeichen unterscheiden sich zwischen Systemen
Ressource, Aktion und Mandant werden in einem zusammengesetzten String kodiert
Tests sichern nur einzelne Literale statt die Semantik des Berechtigungsmodells
Sicherer Refactoring-Ablauf
Alle tatsächlich verwendeten Berechtigungsbezeichnungen inventarisieren und Dubletten sichtbar machen.
Ressource, Aktion und optionalen Geltungsbereich als getrennte Werte definieren.
Kanonisierung und Validierung in einem unveränderlichen Value Object zentralisieren.
Adapter für bestehende Rollen-, Claim- und Datenbankformate einführen.
String-Vergleiche schrittweise ersetzen und unbekannte Werte restriktiv ablehnen.
Wann sinnvoll - und wann nicht?
Sinnvoll, sobald Rechte über mehrere Module, Services oder Identity Provider ausgetauscht werden. Für eine lokale Anwendung mit zwei stabilen, typisierten Enum-Werten kann ein eigenes Objekt unnötig sein.
Risiken und typische Fehlanwendungen
Das Value Object darf keine Autorisierungsentscheidung vorwegnehmen: Es beschreibt ein Recht, erteilt es aber nicht. Eine zu aggressive Normalisierung kann fachlich unterschiedliche Ressourcen zusammenführen; Mandanten- und Objektgrenzen müssen ausdrücklich modelliert werden.
Enterprise-Java-Beispiel
Ein Rechnungsservice übernimmt externe Claims, übersetzt sie im Security-Adapter in `Permission("invoice", "read")` und übergibt dem Use Case nur noch typisierte Berechtigungen. Unbekannte Schreibweisen werden nicht stillschweigend akzeptiert.
Abgrenzung und verwandte Muster
Principal Value Object, Capability Query, Authorization Policy, Resource Scope.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Permission Object
// Pattern: Value Object - Berechtigungen erhalten Invarianten und kanonische Semantik.
public record Permission(String resource, String action) {
public Permission {
if (resource == null || resource.isBlank() || action == null || action.isBlank()) throw new IllegalArgumentException();
resource = resource.trim().toLowerCase(); action = action.trim().toLowerCase();
}
public String canonical() { return resource + ":" + action; }
}
Teststrategie: Property-basierte Tests prüfen Kanonisierung, Gleichheit und ungültige Werte. Vertragstests sichern die Übersetzung aus Token-Claims und Rollenregistern; Negativtests belegen, dass unbekannte Ressourcen oder Aktionen nicht automatisch freigeschaltet werden.
Mehrfaktor-Authentisierung ist keine einzelne Boolean-Abfrage, sondern eine kontextabhängige Sicherheitsentscheidung. Schutzbedarf, Aktion, Risikosignal, vorhandene Faktoren und deren Stärke müssen gemeinsam bewertet werden. Werden diese Regeln über Controller, Filter und Identity-Provider-Konfiguration verteilt, entstehen widersprüchliche Anforderungen und schwer erklärbare Ausnahmen.
Erkennungsmerkmale
`if (risk > ...)` und Rollenprüfungen entscheiden lokal über MFA
verschiedene Kanäle verlangen unterschiedliche Faktoren für dieselbe Aktion
ein vorhandener zweiter Faktor wird pauschal als ausreichend angesehen
Notfall- und Servicekonten umgehen Regeln ohne dokumentierten Geltungsbereich
Sicherer Refactoring-Ablauf
Geschäftskritische Aktionen und erforderliche Authentisierungsstärken klassifizieren.
Faktoren, Vertrauensniveau und Risikosignale als typisierte Eingaben modellieren.
Die Entscheidung als reine Policy mit begründetem Ergebnis extrahieren.
Identity-Provider-spezifische Methoden in einem Adapter auf die fachlichen Faktoren abbilden.
Rollout zunächst im Beobachtungsmodus durchführen und Fehlklassifikationen auswerten.
Wann sinnvoll - und wann nicht?
Sinnvoll bei risikobasiertem Zugriff, privilegierten Aktionen oder mehreren Authentisierungsverfahren. Eine pauschale MFA-Pflicht für jeden Zugriff kann einfacher direkt an der Plattformgrenze konfiguriert werden.
Risiken und typische Fehlanwendungen
MFA schützt nicht automatisch gegen jedes Phishing- oder Sitzungsrisiko. Faktorqualität, Wiederverwendung einer Sitzung und Recovery-Verfahren müssen berücksichtigt werden. Ausnahmen dürfen nicht als dauerhaftes Hintertürmodell enden.
Enterprise-Java-Beispiel
Für das Anzeigen einer Rechnung genügt eine bestehende starke Sitzung; das Ändern einer Bankverbindung verlangt phishingresistente Authentisierung oder eine erneute Bestätigung. Die Policy liefert `allow`, `step-up` oder `deny` samt Begründung.
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Extract Multi-Factor Authentication Policy
// Pattern: Policy / Specification - MFA-Anforderungen werden zentral und testbar.
public final class MultiFactorAuthenticationPolicy {
public record Request(String action, int riskScore, Set<String> factors) { public Request { factors = Set.copyOf(factors); } }
public AccessDecision decide(Request request) {
boolean strong = request.factors().contains("password") && request.factors().contains("otp");
if (request.riskScore() >= 70 && !strong) return AccessDecision.deny("strong-authentication-required");
return AccessDecision.allow("authentication-sufficient");
}
}
Teststrategie: Entscheidungstabellen decken Kombinationen aus Aktion, Risikostufe und Faktoren ab. Tests prüfen fehlende Faktoren, veraltete Nachweise, Break-Glass-Ausnahmen und das sichere Standardverhalten bei unbekannten Methoden.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.util.*;
import org.junit.jupiter.api.Test;
class MultiFactorAuthenticationPolicyTest {
@Test void contract() {
var r=new MultiFactorAuthenticationPolicy().decide(new MultiFactorAuthenticationPolicy.Request("payout",90,Set.of("password")));
org.junit.jupiter.api.Assertions.assertFalse(r.allowed());
}
}
Sitzungen verbinden einen erfolgreichen Identitätsnachweis über einen längeren Zeitraum mit weiteren Zugriffen. Absolute Laufzeit, Inaktivitätsgrenze, erneute Authentisierung und Widerruf sind deshalb Sicherheitsregeln und keine bloßen Cookie-Einstellungen. Eine Session Policy bündelt diese Entscheidungen und trennt sie von HTTP-, Token- und Frameworkdetails.
Erkennungsmerkmale
Timeouts sind in Web-XML, Security-Filter, Tokenkonfiguration und Frontend verteilt
privilegierte und normale Sitzungen verwenden dieselben Laufzeiten
Aktivität verlängert Sitzungen unbegrenzt
Widerruf oder Risikoänderung wird erst nach Ablauf des Tokens wirksam
Sicherer Refactoring-Ablauf
Bestehende Sitzungsarten, Laufzeiten und Re-Authentisierungswege erfassen.
Absolute Dauer, Idle Timeout, Authentisierungsstärke und Risikokontext modellieren.
Eine deterministische Policy mit `valid`, `reauthenticate` und `reason` einführen.
HTTP-Session, OIDC-Token und Identity-Provider-Sitzung über Adapter anbinden.
Zeitgrenzen, Uhrabweichung, Widerruf und parallele Geräte in Tests absichern.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Sicherheitsstufen, langlebigen Websitzungen oder getrennten Token- und Anwendungssitzungen. Für einen zustandslosen, sehr kurzlebigen technischen Client kann die Tokenvalidierung allein genügen.
Risiken und typische Fehlanwendungen
Zu kurze Laufzeiten führen zu Umgehungsdruck, zu lange Laufzeiten erhöhen das Missbrauchsfenster. Aktivität darf nicht automatisch Vertrauenswürdigkeit bedeuten; sensible Aktionen benötigen gegebenenfalls eine frische Authentisierung.
Enterprise-Java-Beispiel
Ein Supportportal erlaubt acht Stunden Grundsitzung, verlangt nach zwanzig Minuten Inaktivität eine erneute Anmeldung und fordert vor Kundendatenexport eine frische starke Authentisierung. Die Anwendung erhält eine begründete Session-Entscheidung.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Session Policy
// Pattern: Policy - Sessionlebensdauer wird als fachlich prüfbare Regel modelliert.
public final class SessionPolicy {
public record Session(Instant createdAt, Instant lastSeenAt, boolean privileged) {}
public record Decision(boolean valid, boolean reauthenticate, String reason) {}
public Decision evaluate(Session session, Instant now) {
Duration max = session.privileged() ? Duration.ofMinutes(30) : Duration.ofHours(8);
if (Duration.between(session.createdAt(), now).compareTo(max) > 0) return new Decision(false,true,"absolute-timeout");
if (Duration.between(session.lastSeenAt(), now).compareTo(Duration.ofMinutes(20)) > 0) return new Decision(false,true,"idle-timeout");
return new Decision(true,false,"active");
}
}
Teststrategie: Tests verwenden eine kontrollierte Clock und prüfen Grenzwerte exakt. Szenarien decken absolute und inaktive Abläufe, privilegierte Sitzungen, Risikoerhöhung, Widerruf und fehlende Zeitangaben ab.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.time.*;
import org.junit.jupiter.api.Test;
class SessionPolicyTest {
@Test void contract() {
var now=Instant.parse("2026-01-01T10:00:00Z");
var d=new SessionPolicy().evaluate(new SessionPolicy.Session(now.minusSeconds(60),now.minusSeconds(30),false),now);
org.junit.jupiter.api.Assertions.assertTrue(d.valid());
}
}
Passwort-Hashing ist kryptografische Infrastruktur mit hohen Änderungs- und Sicherheitsanforderungen. Fach- und Anwendungscode sollten weder Algorithmusparameter noch Salt-Format, Provider oder Upgradeverfahren kennen. Ein kleiner Port erlaubt sichere Implementierungen und kontrollierte Migrationen, ohne Passwörter oder kryptografische Details in Use Cases zu verteilen.
Erkennungsmerkmale
Services rufen `MessageDigest`, BCrypt- oder Provider-APIs direkt auf
Hashparameter werden als Konstanten in mehreren Klassen gepflegt
das gespeicherte Format verrät nicht, welcher Algorithmus verwendet wurde
ein Algorithmuswechsel erfordert Massenänderungen im Fachcode
Sicherer Refactoring-Ablauf
Vorhandene Hashformate und Verifikationspfade inventarisieren.
Port für Hashen, Verifizieren und optionalen Upgradebedarf definieren.
Algorithmuskennung und Parameter im gespeicherten Hashformat versionieren.
Konkrete Implementierung in einen Infrastrukturadapter verschieben.
Migration beim erfolgreichen Login sowie Fehl- und Timingverhalten testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei eigener Passwortauthentisierung oder mehreren Speicherformaten. Wenn ein Identity Provider Passwörter vollständig verwaltet, sollte die Anwendung keinen zusätzlichen Hashing-Port erfinden.
Risiken und typische Fehlanwendungen
Der Port macht eine schwache Implementierung nicht sicher. Klartext und `char[]` dürfen nicht geloggt oder unnötig kopiert werden. Ein stiller Fallback auf veraltete Algorithmen oder ungebremste Verifikation kann Angriffe erleichtern.
Enterprise-Java-Beispiel
Ein Legacy-Hash wird beim Login verifiziert. Liefert der Adapter `upgradeRequired`, erzeugt er nach erfolgreichem Nachweis einen neuen Argon2id-basierten Hash; der Use Case kennt nur das Port-Ergebnis und nie Algorithmusdetails.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Password Hashing Port
// Pattern: Port / Adapter - Kryptografische Implementierungsdetails bleiben austauschbar.
public interface PasswordHashingPort {
record PasswordHash(String algorithm, String encoded) { public PasswordHash { if(algorithm.isBlank()||encoded.isBlank()) throw new IllegalArgumentException(); } }
PasswordHash hash(char[] password);
boolean verify(char[] candidate, PasswordHash stored);
}
Teststrategie: Vertragstests prüfen korrekte und falsche Kandidaten, versionsabhängige Migration und ungültige Formate. Sicherheitstests stellen sicher, dass Klartext weder in Exceptions noch Logs erscheint und dass fehlende Provider restriktiv fehlschlagen.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class PasswordHashingPortTest {
@Test void contract() {
PasswordHashingPort p=new PasswordHashingPort() {
public PasswordHash hash(char[] x) {
return new PasswordHash("demo",new String(x));
}
public boolean verify(char[] x,PasswordHash h) {
return h.encoded().equals(new String(x));
}
};
org.junit.jupiter.api.Assertions.assertTrue(p.verify("x".toCharArray(),p.hash("x".toCharArray())));
}
}
Deutschsprachige Fachgrundlagen
245. Introduce Key Rotation Policy
Fachliche Einordnung
Kryptografische Schlüssel besitzen einen Lebenszyklus: Aktivierung, reguläre Rotation, Überlappung, Sperrung und Notfallersatz. Werden diese Regeln nur über Datumsvergleiche in Jobs und Services ausgedrückt, ist unklar, welche Schlüssel noch zum Entschlüsseln benötigt werden und wann neue Versionen sicher verwendet werden dürfen.
Erkennungsmerkmale
Schlüssel werden manuell oder nur bei Ablauf ersetzt
Verschlüsselung und Entschlüsselung kennen keine Schlüsselversion
alte Schlüssel werden zu früh deaktiviert oder unbegrenzt behalten
kompromittierte Schlüssel folgen demselben Ablauf wie planmäßige Rotation
Sicherer Refactoring-Ablauf
Schlüsselverwendungen, Schutzbedarf und Abhängigkeiten erfassen.
Zustände und Ereignisse für planmäßige und Notfallrotation definieren.
Rotation, Überlappungsfenster und Deaktivierung in einer Policy modellieren.
Ciphertexte und Signaturen immer mit Schlüsselversion referenzieren.
Rotation mit Parallelbetrieb, Rückabwicklung und Alt-Datenzugriff testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei langlebigen Daten, mehreren Schlüsselversionen oder externem KMS. Für kurzlebige, automatisch verwaltete Sitzungsschlüssel genügt oft die Plattformfunktion.
Risiken und typische Fehlanwendungen
Rotation bedeutet nicht automatisch Neuverschlüsselung aller Daten. Alte Versionen dürfen erst entzogen werden, wenn ihre Abhängigkeiten bekannt sind. Zeitbasierte Regeln ohne Kompromittierungs- und Recovery-Pfad sind unvollständig.
Enterprise-Java-Beispiel
Der Dokumentendienst verschlüsselt neue Dateien ab Stichtag mit Version K42, kann Bestandsdaten weiter mit K41 entschlüsseln und beendet die Überlappung erst, wenn ein Rewrap-Bericht vollständig ist. Ein kompromittierter Schlüssel überspringt das normale Fenster.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Key Rotation Policy
// Pattern: Policy / Strategy - Rotation wird explizit, nachvollziehbar und testbar.
public final class KeyRotationPolicy {
public record KeyMetadata(String keyId, Instant activatedAt, boolean compromised) {}
public record Decision(boolean rotate, Duration overlap, String reason) {}
public Decision decide(KeyMetadata key, Instant now) {
if (key.compromised()) return new Decision(true,Duration.ZERO,"compromised");
boolean due = Duration.between(key.activatedAt(),now).compareTo(Duration.ofDays(90)) >= 0;
return due ? new Decision(true,Duration.ofHours(24),"scheduled") : new Decision(false,Duration.ZERO,"current");
}
}
Teststrategie: Tests prüfen planmäßige Fälligkeit, Notfallrotation, Überlappung, Uhrgrenzen und blockierte Schlüssel. Integrationstests simulieren KMS-Ausfall und belegen, dass keine neue Verschlüsselung mit einer abgelaufenen Version erfolgt.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.time.*;
import org.junit.jupiter.api.Test;
class KeyRotationPolicyTest {
@Test void contract() {
var now=Instant.parse("2026-04-01T00:00:00Z");
org.junit.jupiter.api.Assertions.assertTrue(new KeyRotationPolicy().decide(new KeyRotationPolicy.KeyMetadata("k",now.minus(Duration.ofDays(91)),false),now).rotate());
}
}
Datenmaskierung reduziert sichtbare Informationen abhängig von Zweck, Empfänger und Ausgabekanal. Ein pauschales `substring` in DTOs oder Logs kennt weder Schutzklasse noch Berechtigung und kann sensible Werte in Fehlerpfaden oder Exporten unmaskiert lassen. Eine Policy entscheidet zentral, welcher Ausschnitt für welchen Kontext zulässig ist.
Erkennungsmerkmale
unterschiedliche Maskierungslogik in Mappern, Templates und Loggern
Produktionslogs enthalten vollständige Identifikatoren oder Kontodaten
privilegierte Anzeige wird durch ein globales Boolean freigeschaltet
Export, Bildschirm und Supportansicht verwenden dieselbe Darstellung
Sicherer Refactoring-Ablauf
Sensible Datentypen, Zwecke und Ausgabekanäle klassifizieren.
Maskierungsstufen und erlaubte Sichtbarkeit als Policy-Ergebnis definieren.
Darstellung vom Originalwert trennen und unveränderte Weitergabe verhindern.
Logging-, Mapping- und Exportadapter an die Policy anbinden.
Fehlerpfade, Listenansichten, Copy-Funktionen und strukturierte Logs testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei personenbezogenen, finanziellen oder geheimhaltungsbedürftigen Daten. Für Daten, die überhaupt nicht verarbeitet oder angezeigt werden dürfen, ist Maskierung kein Ersatz für Datenminimierung oder Zugriffskontrolle.
Risiken und typische Fehlanwendungen
Maskierung ist keine Anonymisierung und kann durch Kontextinformationen rückgängig gemacht werden. Ein privilegierter Schalter ohne Zweckbindung und Audit führt zu überbreitem Zugriff. Originalwerte dürfen nicht versehentlich in `toString()` oder Traces gelangen.
Enterprise-Java-Beispiel
Im Supportportal zeigt die Standardansicht nur die letzten vier Stellen einer IBAN; ein genehmigter Zahlungsfall erlaubt kurzzeitig die vollständige Anzeige. Logs und Exporte bleiben unabhängig davon stärker maskiert.
Abgrenzung und verwandte Muster
Privacy Classification Policy, Data Access Purpose, Authorization Policy, Redaction Strategy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Data Masking Policy
// Pattern: Policy / Strategy - Darstellung sensibler Daten ist kontextabhängig austauschbar.
public final class DataMaskingPolicy {
public enum Context { DISPLAY, LOG, EXPORT }
public String mask(String value, Context context, boolean privileged) {
if (value == null) return "";
if (privileged && context == Context.DISPLAY) return value;
if (context == Context.EXPORT) return "[REDACTED]";
int visible = Math.min(4, value.length());
return "*".repeat(Math.max(0,value.length()-visible)) + value.substring(value.length()-visible);
}
}
Teststrategie: Golden-Master-Tests sichern die Darstellung je Datentyp und Kontext. Negativtests prüfen kurze Werte, Nullwerte, Unicode, unberechtigte Vollanzeige sowie unbeabsichtigte Offenlegung in Exceptions und Logs.
Einwilligung ist mehr als ein Boolean. Zweck, Version der Erklärung, Zeitpunkt, Reichweite, Ablauf und Widerruf müssen gemeinsam nachvollziehbar sein. Wird nur `consent=true` gespeichert, kann die Anwendung weder Zweckbindung noch Beweisführung und Gültigkeit zuverlässig entscheiden.
Erkennungsmerkmale
ein einziges Flag steuert mehrere Verarbeitungszwecke
Textversion und Zeitpunkt der Erklärung fehlen
Widerruf überschreibt den historischen Zustand
nachgelagerte Systeme erhalten nur `true` oder `false` ohne Begründung
Sicherer Refactoring-Ablauf
Verarbeitungszwecke und bestehende Einwilligungsnachweise inventarisieren.
Gültigkeitskriterien und erforderliche Evidenz je Zweck definieren.
Ein unveränderliches Decision Object mit Grund und Version einführen.
Widerruf als neues Ereignis statt als Überschreiben modellieren.
Alle Aufrufer auf zweckgebundene Entscheidungen und Ablaufprüfung umstellen.
Wann sinnvoll - und wann nicht?
Sinnvoll, wenn eine Verarbeitung tatsächlich auf Einwilligung beruht und mehrere Systeme den Status benötigen. Andere Rechtsgrundlagen dürfen nicht künstlich als Einwilligung modelliert werden.
Risiken und typische Fehlanwendungen
Eine Entscheidung darf fehlende Evidenz nicht als Zustimmung interpretieren. Ablauf und Widerruf müssen konsistent propagiert werden; eine UI-Bestätigung ohne nachweisbare Version und Zweck reicht nicht aus.
Enterprise-Java-Beispiel
Ein Marketing-Use-Case erhält eine `ConsentDecision` für den konkreten Zweck Newsletter. Supportanalyse und Produkttelemetrie fragen getrennte Zwecke ab und können nicht versehentlich dieselbe Zustimmung wiederverwenden.
Abgrenzung und verwandte Muster
Consent Evidence Store, Data Access Purpose, Domain Event, Decision Object.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Consent Decision
// Pattern: Decision Object - Einwilligungsstatus und Evidenz reisen gemeinsam.
public record ConsentDecision(boolean granted, String purpose, String policyVersion, Instant validUntil, String reason) {
public ConsentDecision { if(purpose.isBlank()||policyVersion.isBlank()||reason.isBlank()) throw new IllegalArgumentException(); }
public boolean validAt(Instant now) { return granted && now.isBefore(validUntil); }
}
Teststrategie: Tests prüfen Zwecktrennung, Ablauf, Widerruf, Versionen und fehlende Evidenz. Ereignisbasierte Integrationstests sichern, dass ein Widerruf nachgelagerte Caches und Kampagnen beendet, ohne den historischen Nachweis zu löschen.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.time.*;
import org.junit.jupiter.api.Test;
class ConsentDecisionTest {
@Test void contract() {
var d=new ConsentDecision(true,"analytics","v2",Instant.parse("2027-01-01T00:00:00Z"),"explicit");
org.junit.jupiter.api.Assertions.assertTrue(d.validAt(Instant.parse("2026-01-01T00:00:00Z")));
}
}
Funktionstrennung verhindert, dass eine Person kritische Vorgänge allein beantragt, genehmigt und ausführt. Verstreute Rollenvergleiche bilden diese fachliche Invariante nur unvollständig ab, insbesondere bei Delegation, Stellvertretung und mehreren Genehmigungsstufen. Eine Policy macht die Unabhängigkeitsregel explizit und begründbar.
Erkennungsmerkmale
Antragsteller und Genehmiger werden nur über Benutzernamen verglichen
Delegationen umgehen unbeabsichtigt die Unabhängigkeit
Servicekonten oder Sammelrollen können mehrere Prozessschritte ausführen
Sicherer Refactoring-Ablauf
Kritische Aktionen und unvereinbare Verantwortlichkeiten identifizieren.
Beteiligte, Rollen, Delegationen und Organisationsgrenzen typisieren.
Eine reine Policy für zulässige Kombinationen und Begründungen extrahieren.
Workflow-Übergänge ausschließlich über das Policy-Ergebnis freigeben.
Stellvertretung, Notfallzugriff und technische Konten gesondert testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei Zahlungsfreigaben, Berechtigungsänderungen oder regulatorisch kontrollierten Prozessen. Für risikoarme, vollständig reversible Aktionen kann eine zusätzliche Genehmigungsstufe unverhältnismäßig sein.
Risiken und typische Fehlanwendungen
Ein bloßer Vergleich von IDs erkennt verbundene Konten oder gemeinsame Verantwortungsbereiche nicht. Ausnahmen müssen zeitlich begrenzt, genehmigt und auditiert werden; sonst wird die Policy zur Formalität.
Enterprise-Java-Beispiel
Eine Person darf eine Lieferantenzahlung erfassen, aber weder freigeben noch den Zielbankdatensatz ändern. Die Policy bewertet Identität, Rolle, Delegation und Prozesshistorie und liefert eine nachvollziehbare Ablehnung.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Segregation of Duties Policy
// Pattern: Policy / Specification - Funktionstrennung wird zentral entschieden.
public final class SegregationOfDutiesPolicy {
public AccessDecision decide(String requester, String approver, boolean critical) {
if (!critical) return AccessDecision.allow("single-approval-sufficient");
if (requester.equals(approver)) return AccessDecision.deny("requester-cannot-approve");
return AccessDecision.allow("independent-approval");
}
}
Teststrategie: Entscheidungstabellen prüfen gleiche Identität, gleiche Organisationseinheit, Delegation und Mehrfachfreigabe. Workflowtests belegen, dass direkte Zustandsänderungen die Policy nicht umgehen können.
Authentisierung, Autorisierung, Kontextprüfung und Audit müssen an einer Systemgrenze in konsistenter Reihenfolge stattfinden. Wenn jeder Use Case diese Schritte selbst orchestriert, entstehen Lücken, doppelte Prüfungen und unterschiedliche Fehlerbehandlung. Eine schmale Facade koordiniert den Ablauf, ohne die einzelnen Policies zu verschmelzen.
Erkennungsmerkmale
Controller rufen Security-Komponenten in unterschiedlicher Reihenfolge auf
manche Use Cases auditieren nur erfolgreiche Zugriffe
Framework-Sicherheitsobjekte gelangen bis in die Fachlogik
neue Kanäle kopieren bestehende Sicherheitsorchestrierung
Sicherer Refactoring-Ablauf
Verbindliche Schritte und Verantwortlichkeiten der Sicherheitsgrenze definieren.
Authentisierung, Autorisierung, Kontext und Audit als getrennte Ports oder Policies belassen.
Eine Facade mit kleinem, use-case-orientiertem Vertrag einführen.
Fehler in stabile, nicht informationspreisgebende Ergebnisse übersetzen.
Alle Eintrittspunkte schrittweise auf die Facade umstellen und Bypass-Pfade sperren.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Eingangskanälen oder wiederkehrender Sicherheitsorchestrierung. Eine Facade ist unnötig, wenn ein einziger Frameworkfilter die vollständige Grenze korrekt und testbar abbildet.
Risiken und typische Fehlanwendungen
Die Facade darf nicht zu einem allwissenden Security-God-Object wachsen. Sie koordiniert, entscheidet aber nicht jede Regel selbst. Audit muss sensible Daten minimieren und darf bei technischem Ausfall keine unkontrollierte Freigabe verursachen.
Enterprise-Java-Beispiel
REST, Batch und Messaging übergeben jeweils einen stabilen Security Request an dieselbe Facade. Diese erzeugt Principal und Access Decision, protokolliert das Ergebnis und ruft den Fach-Use-Case erst nach erfolgreicher Prüfung auf.
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.Map;
// Refactoring: Introduce Security Boundary Facade
// Pattern: Facade / Application Service - Sicherheitsablauf wird konsistent orchestriert.
public final class SecurityBoundaryFacade {
public interface AuditPort { void record(SecurityAuditEvent event); }
private final AuthorizationPolicy policy; private final AuditPort audit;
public SecurityBoundaryFacade(AuthorizationPolicy policy, AuditPort audit){this.policy=policy;this.audit=audit;}
public AccessDecision authorize(AuthorizationPolicy.Request request){
AccessDecision decision=policy.decide(request);
audit.record(new SecurityAuditEvent(request.principal().subject(),request.action(),decision.allowed(),decision.reason(),Instant.now(),Map.of()));
return decision;
}
}
Teststrategie: Komponententests prüfen Reihenfolge, Fehlerübersetzung und Audit für Erlaubnis wie Ablehnung. Architekturtests verhindern direkte Abhängigkeiten der Use Cases auf Framework-Securityklassen.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.util.*;
import org.junit.jupiter.api.Test;
class SecurityBoundaryFacadeTest {
@Test void contract() {
var p=new Principal("u","t",Set.of("pwd"));
var q=new AuthorizationPolicy.Request(p,"read",Map.of());
var events=new java.util.ArrayList<SecurityAuditEvent>();
var f=new SecurityBoundaryFacade(r->AccessDecision.allow("ok"),events::add);
org.junit.jupiter.api.Assertions.assertTrue(f.authorize(q).allowed());
org.junit.jupiter.api.Assertions.assertEquals(1,events.size());
}
}
Bedrohungserkennung liefert Signale oder Bewertungen aus SIEM-, Fraud- oder Threat-Intelligence-Systemen. Direkte SDK-Aufrufe im Fachcode koppeln Geschäftsentscheidungen an Produktmodelle, Latenz und Ausfallverhalten. Ein Port übersetzt externe Befunde in ein kleines, fachlich relevantes Assessment.
Erkennungsmerkmale
Use Cases importieren Klassen eines SIEM- oder Fraud-SDKs
externe Scores werden ohne Interpretation als Freigabe benutzt
Providerwechsel verändert Geschäftslogik und Tests
Timeouts oder fehlende Daten führen inkonsistent zu allow oder deny
Sicherer Refactoring-Ablauf
Benötigte Signale und fachliche Reaktionen vom konkreten Produkt trennen.
Ein stabiles Assessment mit Score, Kategorie, Evidenz und Aktualität definieren.
Providerintegration und Datenmapping in einen Adapter verschieben.
Ausfall-, Timeout- und Unbekannt-Fälle als bewusste Policy festlegen.
Shadow-Betrieb und Vergleich mit realen Entscheidungen vor produktiver Sperre durchführen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei austauschbaren Erkennungsdiensten oder mehreren Kanälen. Ein Port ist kein Mehrwert, wenn die Erkennung vollständig außerhalb der Anwendung erfolgt und nur ein fertiges, stabiles Ereignis konsumiert wird.
Risiken und typische Fehlanwendungen
Ein Score ist keine Wahrheit und darf ohne Schwellen-, Aktualitäts- und Evidenzprüfung keine irreversible Aktion auslösen. Falschpositive Befunde brauchen Review- und Einspruchswege; sensible Telemetrie muss minimiert werden.
Enterprise-Java-Beispiel
Der Login-Use-Case sendet ein typisiertes Signal. Der Adapter ruft die externe Engine auf und liefert `Assessment(82, "credential-abuse", ...)`; eine separate Risk Policy entscheidet anschließend über Step-up oder Blockierung.
Kapitel 26 Privacy, Token-Vertrauen und Incident Response10 Refactorings
Refactorings 251–260 führen explizite Datenschutz-, Kryptografie-, Token-, Geräte-, Risiko- und Incident-Grenzen ein.
Refactoring-Katalog · Abschnitt 11
Privacy, Token Trust und Incident Response
Refactorings 251–260 führen explizite Datenschutz-, Kryptografie-, Token-, Geräte-, Risiko- und Incident-Grenzen ein.
260 von 300 Refactorings abgeschlossen
40 Refactorings offen.
251. Extract Privacy Classification Policy
Fachliche Einordnung
Datenschutz- und Vertraulichkeitsklassen bestimmen, wie Daten gespeichert, angezeigt, exportiert und protokolliert werden dürfen. Freie Labels in DTOs oder Tabellen erzeugen keine verlässliche Schutzentscheidung. Eine Classification Policy bewertet Datentyp, Zweck und Kontext und liefert eine nachvollziehbare Schutzklasse mit zulässigen Verwendungen.
Erkennungsmerkmale
unterschiedliche Labels wie `sensitive`, `pii` oder `confidential` ohne gemeinsame Semantik
Schutzklasse wird erst im Export oder Logging entschieden
neue Datenfelder erben unbeabsichtigt die Klasse ihres Containers
Klassifizierung und technische Maßnahmen sind nicht miteinander verknüpft
Sicherer Refactoring-Ablauf
Datenarten, Zwecke und regulatorische Anforderungen inventarisieren.
Wenige verständliche Klassen und ihre Schutzwirkung definieren.
Klassifikation als reine Policy mit begründetem Decision Object extrahieren.
Maskierung, Verschlüsselung, Logging und Export an das Ergebnis koppeln.
Neue Felder und unbekannte Kategorien standardmäßig restriktiv behandeln.
Wann sinnvoll - und wann nicht?
Sinnvoll in datenreichen Plattformen, mehreren Ausgabekanälen oder regulierten Domänen. Bei einem kleinen Datensatz mit durchgehend gleichem Schutzbedarf kann eine einheitliche technische Schutzklasse genügen.
Risiken und typische Fehlanwendungen
Klassifizierung ersetzt weder Rechtsgrundlage noch Autorisierung. Zu grobe Klassen über- oder unterschützen Daten; zu viele Klassen werden nicht verstanden. Automatische Klassifikatoren benötigen Review und nachvollziehbare Evidenz.
Enterprise-Java-Beispiel
Kundennummer und Kontaktadresse werden als vertraulich, Gesundheits- oder Zahlungsdaten als besonders geschützt klassifiziert. Die Entscheidung steuert Verschlüsselung, Maskierung und zulässige Supportnutzung.
Abgrenzung und verwandte Muster
Data Masking Policy, Data Access Purpose, Encryption Port, Policy Enforcement Point.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Extract Privacy Classification Policy
// Pattern: Policy / Decision Object - Datenschutzregeln werden zentral und auditierbar entschieden.
public final class PrivacyClassificationPolicy {
public enum Classification { PUBLIC, INTERNAL, CONFIDENTIAL, RESTRICTED }
public record Decision(Classification classification, Set<String> allowedUses, boolean encryptionRequired) { public Decision { allowedUses=Set.copyOf(allowedUses); } }
public Decision classify(boolean personal, boolean financial, boolean health) {
if (health || financial) return new Decision(Classification.RESTRICTED, Set.of("service"), true);
if (personal) return new Decision(Classification.CONFIDENTIAL, Set.of("service","support"), true);
return new Decision(Classification.INTERNAL, Set.of("service","analytics"), false);
}
}
Teststrategie: Entscheidungstabellen prüfen Datenart, Zweck und Kontext. Schema-Tests verlangen für jedes neue sensible Feld eine explizite Klassifikation; Negativtests sichern das restriktive Verhalten bei unbekannten Kategorien.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class PrivacyClassificationPolicyTest { @Test void contract(){ var d=new PrivacyClassificationPolicy().classify(true,false,false); org.junit.jupiter.api.Assertions.assertTrue(d.encryptionRequired()); } }
Aufbewahrung und Löschung hängen von Zweck, Rechtsgrundlage, Fristbeginn, Sperren und Nachweispflichten ab. Einzelne Zahlenwerte oder Booleans verlieren diesen Zusammenhang. Ein Retention Decision Object transportiert Löschzeitpunkt, Grundlage, Legal Hold und Begründung als unveränderliche, auditierbare Entscheidung.
Erkennungsmerkmale
TTL-Werte stehen in Tabellen oder Properties ohne Fristbeginn
Legal Hold wird als globales Flag ohne Geltungsbereich geführt
nach einer Löschung ist die zugrunde liegende Entscheidung nicht mehr nachvollziehbar
Sicherer Refactoring-Ablauf
Datenkategorien, Zwecke, Fristbeginne und Sperrgründe erfassen.
Retention-Regeln von Scheduler und Speichertechnologie trennen.
Eine Entscheidung mit Version, Löschzeitpunkt, Grundlage und Hold modellieren.
Lösch- und Archivprozesse ausschließlich aus dieser Entscheidung ableiten.
Friständerung, Hold, Freigabe und nachweisbare Löschung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Datenkategorien, gesetzlichen Fristen oder verteilten Speichern. Für rein technische Kurzzeitcaches kann eine einfache TTL ausreichend sein.
Risiken und typische Fehlanwendungen
Ein Decision Object löst keine widersprüchlichen Rechtsanforderungen. Legal Holds dürfen nicht global und unbegrenzt wirken. Fristbeginn und Zeitzone müssen eindeutig sein; Löschbestätigung darf keine unnötigen Originaldaten enthalten.
Enterprise-Java-Beispiel
Ein Schadenfall bleibt bis zum Abschluss und einer anschließenden Frist gespeichert. Ein aktiver Legal Hold verschiebt die Löschung begründet; nach Freigabe wird ein neuer, versionierter Löschzeitpunkt berechnet.
package com.aydinsude.workbench.catalog;
import java.time.*;
// Refactoring: Introduce Data Retention Decision
// Pattern: Decision Object - Aufbewahrung und Löschung reisen als konsistente Entscheidung.
public record DataRetentionDecision(Instant deleteAt, String legalBasis, boolean legalHold, String reason) {
public DataRetentionDecision { if(deleteAt==null||legalBasis.isBlank()||reason.isBlank()) throw new IllegalArgumentException(); }
public boolean deletableAt(Instant now){ return !legalHold && !now.isBefore(deleteAt); }
}
Teststrategie: Tests prüfen Fristbeginn, Schaltjahre, Hold und Regelversion. Integrationsszenarien belegen idempotente Löschung über Primärspeicher, Suchindex und Archiv sowie einen datensparsamen Nachweis.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class DataRetentionDecisionTest { @Test void contract(){ var d=new DataRetentionDecision(java.time.Instant.EPOCH,"contract",false,"expired"); org.junit.jupiter.api.Assertions.assertTrue(d.deletableAt(java.time.Instant.EPOCH)); } }
Verschlüsselung schützt Daten nur, wenn Algorithmuswahl, Schlüsselzugriff, Kontextbindung und Fehlerverhalten konsistent umgesetzt sind. Direkte Cipher- oder KMS-Aufrufe im Fachcode verteilen diese Verantwortung. Ein Encryption Port hält die Anwendung auf der Ebene von Klartext, typisiertem Ciphertext und Zweckkontext.
Erkennungsmerkmale
Fachservices importieren JCA-, Provider- oder KMS-Klassen
Ciphertexte speichern weder Schlüsselversion noch Algorithmuskontext
AAD, Nonce oder Encoding werden unterschiedlich behandelt
Tests benötigen echte Schlüssel oder deaktivieren Verschlüsselung vollständig
Sicherer Refactoring-Ablauf
Schutzobjekte, Zwecke und erforderliche kryptografische Eigenschaften bestimmen.
Portvertrag für Verschlüsseln und Entschlüsseln mit typisiertem Kontext definieren.
Schlüsselwahl, Nonce, AAD und Provider in einen Adapter verschieben.
Ciphertextformat versionieren und Integrität verbindlich prüfen.
Fehler, Schlüsselrotation, beschädigte Daten und Altformate testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren KMS-Providern, langlebigen Daten oder hexagonaler Architektur. Wenn eine Datenbank transparent und vollständig ausreichend verschlüsselt, sollte keine zusätzliche Anwendungsschicht ohne konkreten Zweck eingeführt werden.
Risiken und typische Fehlanwendungen
Der Port darf kryptografisch relevante Parameter nicht verstecken, wenn der fachliche Zweck sie bestimmt. Entschlüsselungsfehler dürfen keine Klartextfragmente preisgeben. Eigenentwickelte Kryptografie und unsichere Fallbacks sind zu vermeiden.
Enterprise-Java-Beispiel
Der Dokument-Use-Case übergibt Inhalt und Zweck `customer-document`; der Adapter wählt den passenden Schlüssel, bindet Mandant und Dokument-ID als Kontext und liefert einen versionierten Ciphertext. Tests verwenden einen kontrollierten Fake, nicht Klartextproduktion.
Zugriff auf kryptografische Schlüssel ist eine besonders schützenswerte Entscheidung. Ein Boolean verliert Zweck, Operation, Schlüsselversion, Gültigkeit und Begründung. Ein Key Access Decision Object hält diese Informationen zusammen und ermöglicht least-privilege-orientierte, zeitlich begrenzte Freigaben.
Erkennungsmerkmale
Dienste erhalten pauschalen Zugriff auf einen gesamten Tresor
Verschlüsseln, Entschlüsseln und Signieren werden nicht unterschieden
Zugriff wird nur über technische Rolle statt fachlichen Zweck entschieden
Audit kann nicht erklären, warum eine Schlüsseloperation erlaubt wurde
Sicherer Refactoring-Ablauf
Schlüssel, Operationen und legitime Zwecke inventarisieren.
Entscheidungsattribute wie Mandant, Datenklasse, Operation und Gültigkeit definieren.
Policy-Ergebnis als unveränderliches Decision Object modellieren.
KMS-Adapter nur mit einer gültigen Entscheidung aufrufen.
Ablauf, Widerruf, falschen Zweck und Cross-Tenant-Zugriff testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei zentralem KMS, mehreren Schlüsseln oder strengem Audit. Für einen isolierten Prozess mit einem ausschließlich technisch gebundenen Schlüssel kann die Plattformberechtigung ausreichen.
Risiken und typische Fehlanwendungen
Die Entscheidung darf keine langlebige Kopie eines geheimen Schlüssels enthalten. Zeitliche Gültigkeit allein schützt nicht vor Kontextwechsel; Operation, Zweck und Ressource müssen Teil der Prüfung und des Audit sein.
Enterprise-Java-Beispiel
Ein Exportdienst darf mit Schlüssel K17 nur für den Zweck `regulated-export` und nur zum Verschlüsseln arbeiten. Entschlüsselung oder Nutzung für einen anderen Mandanten wird bereits vor dem KMS-Aufruf abgelehnt.
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Key Access Decision
// Pattern: Decision Object - Schlüsselzugriff wird nachvollziehbar und zeitlich begrenzt.
public record KeyAccessDecision(boolean allowed, String keyId, String purpose, Instant validUntil, String reason) {
public KeyAccessDecision { if(keyId.isBlank()||purpose.isBlank()||reason.isBlank()) throw new IllegalArgumentException(); }
public boolean validAt(Instant now){ return allowed && now.isBefore(validUntil); }
}
Teststrategie: Entscheidungstabellen prüfen Operation, Zweck, Mandant, Datenklasse und Zeitfenster. Audit-Tests belegen, dass Gründe sichtbar sind, aber keine Schlüssel- oder Klartextdaten protokolliert werden.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class KeyAccessDecisionTest { @Test void contract(){ var d=new KeyAccessDecision(true,"k1","decrypt",java.time.Instant.parse("2027-01-01T00:00:00Z"),"approved"); org.junit.jupiter.api.Assertions.assertTrue(d.validAt(java.time.Instant.parse("2026-01-01T00:00:00Z"))); } }
Tokenvalidierung umfasst Signatur, Aussteller, Zielgruppe, Zeitgrenzen, Typ, Scope und gegebenenfalls Widerruf. Werden nur einzelne Claims in Filtern geprüft, können gültig signierte, aber für einen anderen Zweck ausgestellte Tokens akzeptiert werden. Eine Policy bündelt die vollständigen Invarianten und liefert ein typisiertes Ergebnis.
Erkennungsmerkmale
Controller lesen `sub`, `aud` oder `scope` direkt aus Claims
Signaturprüfung und fachliche Claimprüfung liegen in verschiedenen Schichten
mehrere APIs akzeptieren unterschiedliche Issuer- oder Audience-Werte
abgelaufene, noch nicht gültige oder widerrufene Tokens werden uneinheitlich behandelt
Sicherer Refactoring-Ablauf
Alle akzeptierten Tokentypen, Issuer, Audience und Scopes dokumentieren.
Kryptografische Prüfung im Protokolladapter von fachlicher Policy trennen.
Zeit, Typ, Zielgruppe und benötigte Capability zentral validieren.
Widerruf und Schlüsselversion als explizite Abhängigkeiten einbinden.
Fehler in stabile Gründe übersetzen, ohne sicherheitsrelevante Details preiszugeben.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren APIs, Identity Providern oder Tokentypen. Ein Framework kann die kryptografische Basisprüfung übernehmen, ersetzt aber nicht automatisch die anwendungsspezifische Audience- und Scope-Policy.
Risiken und typische Fehlanwendungen
Das bloße Parsen eines JWT ist keine Validierung. Uhrabweichung muss begrenzt sein; unbekannte Algorithmen, Issuer oder Claimtypen sind abzulehnen. Fehlermeldungen dürfen Angreifern keine unnötige Diagnose liefern.
Enterprise-Java-Beispiel
Der REST-Adapter prüft Signatur und normalisiert Claims. Die Policy akzeptiert nur Tokens des vorgesehenen Issuers, für Audience `claims-api`, mit Scope `claim:read`, gültigem Zeitraum und ohne aktiven Widerruf.
Abgrenzung und verwandte Muster
Authentication Port, Revocation Registry, Permission Object, Principal Value Object.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.Set;
// Refactoring: Extract Token Validation Policy
// Pattern: Policy / Specification - Tokenregeln werden zentral und testbar.
public final class TokenValidationPolicy {
public record Token(String issuer, String audience, Instant expiresAt, Set<String> scopes){ public Token { scopes=Set.copyOf(scopes); } }
public record Decision(boolean valid, String reason){}
public Decision validate(Token token, String issuer, String audience, String scope, Instant now){
if(!issuer.equals(token.issuer())) return new Decision(false,"issuer");
if(!audience.equals(token.audience())) return new Decision(false,"audience");
if(!now.isBefore(token.expiresAt())) return new Decision(false,"expired");
return token.scopes().contains(scope)?new Decision(true,"valid"):new Decision(false,"scope");
}
}
Teststrategie: Tests prüfen jeden Claim einzeln und in Kombination: falscher Issuer, Audience, Scope, Ablauf, `nbf`, Uhrabweichung und Widerruf. Mutations- oder Negativtests sichern, dass keine einzelne Prüfung versehentlich entfernt werden kann.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class TokenValidationPolicyTest { @Test void contract(){ var t=new TokenValidationPolicy.Token("i","a",java.time.Instant.parse("2027-01-01T00:00:00Z"),java.util.Set.of("read")); org.junit.jupiter.api.Assertions.assertTrue(new TokenValidationPolicy().validate(t,"i","a","read",java.time.Instant.EPOCH).valid()); } }
Widerruf beendet Vertrauen vor dem regulären Ablauf eines Tokens, Schlüssels oder einer Sitzung. Lokale Sets oder unversionierte Caches sind dafür ungeeignet, weil Verteilung, Ablauf und Konsistenz unklar bleiben. Eine Registry definiert einen expliziten Vertrag für Widerruf, Grund, Geltungsbereich und zeitliche Wirksamkeit.
Erkennungsmerkmale
jeder Prozess führt eine eigene Blockliste
Widerrufe gehen nach Neustart oder Cache-Eviction verloren
Ablauf und Geltungsbereich werden nicht gespeichert
Validierung kennt keine definierte Reaktion auf Registry-Ausfall
Sicherer Refactoring-Ablauf
Widerrufbare Subjekte und erforderliche Reaktionszeit festlegen.
Eintrag mit ID, Typ, Grund, Zeitpunkt, Ablauf und Version modellieren.
Registry-Port und konsistente Abfrage in Validierungs-Policies einführen.
Verteilung, Cache und Invalidierung als Infrastruktur behandeln.
Ausfall, verspätete Replikation, Ablauf und Wiederholung testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei lang lebenden Tokens, Sitzungen, Schlüsseln oder Zertifikaten. Sehr kurzlebige Tokens können durch geringe Laufzeit und fortlaufende Zugriffsevaluierung einfacher abgesichert werden.
Risiken und typische Fehlanwendungen
Eine Registry kann zum zentralen Ausfallpunkt werden. Caches dürfen den Sicherheitszweck nicht durch zu lange TTL unterlaufen. Löschung abgelaufener Einträge muss von forensischer Nachweisführung getrennt werden.
Enterprise-Java-Beispiel
Nach einem kompromittierten Konto schreibt der Incident-Workflow einen Widerruf für Sitzung und Refresh-Token. Alle API-Instanzen prüfen die Registry über einen kurzlebigen Cache; bei Unsicherheit gilt für privilegierte Aktionen fail closed.
Gerätevertrauen entsteht aus mehreren Signalen: Verwaltung, Verschlüsselung, Patchstand, Attestierung, Standort und beobachtetes Verhalten. User-Agent oder ein einzelnes `trustedDevice`-Flag bilden dies nicht belastbar ab. Eine Policy kombiniert Signale zu einer erklärbaren Vertrauensstufe und einer möglichen Step-up-Anforderung.
Erkennungsmerkmale
Geräteprüfung basiert nur auf Cookie, User-Agent oder IP
MDM-, Compliance- und Risikosignale werden getrennt ausgewertet
ein ehemals vertrauenswürdiges Gerät bleibt unbegrenzt freigeschaltet
fehlende oder widersprüchliche Signale werden automatisch als vertrauenswürdig behandelt
Sicherer Refactoring-Ablauf
Benötigte Gerätesignale und deren Aktualität definieren.
Signale über Adapter in ein stabiles Eingabemodell übersetzen.
Vertrauensstufen und Reaktionen als Policy-Ergebnis modellieren.
Fehlende, veraltete und widersprüchliche Evidenz restriktiv behandeln.
Rollout mit Berichtmodus und kontrollierten Ausnahmen durchführen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei Zero-Trust-Zugriff, mobilen Geräten oder privilegierten Anwendungen. Für öffentliche, nicht personalisierte Inhalte darf Gerätevertrauen keine unnötige Hürde werden.
Risiken und typische Fehlanwendungen
Gerätevertrauen ist keine Benutzeridentität und darf Autorisierung nicht ersetzen. Signale können veraltet oder manipulierbar sein; BYOD und Barrierefreiheit benötigen bewusste Alternativen statt pauschaler Sperre.
Enterprise-Java-Beispiel
Ein verwaltetes, verschlüsseltes und aktuelles Gerät erhält `TRUSTED`; ein privates Gerät darf Rechnungen lesen, muss aber für Export oder Stammdatenänderung eine starke Step-up-Prüfung erfüllen.
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Device Trust Policy
// Pattern: Policy / Specification - Gerätesignale werden als testbare Entscheidung kombiniert.
public final class DeviceTrustPolicy {
public enum Trust { TRUSTED, LIMITED, UNTRUSTED }
public record Signals(boolean managed, boolean encrypted, boolean patched, int anomalyScore){}
public record Decision(Trust trust, boolean stepUpRequired, String reason){}
public Decision evaluate(Signals s){
if(s.anomalyScore()>=80) return new Decision(Trust.UNTRUSTED,true,"anomaly");
if(s.managed()&&s.encrypted()&&s.patched()) return new Decision(Trust.TRUSTED,false,"managed");
return new Decision(Trust.LIMITED,true,"insufficient-signals");
}
}
Teststrategie: Entscheidungstabellen prüfen Kombinationen und Aktualität der Signale. Tests sichern restriktives Verhalten bei fehlender Attestierung, hohem Anomaliescore und widersprüchlicher Compliance-Meldung.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class DeviceTrustPolicyTest { @Test void contract(){ var d=new DeviceTrustPolicy().evaluate(new DeviceTrustPolicy.Signals(true,true,true,0)); org.junit.jupiter.api.Assertions.assertEquals(DeviceTrustPolicy.Trust.TRUSTED,d.trust()); } }
Ein einzelner Risikoscore ohne Faktoren, Modellversion und empfohlene Maßnahme ist nicht erklärbar und wird leicht falsch interpretiert. Ein Risk Assessment bündelt Bewertung, Stufe, Evidenz, Aktualität und Reaktion. Damit bleibt sichtbar, ob eine Aktion erlaubt, verstärkt geprüft, manuell bewertet oder blockiert werden soll.
Erkennungsmerkmale
verschiedene Systeme verwenden Scores mit unterschiedlichen Skalen
Schwellenwerte stehen verteilt in Controllern und Regeln
Entscheidungen speichern den Score, aber nicht die Faktoren
Modell- oder Regeländerungen sind im Audit nicht erkennbar
Sicherer Refactoring-Ablauf
Risikoobjekt, Schutzbedarf und mögliche Auswirkungen definieren.
Signale normalisieren und Modell- beziehungsweise Regelversion erfassen.
Score, Stufe, Faktoren und empfohlene Aktion gemeinsam modellieren.
Schwellen in einer separaten, versionierten Policy halten.
Ergebnisse gegen reale Vorfälle und Fehlalarme kalibrieren.
Wann sinnvoll - und wann nicht?
Sinnvoll bei risikobasiertem Zugriff, Betrugserkennung oder Priorisierung. Ein Assessment ist überdimensioniert, wenn eine klare deterministische Regel ohne Unsicherheit entscheidet.
Risiken und typische Fehlanwendungen
Ein Score darf keine scheinbare mathematische Genauigkeit vortäuschen. Modelle können verzerrt, veraltet oder nicht erklärbar sein. Kritische Entscheidungen benötigen menschliche Prüfung, Einspruch und nachvollziehbare Faktoren.
Enterprise-Java-Beispiel
Ein Login erhält Score 76 mit Faktoren „neues Gerät“ und „untypischer Standort“. Das Assessment empfiehlt Step-up; erst die MFA Policy entscheidet, welcher zusätzliche Nachweis erforderlich ist.
package com.aydinsude.workbench.catalog;
import java.util.List;
// Refactoring: Introduce Risk Assessment
// Pattern: Decision Object - Score, Evidenz und Maßnahme bleiben zusammen.
public record RiskAssessment(int score, Level level, List<String> factors, Action action) {
public enum Level { LOW, MEDIUM, HIGH, CRITICAL } public enum Action { ALLOW, STEP_UP, REVIEW, BLOCK }
public RiskAssessment { if(score<0||score>100) throw new IllegalArgumentException(); factors=List.copyOf(factors); }
public static RiskAssessment of(int score,List<String> factors){
if(score>=90) return new RiskAssessment(score,Level.CRITICAL,factors,Action.BLOCK);
if(score>=70) return new RiskAssessment(score,Level.HIGH,factors,Action.REVIEW);
if(score>=40) return new RiskAssessment(score,Level.MEDIUM,factors,Action.STEP_UP);
return new RiskAssessment(score,Level.LOW,factors,Action.ALLOW);
}
}
Teststrategie: Grenzwerttests prüfen jede Stufe. Replay- und Kalibrierungstests vergleichen Ergebnisse mit gekennzeichneten Fällen; Versionswechsel müssen im Audit und in reproduzierbaren Testdaten sichtbar bleiben.
Anomalieerkennung vergleicht Beobachtungen mit erwartetem Verhalten und liefert einen Befund, keine endgültige Geschäftsentscheidung. Wird die Anwendung direkt an eine Regelengine oder ein ML-Produkt gekoppelt, wandern proprietäre Features, Kategorien und Ausfallsemantik in den Use Case. Ein Port hält die Grenze klein und austauschbar.
ein ML-Score steuert direkt Blockierung oder Auszahlung
Training, Modellversion und Aktualität fehlen im Ergebnis
Provider-Ausfall führt zu unkontrolliertem Standardverhalten
Sicherer Refactoring-Ablauf
Fachlich relevante Beobachtungen und minimale Daten festlegen.
Finding mit Score, Kategorie, Evidenz, Modellversion und Zeit definieren.
Feature Engineering und Provideraufruf in Adapter verschieben.
Ausfall und Unsicherheit an eine separate Risk Policy übergeben.
Shadow-, Replay- und Drift-Überwachung vor automatischer Reaktion etablieren.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Erkennungsverfahren oder externen Plattformen. Wenn eine zentrale Detection-Plattform ausschließlich Ereignisse publiziert, kann ein stabiler Eventvertrag statt eines synchronen Ports geeigneter sein.
Risiken und typische Fehlanwendungen
Anomalie bedeutet nicht automatisch Angriff. Personenbezogene Telemetrie muss minimiert werden; Modell- und Datenänderungen können Drift erzeugen. Blockierungen ohne Review- oder Recovery-Pfad sind riskant.
Enterprise-Java-Beispiel
Der Zahlungsdienst übergibt Betragsabweichung, Gerätewechsel und Frequenz als typisierte Beobachtung. Der Adapter liefert einen Finding; die Risk Policy kombiniert ihn mit Kunden- und Prozesskontext, bevor eine Maßnahme gewählt wird.
Bei Sicherheitsvorfällen müssen Sperren, Widerrufe, Isolation, Benachrichtigung und Beweissicherung kontrolliert ausgelöst werden. Direkte Serviceaufrufe aus Alert-Handlern sind schwer wiederholbar und verlieren Reihenfolge, Idempotenz und Verantwortlichkeit. Ein Incident Command beschreibt eine beabsichtigte, auditierbare Aktion mit stabiler Incident-ID.
Erkennungsmerkmale
Alert-Handler ruft mehrere Systeme nacheinander ohne Wiederholschutz auf
manuelle und automatische Maßnahmen verwenden unterschiedliche Wege
Incident-ID, Grund und anfordernde Rolle fehlen im technischen Auftrag
Sicherer Refactoring-Ablauf
Erlaubte Reaktionsaktionen und notwendige Freigaben definieren.
Command mit Incident-ID, Subjekt, Aktion, Grund und Idempotenzschlüssel modellieren.
Ausführung in einen Handler oder Workflow mit Statusübergängen verschieben.
Seiteneffekte über Ports ausführen und Ergebnis je Schritt protokollieren.
Wiederholung, Teilfehler, Reihenfolge, Kompensation und manuelle Übernahme testen.
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Reaktionsschritten, automatisierten Playbooks oder verteilten Systemen. Eine lokale, atomare Sperre kann direkt ausgeführt werden, sofern Audit und Fehlerbehandlung klar bleiben.
Risiken und typische Fehlanwendungen
Automatisierung darf keine Beweise zerstören oder unverhältnismäßige Aktionen auslösen. Commands benötigen Autorisierung, Priorität und gegebenenfalls Vier-Augen-Freigabe. Idempotenz darf fachlich verschiedene Wiederholungen nicht fälschlich zusammenfassen.
Enterprise-Java-Beispiel
Ein bestätigter Tokenmissbrauch erzeugt Commands zum Widerruf der Sitzung, Isolieren des Geräts und Sichern relevanter Auditdaten. Jeder Schritt ist idempotent, statusgeführt und kann nach einem Teilausfall sicher fortgesetzt werden.
Abgrenzung und verwandte Muster
Command, Process Manager, Incident Workflow, Outbox, Revocation Registry.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Security Incident Command
// Pattern: Command / Workflow - Incident-Reaktionen werden wiederholbar und auditierbar.
public record SecurityIncidentCommand(String incidentId, Action action, String subjectId, Instant requestedAt, String reason) {
public enum Action { SUSPEND, REVOKE, ISOLATE, NOTIFY, PRESERVE_EVIDENCE }
public SecurityIncidentCommand { if(incidentId.isBlank()||subjectId.isBlank()||reason.isBlank()) throw new IllegalArgumentException(); }
public String idempotencyKey(){ return incidentId+":"+action+":"+subjectId; }
}
Teststrategie: Workflowtests simulieren Wiederholung, Teilausfall und verspätete Antworten. Autorisierungstests prüfen automatische und manuelle Aktionen; Audit-Tests sichern vollständige Reihenfolge ohne unnötige sensible Nutzdaten.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class SecurityIncidentCommandTest { @Test void contract(){ var c=new SecurityIncidentCommand("i1",SecurityIncidentCommand.Action.REVOKE,"u1",java.time.Instant.EPOCH,"compromise"); org.junit.jupiter.api.Assertions.assertEquals("i1:REVOKE:u1",c.idempotencyKey()); } }
Deutschsprachige Fachgrundlagen
Kapitel 27 Tenant-Isolation und Zero-Trust-Grenzen10 Refactorings
Refactorings 261–270 führen Tenant-Isolation, delegierte Fähigkeiten, privilegierte Workflows, Access Reviews und explizite Trust Boundaries ein.
Refactoring-Katalog · Abschnitt 12
Identity Governance und Zero-Trust-Grenzen
Refactorings 261–270 führen Tenant-Isolation, delegierte Fähigkeiten, privilegierte Workflows, Access Reviews und explizite Trust Boundaries ein.
In mandantenfähigen Enterprise-Systemen ist die Tenant-Grenze eine fachliche Sicherheitsinvariante. Bleibt sie als String-Vergleich in Repositorys, Services und Filtern verteilt, entstehen leicht Querzugriffe, inkonsistente Sonderfälle und schwer prüfbare Umgehungspfade. Eine Tenant Isolation Policy bündelt die Entscheidung über Akteur, Ressource und Operation an einer expliziten, testbaren Grenze.
Erkennungsmerkmale
Tenant-IDs werden an vielen Stellen direkt verglichen
Repository-Filter und Service-Prüfung widersprechen sich
Support- oder Admin-Sonderwege umgehen normale Grenzen
Cross-Tenant-Zugriffe sind nur über Logsuche nachvollziehbar
Sicherer Refactoring-Ablauf
bestehende Tenant-Prüfungen und Sonderfälle inventarisieren
Request und Decision als typisierte Werte definieren
Default-Deny sowie erlaubte Ausnahmen ausdrücklich modellieren
Policy an Controller-, Service- und Repository-Grenzen erzwingen
Negativ-, Mutation- und Mandantenwechseltests ergänzen
Wann sinnvoll - und wann nicht?
Sinnvoll bei SaaS-, Plattform- und Shared-Database-Systemen. Für vollständig getrennte Deployments ohne gemeinsame Daten- oder Prozessgrenzen kann die Policy kleiner ausfallen.
Risiken und typische Fehlanwendungen
Eine zentrale Policy darf nicht nur als Hilfsmethode existieren, während alte Direktvergleiche weiterlaufen. Besonders gefährlich sind globale Administratorrollen ohne begrenzten Scope und fehlende Objektprüfung.
Enterprise-Java-Beispiel
Ein Claims-Service bewertet vor jedem Lesen und Ändern, ob Bearbeiter-Tenant, Schaden-Tenant und Operation zusammenpassen. Supportzugriffe benötigen einen separat dokumentierten Ausnahmegrund.
Abgrenzung und verwandte Muster
Specification, Policy Enforcement Point, Resource Scope, Default Deny.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Extract Tenant Isolation Policy
// Pattern: Policy / Specification - Mandantengrenzen werden zentral und testbar entschieden.
public final class TenantIsolationPolicy {
public record Request(String actorTenant, String resourceTenant, String operation) {
public Request { if(actorTenant.isBlank()||resourceTenant.isBlank()||operation.isBlank()) throw new IllegalArgumentException(); }
}
public record Decision(boolean allowed, String reason) {}
public Decision evaluate(Request r) {
return r.actorTenant().equals(r.resourceTenant())
? new Decision(true, "same-tenant")
: new Decision(false, "cross-tenant-denied");
}
}
Teststrategie: Tabellentests decken Same-Tenant, Cross-Tenant, Supportausnahme und fehlende Tenant-ID ab. Integrationstests prüfen, dass auch Repository-Abfragen keinen fremden Datensatz liefern.
Unstrukturierte Ressourcenpfade vermischen Tenant, Typ und ID und lassen Präfixvergleiche wie `/customer/12` gegen `/customer/123` zu. Ein Resource Scope Value Object trennt die Bestandteile, validiert sie beim Erzeugen und stellt semantische Vergleichsoperationen bereit.
Erkennungsmerkmale
Scopes werden als Pfad oder zusammengesetzter String transportiert
startsWith- oder contains-Prüfungen entscheiden über Zugriff
Mandant und Ressourcentyp sind nicht getrennt auswertbar
Scope-Fehler erscheinen erst in produktiven Autorisierungsfällen
Sicherer Refactoring-Ablauf
verwendete Scope-Formate und Wildcards erfassen
Tenant, Ressourcentyp und ID als Pflichtwerte modellieren
kanonische Gleichheit und contains-Semantik definieren
Parser nur an Integrationsgrenzen zulassen
alte String-Prüfungen schrittweise entfernen
Wann sinnvoll - und wann nicht?
Sinnvoll, sobald Ressourcen über mehrere Module oder Tokens referenziert werden. Bei einem einzelnen, lokal typisierten Identifier kann ein bestehender Value Type genügen.
Risiken und typische Fehlanwendungen
contains darf keine implizite Hierarchie vortäuschen. Wildcards, globale Scopes und Tenant-übergreifende Ressourcen brauchen eigene, restriktive Typen.
Enterprise-Java-Beispiel
Ein Dokumentendienst wandelt externe Claim-Pfade im Adapter in ResourceScope um; der Use Case arbeitet nur noch mit validierten Werten.
Abgrenzung und verwandte Muster
Value Object, Permission Object, Capability, Authorization Policy.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Resource Scope
// Pattern: Value Object - Ressourcengrenzen erhalten eine validierte Identität.
public record ResourceScope(String tenantId, String resourceType, String resourceId) {
public ResourceScope {
if(tenantId.isBlank()||resourceType.isBlank()||resourceId.isBlank()) throw new IllegalArgumentException();
}
public boolean contains(ResourceScope other){ return tenantId.equals(other.tenantId) && resourceType.equals(other.resourceType); }
}
Teststrategie: Property-basierte Tests prüfen Gleichheit, Kanonisierung und ungültige IDs. Sicherheitsfälle decken Präfixkollisionen und Cross-Tenant-Scope ab.
263. Extract Privileged Access Workflow Workflow / State Machine
Fachliche Einordnung
Privilegierter Zugriff ist kein einzelner Boolean, sondern ein zeitlich begrenzter Prozess aus Antrag, Genehmigung, Aktivierung und Ablauf. Eine explizite State Machine verhindert direkte Sprünge und macht jede Zustandsänderung nachvollziehbar.
Erkennungsmerkmale
Genehmigung und Aktivierung erfolgen in getrennten Services ohne gemeinsamen Zustand
Ablaufzeiten werden nur in Hintergrundjobs geprüft
direkte Datenbankupdates setzen privilegierte Rollen
abgelehnte oder abgelaufene Anträge können reaktiviert werden
Sicherer Refactoring-Ablauf
fachliche Zustände und erlaubte Übergänge definieren
Invarianten für Genehmiger, Dauer und Zweck festlegen
Übergänge in Methoden des Workflows kapseln
Zeit über Clock/Port statt Instant.now kontrollieren
Persistenz und Audit auf Zustandsereignisse ausrichten
Wann sinnvoll - und wann nicht?
Sinnvoll für Just-in-Time-Administration, Produktionszugriff und sensible Betriebsrollen. Ein dauerhaftes, niedrig privilegiertes Standardrecht benötigt keinen solchen Workflow.
Risiken und typische Fehlanwendungen
Zeitprüfung im Domänenobjekt darf nicht untestbar an die Systemuhr gekoppelt sein. Selbstgenehmigung und Wiederaktivierung abgelaufener Grants müssen ausgeschlossen werden.
Enterprise-Java-Beispiel
Ein SRE beantragt 20 Minuten Produktionszugriff; nach Vier-Augen-Freigabe wird der Grant aktiviert und automatisch abgelaufen.
Abgrenzung und verwandte Muster
State Machine, Process Manager, Break Glass Policy, Temporal Grant.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Extract Privileged Access Workflow
// Pattern: Workflow / State Machine - Privilegierter Zugriff folgt überprüfbaren Übergängen.
public final class PrivilegedAccessWorkflow {
public enum State { REQUESTED, APPROVED, ACTIVE, EXPIRED, REJECTED }
private State state=State.REQUESTED;
private Instant validUntil;
public State state(){ return state; }
public void approve(){ require(State.REQUESTED); state=State.APPROVED; }
public void activate(Instant until){ require(State.APPROVED); validUntil=until; state=State.ACTIVE; }
public void expire(Instant now){ if(state==State.ACTIVE && !now.isBefore(validUntil)) state=State.EXPIRED; }
private void require(State expected){ if(state!=expected) throw new IllegalStateException(state.name()); }
}
Teststrategie: Übergangstests prüfen erlaubte und verbotene Zustandswechsel, Ablaufgrenzen, doppelte Genehmigung und Wiederholung nach Persistenzfehlern.
264. Introduce Delegation Grant Value Object / Capability
Fachliche Einordnung
Delegation ist fachlich mehr als eine Rolle mit Ablaufdatum. Ein Delegation Grant hält Aussteller, Empfänger, Capability, Ressourcen-Scope und Gültigkeit als unveränderlichen Wert zusammen und verhindert unkontrollierte Rechteweitergabe.
Erkennungsmerkmale
delegierte Rollen verlieren Aussteller und Zweck
Scope wird getrennt vom Grant gespeichert
Ablauf wird nur beim Erzeugen, nicht beim Verwenden geprüft
Empfänger kann mehr delegieren als der Aussteller besitzt
Sicherer Refactoring-Ablauf
zulässige delegierbare Fähigkeiten bestimmen
Grant als unveränderlichen Fachwert modellieren
Scope- und Zeitprüfung in validFor bündeln
Weiterdelegation ausdrücklich verbieten oder begrenzen
Widerruf und Audit über stabile Grant-ID ergänzen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Stellvertretung, Service-zu-Service-Fähigkeiten und zeitweiser Aufgabenübernahme. Nicht nötig für dauerhaft zugewiesene Standardrollen.
Risiken und typische Fehlanwendungen
Ein Grant darf nie mehr Rechte enthalten als der Aussteller wirksam besitzt. Uhrzeit, Zeitzone, Widerruf und Scope-Unterordnung müssen konsistent ausgewertet werden.
Enterprise-Java-Beispiel
Eine Teamleitung delegiert die Freigabe eines konkreten Schadensfalls bis Schichtende an einen Vertreter, nicht die gesamte Managerrolle.
Abgrenzung und verwandte Muster
Capability, Value Object, Temporal Authorization, Resource Scope.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Delegation Grant
// Pattern: Value Object / Capability - Delegation wird begrenzt, zeitlich gültig und auditierbar.
public record DelegationGrant(String issuer, String subject, String capability, ResourceScope scope, Instant validUntil) {
public DelegationGrant { if(issuer.isBlank()||subject.isBlank()||capability.isBlank()||scope==null||validUntil==null) throw new IllegalArgumentException(); }
public boolean validFor(String actor, String requestedCapability, ResourceScope requestedScope, Instant now){
return subject.equals(actor) && capability.equals(requestedCapability) && scope.contains(requestedScope) && now.isBefore(validUntil);
}
}
Break-Glass-Zugriffe sind notwendige Ausnahmewege, dürfen aber normale Kontrollen nicht unsichtbar umgehen. Eine eigene Policy bewertet Incident-Bezug, Genehmigung, maximale Dauer und Folgepflichten wie Benachrichtigung und Review.
Erkennungsmerkmale
versteckte Feature-Flags oder Admin-Header öffnen Notfallzugriff
kein Incident oder Genehmiger wird erfasst
Notfallrechte bleiben nach Ende des Vorfalls aktiv
Security erfährt erst nachträglich von der Nutzung
Sicherer Refactoring-Ablauf
zulässige Notfallszenarien und Maximaldauer festlegen
Request und Decision mit Incident-ID und Genehmigung modellieren
technischen Ausnahmeweg vom Normalweg trennen
automatische Benachrichtigung, Ablauf und Nachreview erzwingen
Missbrauchs- und Wiederholtests durchführen
Wann sinnvoll - und wann nicht?
Sinnvoll für kritische Produktions- oder Patienten-/Finanzsysteme mit echter Betriebsnotwendigkeit. Nicht als bequeme Alternative zu regulären Berechtigungsprozessen.
Risiken und typische Fehlanwendungen
Break Glass ohne unabhängiges Audit wird zur Hintertür. Die Policy muss restriktiv, zeitlich begrenzt und gegen Selbstgenehmigung geschützt sein.
Enterprise-Java-Beispiel
Bei einem Produktionsausfall erhält ein benannter Operator 15 Minuten Zugriff auf einen begrenzten Diagnose-Scope; Security und Incident Lead werden automatisch informiert.
package com.aydinsude.workbench.catalog;
import java.time.Duration;
// Refactoring: Extract Break Glass Policy
// Pattern: Policy / Command - Notfallzugriff wird eng begrenzt und auditierbar.
public final class BreakGlassPolicy {
public record Request(String actor, String incidentId, Duration duration, boolean approverPresent) {}
public record Decision(boolean allowed, Duration grantedFor, boolean notifySecurity, String reason) {}
public Decision evaluate(Request r){
if(r.actor()==null||r.actor().isBlank()||r.incidentId()==null||r.incidentId().isBlank()) return new Decision(false,Duration.ZERO,false,"missing-evidence");
if(!r.approverPresent() || r.duration().compareTo(Duration.ofMinutes(30))>0) return new Decision(false,Duration.ZERO,true,"approval-or-duration");
return new Decision(true,r.duration(),true,"incident-response");
}
}
Teststrategie: Negativtests prüfen fehlenden Incident, zu lange Dauer, fehlenden Genehmiger und Wiederverwendung alter Requests. End-to-End-Tests sichern automatisches Ablaufverhalten.
Ein reines allow/deny verliert, welche Policy-Version, Faktoren und Gründe zur Entscheidung führten. Authorization Evidence konserviert die Entscheidungsgrundlage für Audit, Support und reproduzierbare Analyse, ohne die Policy selbst in Logs nachzubauen.
Erkennungsmerkmale
Auditlogs enthalten nur Ergebnis und Benutzer-ID
Policyänderungen machen alte Entscheidungen unerklärbar
Support kann abgelehnte Zugriffe nicht reproduzieren
sensible Rohattribute werden unkontrolliert protokolliert
Sicherer Refactoring-Ablauf
minimal erforderliche Evidenzfelder definieren
Policy-Version und Faktorbezeichnungen stabilisieren
sensible Werte pseudonymisieren oder weglassen
Evidence atomar mit Entscheidung speichern
Retention, Zugriff und Export der Evidenz regeln
Wann sinnvoll - und wann nicht?
Sinnvoll bei regulierten oder komplexen Autorisierungsentscheidungen. Für triviale lokale Checks kann ein strukturierter Reason Code ausreichen.
Risiken und typische Fehlanwendungen
Evidenz darf kein zweites Geheimnis- oder Personendatenlager werden. Nur entscheidungsrelevante, minimierte Informationen speichern und Integrität schützen.
Enterprise-Java-Beispiel
Eine Zahlungsfreigabe speichert Policy-Version, geprüfte Faktoren und Reason Code; bei Reklamation lässt sich die damalige Entscheidung nachvollziehen.
Abgrenzung und verwandte Muster
Decision Object, Audit Log, Policy Decision Point, Observability.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant; import java.util.List;
// Refactoring: Introduce Authorization Evidence
// Pattern: Decision Object - Autorisierung bleibt mit ihrer Beweiskette nachvollziehbar.
public record AuthorizationEvidence(boolean allowed, String policyVersion, List<String> evaluatedFactors, String reason, Instant decidedAt) {
public AuthorizationEvidence { if(policyVersion.isBlank()||reason.isBlank()||decidedAt==null) throw new IllegalArgumentException(); evaluatedFactors=List.copyOf(evaluatedFactors); }
}
Teststrategie: Vertragstests sichern unveränderliche Faktorenlisten, Pflichtfelder und Policy-Version. Datenschutztests prüfen, dass Tokens und Rohattribute nicht enthalten sind.
Berechtigungsabgleiche werden oft als unübersichtliche Schleifen implementiert, die Änderungen sofort ausführen. Ein Entitlement Reconciler erzeugt zuerst einen expliziten Plan aus grant, revoke und unchanged; Ausführung, Freigabe und Audit können danach kontrolliert erfolgen.
Erkennungsmerkmale
periodische Jobs ändern Rechte während des Vergleichs
Teilfehler erzeugen unbekannten Zwischenstand
gleiche Differenzen werden bei jedem Lauf erneut ausgelöst
manuelle und automatische Quellen widersprechen sich
Sicherer Refactoring-Ablauf
gewünschten und tatsächlichen Zustand eindeutig bestimmen
reinen Reconcile-Schritt ohne Seiteneffekte extrahieren
Plan als unveränderliches Ergebnis modellieren
Ausführung idempotent und separat implementieren
Konflikte, Ownership und Drift-Metriken ergänzen
Wann sinnvoll - und wann nicht?
Sinnvoll bei IAM-Synchronisation, Gruppenabgleich und deklarativen Plattformen. Für eine einzelne atomare Rollenänderung wäre der Reconciler überdimensioniert.
Risiken und typische Fehlanwendungen
Falsche Quelle der Wahrheit kann massenhaft Rechte entziehen. Revoke-Pläne brauchen Schutzschwellen, Dry Run und gegebenenfalls Freigabe.
Enterprise-Java-Beispiel
Ein nächtlicher Job vergleicht HR-Sollrollen mit Identity-Provider-Istrollen und erzeugt einen prüfbaren Änderungsplan.
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Extract Entitlement Reconciler
// Pattern: Reconciliation Loop - Soll/Ist-Abweichungen werden als expliziter Plan behandelt.
public final class EntitlementReconciler {
public record Plan(Set<String> grant, Set<String> revoke, Set<String> unchanged){ public Plan { grant=Set.copyOf(grant); revoke=Set.copyOf(revoke); unchanged=Set.copyOf(unchanged); } }
public Plan reconcile(Set<String> desired, Set<String> actual){
var grant=new java.util.HashSet<>(desired); grant.removeAll(actual);
var revoke=new java.util.HashSet<>(actual); revoke.removeAll(desired);
var unchanged=new java.util.HashSet<>(desired); unchanged.retainAll(actual);
return new Plan(grant,revoke,unchanged);
}
}
Teststrategie: Mengentests prüfen grant/revoke/unchanged, leere Mengen, Wiederholung und große Datenmengen. Safety-Tests blockieren ungewöhnlich große Revoke-Pläne.
Access Reviews bestehen aus vielen Einzelentscheidungen, gehören aber fachlich zu einer Kampagne mit Fälligkeit, Verantwortlichen und Abschlussregel. Ein Aggregate verhindert den Abschluss mit offenen Items und bündelt Statusänderungen konsistent.
269. Extract Identity Proofing Port Port / Adapter
Fachliche Einordnung
Identitätsnachweis ist externe Infrastruktur mit unterschiedlichen Providern, Qualitätsstufen und Fehlerbildern. Ein Port trennt den Onboarding-Use-Case von Video-Ident, Dokumentenprüfung oder eID und liefert ein typisiertes Ergebnis.
Erkennungsmerkmale
Provider-SDKs erscheinen im Domänen- oder Application-Code
fachliche Logik reagiert auf providerinterne Statuscodes
Providerwechsel erfordert Änderungen in mehreren Modulen
Tests benötigen echte externe Identitätsdienste
Sicherer Refactoring-Ablauf
fachlichen Request und erforderliches Assurance Level definieren
providerneutrales Resultat mit Referenz und Grund modellieren
Port im Application Layer einführen
SDK und Mapping in Adapter verschieben
Vertragstests für alle Adapter aufbauen
Wann sinnvoll - und wann nicht?
Sinnvoll bei externen Identitäts- oder KYC-Providern. Bei rein interner, stabiler Verzeichnisprüfung kann ein vorhandener Identity-Port erweitert werden.
Risiken und typische Fehlanwendungen
Der Port darf Providerdetails nicht nur umbenennen. Assurance Levels, manuelle Prüfung und asynchrone Ergebnisse müssen fachlich sauber abgebildet werden.
Enterprise-Java-Beispiel
Das Kunden-Onboarding fordert Level substantial; Adapter A nutzt eID, Adapter B Video-Ident, der Use Case bleibt unverändert.
270. Introduce Trust Boundary Map Architecture Decision Model
Fachliche Einordnung
Architekturdiagramme zeigen Zonen, erzwingen sie aber nicht. Eine Trust Boundary Map macht erlaubte Übergänge und erforderliche Kontrollen maschinenlesbar und kann Architekturtests, Deployment-Prüfungen und Security Reviews unterstützen.
Erkennungsmerkmale
Netzwerk- und Modulgrenzen existieren nur in Folien
neue Integrationen umgehen Gateway oder Authentisierung
erforderliche Kontrollen sind nicht versioniert
Security Reviews erkennen Drift erst spät
Sicherer Refactoring-Ablauf
reale Zonen und Datenflüsse inventarisieren
Transition mit erforderlichen Controls modellieren
Default-Deny für unbekannte Übergänge festlegen
Map in Architektur- und Deploymenttests einbinden
Ausnahmen als dokumentierte Entscheidungen behandeln
Wann sinnvoll - und wann nicht?
Sinnvoll bei verteilten Systemen, Zero-Trust- und Plattformarchitekturen. Für eine kleine In-Process-Anwendung genügt meist eine Modulabhängigkeitsregel.
Risiken und typische Fehlanwendungen
Das Modell darf nicht von der realen Infrastruktur abweichen. Ein dokumentierter Übergang ohne technisch durchgesetzte Kontrolle erzeugt falsche Sicherheit.
Enterprise-Java-Beispiel
Ein Zugriff von PUBLIC nach DATA ist unzulässig; PUBLIC nach EDGE verlangt TLS und Rate Limit, EDGE nach APPLICATION zusätzlich starke Identität.
Abgrenzung und verwandte Muster
Architecture Decision Model, Zero Trust, Policy as Code, Fitness Function.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.*;
// Refactoring: Introduce Trust Boundary Map
// Pattern: Architecture Decision Model - Zonenübergänge werden explizit und prüfbar.
public final class TrustBoundaryMap {
public enum Zone { PUBLIC, EDGE, APPLICATION, DATA, SECURITY }
public record Transition(Zone from, Zone to, Set<String> controls){ public Transition { controls=Set.copyOf(controls); } }
private final Set<Transition> allowed;
public TrustBoundaryMap(Collection<Transition> allowed){ this.allowed=Set.copyOf(allowed); }
public boolean permits(Zone from, Zone to, String control){ return allowed.stream().anyMatch(t->t.from()==from && t.to()==to && t.controls().contains(control)); }
}
Teststrategie: Architekturtests prüfen erlaubte und verbotene Übergänge sowie Pflichtkontrollen. Deploymenttests vergleichen deklarierte Routen mit der Map.
271. Extract Policy Enforcement Point Policy / Facade
Fachliche Einordnung
Verteilte Prüfungen in Controllern, Services und Repositorys führen zu Lücken und inkonsistenten Reason Codes. Ein Policy Enforcement Point bildet eine klare Grenze, an der jeder geschützte Use Case eine typisierte Anfrage bewertet und die Entscheidung tatsächlich erzwingt.
Erkennungsmerkmale
gleiche Aktion wird an verschiedenen Stellen unterschiedlich geprüft
Tests prüfen Policy, aber nicht deren Durchsetzung
Sicherer Refactoring-Ablauf
geschützte Eintrittspunkte und Aktionen inventarisieren
Request/Decision-Vertrag definieren
PEP vor dem Use Case platzieren
Policy-Auswertung über Interface anbinden
Bypass-Pfade entfernen und Enforcement-Tests ergänzen
Wann sinnvoll - und wann nicht?
Sinnvoll bei mehreren Use Cases und zentraler Policy-Entscheidung. Nicht als globaler God Service für fachlich unterschiedliche Sicherheitsdomänen.
Risiken und typische Fehlanwendungen
Ein PEP darf keine fachlichen Objektprüfungen verlieren. Caching und Fehlerbehandlung müssen fail closed sein; technische Ausfälle dürfen nicht zu allow führen.
Enterprise-Java-Beispiel
Jeder Claims-Use-Case ruft authorize(principal, action, claimId) auf; ein Deny beendet den Ablauf vor Repositoryzugriff.
Abgrenzung und verwandte Muster
Policy Enforcement Point, Policy Decision Point, Facade, Default Deny.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Objects;
// Refactoring: Extract Policy Enforcement Point
// Pattern: Policy / Facade - Autorisierungsentscheidungen werden an einer zentralen Grenze erzwungen.
public final class PolicyEnforcementPoint {
public interface Policy { Decision evaluate(Request request); }
public record Request(String principalId, String action, String resourceId) {
public Request { if(principalId.isBlank() || action.isBlank() || resourceId.isBlank()) throw new IllegalArgumentException(); }
}
public record Decision(boolean allowed, String reason) {}
private final Policy policy;
public PolicyEnforcementPoint(Policy policy){ this.policy=Objects.requireNonNull(policy); }
public Decision authorize(Request request){ return policy.evaluate(request); }
}
Teststrategie: Integrationstests rufen Use Cases auch ohne Controller auf und erwarten Deny. Fehler- und Timeouttests sichern Fail-Closed-Verhalten.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class PolicyEnforcementPointTest { @Test void contract(){ org.junit.jupiter.api.Assertions.assertTrue(new PolicyEnforcementPoint(r -> new PolicyEnforcementPoint.Decision(true,"ok")).authorize(new PolicyEnforcementPoint.Request("u","read","r")).allowed()); } }
272. Introduce Attribute-Based Access Control Specification / Decision Object
Fachliche Einordnung
Immer feinere Rollen können Ressourceneigenschaften, Mandant, Netzwerkvertrauen oder Risiko nicht sauber ausdrücken. ABAC bewertet explizite Subject-, Resource- und Environment-Attribute und liefert eine begründete Entscheidung.
Erkennungsmerkmale
Rollenanzahl wächst für jede Kombination aus Region, Abteilung und Kontext
Policy enthält verschachtelte Rollen-Sonderfälle
Ressourcenattribute werden erst nach der Entscheidung geprüft
Umgebungskontext ist implizit in Infrastrukturcode versteckt
Sicherer Refactoring-Ablauf
relevante Attribute und vertrauenswürdige Quellen bestimmen
Context unveränderlich und vollständig modellieren
Regeln als kombinierbare Specifications implementieren
fehlende Attribute restriktiv behandeln
Policy-Version, Evidenz und Performance messen
Wann sinnvoll - und wann nicht?
Sinnvoll bei dynamischen, kontextabhängigen Entscheidungen. Für wenige stabile Berechtigungen bleibt RBAC oft verständlicher.
Risiken und typische Fehlanwendungen
Unkontrollierte Attribute erzeugen schwer erklärbare Policies. Herkunft, Aktualität und Manipulationsschutz jedes Attributs müssen bekannt sein.
Enterprise-Java-Beispiel
Ein Sachbearbeiter darf einen Schaden nur im eigenen Tenant und aus einem vertrauenswürdigen Netz bearbeiten; beide Bedingungen sind explizite Attribute.
Abgrenzung und verwandte Muster
ABAC, Specification, Decision Object, Policy Information Point.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Map;
// Refactoring: Introduce Attribute-Based Access Control
// Pattern: Specification / Decision Object - Zugriff wird aus typisierten Attributen entschieden.
public final class AttributeBasedAccessControl {
public record Context(Map<String,String> subject, Map<String,String> resource, Map<String,String> environment) {
public Context { subject=Map.copyOf(subject); resource=Map.copyOf(resource); environment=Map.copyOf(environment); }
}
public record Decision(boolean allowed, String reason) {}
public Decision evaluate(Context c){
boolean sameTenant = c.subject().getOrDefault("tenant","").equals(c.resource().getOrDefault("tenant","!"));
boolean trusted = "trusted".equals(c.environment().get("network"));
return new Decision(sameTenant && trusted, sameTenant && trusted ? "attributes-match" : "attribute-denied");
}
}
Teststrategie: Tabellentests kombinieren Tenant, Ressource, Netzwerk und fehlende Attribute. Mutationstests prüfen, dass jede Bedingung wirklich wirksam ist.
273. Extract Consent Evidence Store Repository / Audit Log
Fachliche Einordnung
Ein Boolean kann nicht belegen, welcher Text, Zweck, Kanal und Zeitpunkt einer Einwilligung zugrunde lagen. Ein append-only Consent Evidence Store bewahrt unveränderliche Evidenz und ermöglicht Widerruf sowie historische Rekonstruktion.
Erkennungsmerkmale
consent=true überschreibt frühere Zustände
Policy-Version oder Quelle fehlen
Widerruf löscht den ursprünglichen Nachweis
mehrere Systeme führen widersprüchliche Consent-Stände
Sicherer Refactoring-Ablauf
Evidenzfelder und Zweckschlüssel definieren
append-only Repository-Vertrag extrahieren
aktiven Status aus Historie ableiten
Integrität, Zeitquelle und Aufbewahrung regeln
Adapter für bestehende Consent-Daten migrieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei rechtlich oder regulatorisch relevanten Einwilligungen. Für rein technische, nicht personenbezogene Featurepräferenzen ist ein einfacherer Preference Store ausreichend.
Risiken und typische Fehlanwendungen
Evidenz muss minimiert, unveränderlich und zugriffsgeschützt sein. Ein Store ersetzt keine Prüfung, ob die Einwilligung fachlich wirksam und freiwillig war.
Enterprise-Java-Beispiel
Marketing-Einwilligung speichert Zweck, Textversion, Quelle und Zeitpunkt; der spätere Widerruf wird als neues Ereignis angehängt.
274. Introduce Data Access Purpose Value Object / Policy
Fachliche Einordnung
Technische Operationen wie READ oder EXPORT erklären nicht, warum personenbezogene oder sensible Daten benötigt werden. Ein Data Access Purpose macht den fachlichen Zweck zu einem validierten Wert und koppelt ihn an erlaubte Datenklassen.
Erkennungsmerkmale
Datenzugriffe werden nur mit CRUD-Operation protokolliert
gleicher Endpunkt dient vielen fachlichen Zwecken
Exportpfade umgehen Zweckprüfung
Purpose wird als freier Kommentar übergeben
Sicherer Refactoring-Ablauf
stabile fachliche Zwecke katalogisieren
Purpose und erlaubte Datenklassen als Value Object modellieren
Zweck am Use-Case-Eingang verpflichtend machen
Policy vor Datenauflösung prüfen
Audit und Reporting auf Purpose Codes ausrichten
Wann sinnvoll - und wann nicht?
Sinnvoll bei Datenschutz, Datenplattformen und bereichsübergreifenden APIs. Nicht als Ersatz für Berechtigung, Rechtsgrundlage oder Datenminimierung.
Risiken und typische Fehlanwendungen
Zu breite Zwecke wie business oder support sind wertlos. Zweckcodes müssen governancefähig, versioniert und gegen missbräuchliche Auswahl geschützt sein.
Enterprise-Java-Beispiel
Ein Support-Use-Case darf Kontaktdaten für INCIDENT_SUPPORT lesen, aber keine Gesundheitsdaten oder Marketingprofile.
Abgrenzung und verwandte Muster
Value Object, Purpose Limitation, Authorization Policy, Data Classification.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Set;
// Refactoring: Introduce Data Access Purpose
// Pattern: Value Object / Policy - Zweckbindung wird als fachlicher Wert prüfbar.
public record DataAccessPurpose(String code, Set<String> allowedDataClasses) {
public DataAccessPurpose {
if(code==null || code.isBlank()) throw new IllegalArgumentException("code");
allowedDataClasses=Set.copyOf(allowedDataClasses);
}
public boolean permits(String dataClass){ return allowedDataClasses.contains(dataClass); }
}
Teststrategie: Tests prüfen erlaubte und verbotene Datenklassen, unbekannte Zwecke und unveränderliche Mengen. End-to-End-Tests verifizieren Audit-Purpose.
275. Extract Data Loss Prevention Port Port / Adapter
Fachliche Einordnung
DLP-Erkennung ist Infrastruktur und variiert zwischen Providern. Ein Port hält Export- und Upload-Use-Cases frei von SDKs, Regex-Listen und providerinternen Kategorien und liefert typisierte Findings sowie eine fachliche Entscheidung.
Erkennungsmerkmale
Controller enthält DLP-SDK-Aufrufe
Regex-Listen sind über mehrere Services verteilt
Providerstatus wird direkt an Benutzer ausgegeben
Tests benötigen Cloud-Zugang oder echte Dateien
Sicherer Refactoring-Ablauf
zu prüfende Content-Metadaten und Größenlimits festlegen
Finding und ScanResult providerneutral modellieren
Port vor Export/Upload einführen
Provideradapter und lokale Testimplementierung bauen
Timeout, Quarantäne und Fail-Closed-Regel definieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei Datei-, Nachrichten- und Datenexporten mit sensiblen Inhalten. Für kleine, rein interne Textvalidierung kann eine lokale Specification genügen.
Risiken und typische Fehlanwendungen
DLP ist probabilistisch und darf nicht als einzige Schutzschicht gelten. Timeouts, große Inhalte, False Positives und Quarantäne benötigen explizite Behandlung.
Enterprise-Java-Beispiel
Vor einem Dokumentexport scannt der Use Case den Inhalt über den Port; kritische Findings blockieren, mittlere führen in manuelle Prüfung.
276. Introduce Security Exception Workflow Workflow / State Machine
Fachliche Einordnung
Sicherheitsausnahmen per E-Mail haben keinen konsistenten Zustand, keine automatische Befristung und kein sauberes Widerrufsmodell. Ein Workflow macht Antrag, Risiko, Genehmigung, Ablauf und Revocation explizit.
Erkennungsmerkmale
Ausnahmen besitzen kein Ablaufdatum
Genehmigungen sind nicht mit Risiko oder Kontrollkompensation verbunden
abgelaufene Ausnahmen bleiben technisch aktiv
Widerruf und Verlängerung überschreiben Historie
Sicherer Refactoring-Ablauf
Zustände und Pflichtinformationen definieren
Genehmigung und Dauer begrenzen
Workflowmethoden für approve/reject/revoke/expire kapseln
technische Enforcement-Konfiguration aus Status ableiten
Eskalation und regelmäßige Review einbauen
Wann sinnvoll - und wann nicht?
Sinnvoll für dokumentierte Abweichungen von Security Baselines. Nicht für dauerhafte Architekturentscheidungen, die als Standard neu bewertet werden müssen.
Risiken und typische Fehlanwendungen
Exception-Prozesse dürfen keine Schatten-Policy werden. Unbefristete oder automatisch verlängerte Ausnahmen sind besonders kritisch.
Enterprise-Java-Beispiel
Eine veraltete Schnittstelle erhält für 30 Tage eine genehmigte TLS-Ausnahme mit Kompensationskontrolle und festem Migrationsowner.
Abgrenzung und verwandte Muster
Workflow, State Machine, Risk Acceptance, Expiring Grant.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
// Refactoring: Introduce Security Exception Workflow
// Pattern: Workflow / State Machine - Ausnahmen werden zeitlich begrenzt und nachvollziehbar verwaltet.
public final class SecurityExceptionWorkflow {
public enum State { REQUESTED, APPROVED, REJECTED, EXPIRED, REVOKED }
private State state=State.REQUESTED; private Instant validUntil;
public void approve(Instant until){ if(state!=State.REQUESTED || !until.isAfter(Instant.now())) throw new IllegalStateException(); state=State.APPROVED; validUntil=until; }
public void reject(){ if(state!=State.REQUESTED) throw new IllegalStateException(); state=State.REJECTED; }
public void revoke(){ if(state!=State.APPROVED) throw new IllegalStateException(); state=State.REVOKED; }
public boolean active(Instant now){ if(state==State.APPROVED && !now.isBefore(validUntil)) state=State.EXPIRED; return state==State.APPROVED; }
public State state(){ return state; }
}
Teststrategie: Zustandstests prüfen Genehmigung, Ablehnung, Ablauf, Widerruf, Verlängerungsverbot und Zeitgrenzen mit kontrollierter Clock.
Lange if-Ketten vermischen mehrere regulatorische Anforderungen und liefern kaum nachvollziehbare Gründe. Einzelne Compliance Rules können kombiniert, separat getestet und mit strukturierten Reasons ausgewertet werden.
Erkennungsmerkmale
eine Methode enthält viele unabhängige Regelfälle
Änderung einer Regel beeinflusst andere Zweige
Ergebnis ist nur boolean ohne Begründung
gleiche Regel wird in mehreren Prozessen kopiert
Sicherer Refactoring-Ablauf
Regeln nach fachlicher Aussage trennen
Result mit compliant und Reasons definieren
Komposition wie and/or/not bewusst implementieren
Regelkontext und Version stabilisieren
Entscheidungsevidenz und Tests je Regel ergänzen
Wann sinnvoll - und wann nicht?
Sinnvoll bei vielen unabhängigen, kombinierbaren Prüfungen. Für einen kleinen linearen Algorithmus wäre ein Specification-Framework unnötig.
Risiken und typische Fehlanwendungen
Zu feingranulare Regeln können Kontext verlieren. Reason Codes müssen stabil bleiben; Komposition darf widersprüchliche oder doppelte Gründe nicht verschleiern.
Enterprise-Java-Beispiel
Eine Aufbewahrungsfreigabe kombiniert Region, Datenklasse und Legal-Hold-Regel und liefert alle verletzten Anforderungen.
Normale Retention darf Daten nicht löschen, die wegen Untersuchung oder Rechtsstreit auf Legal Hold stehen. Ein eigenes Aggregat hält Fall, betroffene Ressourcen, Aktivstatus und Freigabe zusammen und übersteuert Löschentscheidungen.
Erkennungsmerkmale
Retention-Jobs kennen laufende Verfahren nicht
Sperrlisten liegen als manuelle Tabellen vor
Freigabe löscht oder überschreibt die Historie
Teilressourcen eines Falls werden übersehen
Sicherer Refactoring-Ablauf
Scope und Identität eines Holds definieren
Hold als unveränderliche Zuordnung plus kontrollierte Release-Aktion modellieren
Deletion Policy vor jeder physischen Löschung abfragen
Änderungen auditieren und gegen unberechtigte Freigabe schützen
Migration bestehender Sperrlisten verifizieren
Wann sinnvoll - und wann nicht?
Sinnvoll in dokumenten-, revisions- und rechtsrelevanten Systemen. Nicht für normale fachliche Sperren ohne rechtlichen Charakter.
Risiken und typische Fehlanwendungen
Zu breite Holds blockieren unnötig große Datenmengen; zu enge verlieren Beweise. Release benötigt starke Autorisierung und unveränderliches Audit.
Enterprise-Java-Beispiel
Alle Dokumente eines Streitfalls werden vor dem Retention-Job geprüft und bleiben bis zur autorisierten Freigabe unverändert erhalten.
Fest verdrahtete Algorithmen und Schlüssellängen erschweren Migration bei neuen Vorgaben oder Schwachstellen. Eine versionierte Cryptographic Agility Policy bewertet zulässige Verfahren, Mindeststärken und Übergangsfristen zentral.
Erkennungsmerkmale
Algorithmusnamen und Key Sizes stehen als Konstanten in vielen Services
Legacy- und Zielverfahren können nicht parallel bewertet werden
Ablauffristen werden manuell verfolgt
preferred-Entscheidung ist nicht testbar oder versioniert
Sicherer Refactoring-Ablauf
eingesetzte Verfahren und Datenformate inventarisieren
Algorithmusmetadaten und Fristen modellieren
Policy getrennt von Kryptografieimplementierung einführen
Dual-Read/Dual-Write- oder Re-Encryption-Strategie planen
Telemetrie und Abschaltung alter Verfahren testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei langlebigen Daten, mehreren Services und Compliance-Vorgaben. Für eine einzelne lokale Hashfunktion kann eine kleinere Konfiguration ausreichen.
Risiken und typische Fehlanwendungen
Policy-Auswahl ersetzt keine sichere Implementierung und kein Key Management. Ungeprüfte Algorithmen dürfen nicht allein durch Konfiguration aktivierbar sein.
Enterprise-Java-Beispiel
Ein Tokenservice akzeptiert während einer Übergangsfrist alte Signaturen, signiert neu aber ausschließlich mit dem bevorzugten Verfahren.
Abgrenzung und verwandte Muster
Strategy, Policy, Crypto Agility, Key Rotation.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
import java.util.Map;
// Refactoring: Extract Cryptographic Agility Policy
// Pattern: Strategy / Policy - Kryptografie kann ohne Fachcodeänderung migriert werden.
public final class CryptographicAgilityPolicy {
public record Algorithm(String name, int minimumKeyBits, Instant allowedUntil) {}
private final Map<String,Algorithm> algorithms;
public CryptographicAgilityPolicy(Map<String,Algorithm> algorithms){ this.algorithms=Map.copyOf(algorithms); }
public boolean allowed(String name, int keyBits, Instant now){ var a=algorithms.get(name); return a!=null && keyBits>=a.minimumKeyBits() && now.isBefore(a.allowedUntil()); }
public String preferred(){ return algorithms.values().stream().max(java.util.Comparator.comparingInt(Algorithm::minimumKeyBits)).orElseThrow().name(); }
}
Teststrategie: Zeitgrenzentests prüfen Mindeststärke, Ablauf, unbekannte Verfahren und preferred. Integrationsprüfungen sichern Migration alter Daten.
Wenn eine Entscheidung mehrere Live-Systeme nacheinander abfragt, können Geräte-, Kontroll- und Risikostatus währenddessen wechseln. Ein unveränderlicher Security Posture Snapshot friert die relevante Sicht mit Erfassungszeitpunkt ein und macht Entscheidungen reproduzierbar.
Erkennungsmerkmale
Risk Score und Controls werden separat geladen
ein Request sieht unterschiedliche Zeitstände
Audit kann die damalige Lage nicht rekonstruieren
Map-Inhalte werden nach Erzeugung weiter verändert
Sicherer Refactoring-Ablauf
entscheidungsrelevante Kontrollen und Aktualitätsgrenze bestimmen
Snapshot unveränderlich und defensiv kopiert modellieren
Erfassung in einen koordinierten Application Service verlagern
Staleness und fehlende Signale explizit bewerten
Snapshot-ID oder Hash in Decision Evidence referenzieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei Zero-Trust-, Geräte- und Risikoprüfungen mit mehreren Quellen. Für eine einzelne atomare Datenbankabfrage ist ein Snapshot oft nicht nötig.
Risiken und typische Fehlanwendungen
Ein Snapshot kann bereits beim Erzeugen veraltet sein. capturedAt, maximale Gültigkeit und unvollständige Controls müssen Teil der Policy sein.
Enterprise-Java-Beispiel
Vor einer privilegierten Aktion werden Gerätekonformität, MFA, Netzvertrauen und Risikoscore in einem Snapshot gebündelt und gemeinsam bewertet.
Abgrenzung und verwandte Muster
Immutable Snapshot, Decision Object, Zero Trust, Authorization Evidence.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
import java.util.Map;
// Refactoring: Introduce Security Posture Snapshot
// Pattern: Immutable Snapshot / Decision Object - Sicherheitsentscheidungen nutzen einen konsistenten Zustand.
public record SecurityPostureSnapshot(String subjectId, Instant capturedAt, Map<String,Boolean> controls, int riskScore) {
public SecurityPostureSnapshot {
if(subjectId.isBlank() || riskScore<0 || riskScore>100) throw new IllegalArgumentException();
controls=Map.copyOf(controls);
}
public boolean trusted(){ return riskScore<30 && controls.values().stream().allMatch(Boolean::booleanValue); }
}
Teststrategie: Tests prüfen defensive Map-Kopie, Scoregrenzen, fehlende/false Controls und Staleness in der aufrufenden Policy.
Kapitel 29 Audit, Evidenz und regulatorische Abläufe10 Refactorings
Refactorings 281–290 machen Audit-Trails, Evidenz, Data Lineage, Retention, Erasure, Breach Notification, Fristen, Kontrollwirksamkeit und Compliance-Snapshots explizit.
Refactoring-Katalog · Abschnitt 14
Audit, Evidenz und regulatorische Abläufe
Refactorings 281–290 machen Audit-Trails, Evidenz, Data Lineage, Retention, Erasure, Breach Notification, Fristen, Kontrollwirksamkeit und Compliance-Snapshots explizit.
290 von 300 Refactorings abgeschlossen
10 Refactorings offen.
281. Extract Audit Trail Port Port / Adapter
Fachliche Einordnung
Auditdaten sind fachliche Nachweise und nicht bloß technische Logzeilen. Werden Domänenservices direkt an Logger, SIEM-Clients oder Hersteller-SDKs gekoppelt, vermischen sich Fachentscheidung, Transportformat und Betriebsinfrastruktur. Ein Audit Trail Port hält den fachlichen Vertrag stabil und erlaubt manipulationsgeschützte, nachvollziehbare Ablage über austauschbare Adapter.
Erkennungsmerkmale
Domänencode importiert Logging- oder SIEM-Bibliotheken
Ereignisse bestehen aus frei formatierten Strings
Zeitpunkt, Akteur oder betroffenes Objekt fehlen
Tests müssen reale Infrastruktur nachbilden
Sicherer Refactoring-Ablauf
auditrelevante Fachereignisse und Pflichtattribute bestimmen
unveränderlichen Ereignisvertrag mit Zeit, Akteur, Subjekt und Typ definieren
Port im Anwendungskern platzieren und technische Adapter nach außen verschieben
Fehlerstrategie für nicht verfügbare Audit-Infrastruktur festlegen
Integrität, Reihenfolge und Aufbewahrung unabhängig testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei regulatorisch oder sicherheitsrelevanten Aktionen, privilegierten Zugriffen und fachlichen Zustandswechseln. Für reine Debug-Ausgaben ohne Nachweiswert ist ein eigener Audit-Port unnötig.
Risiken und typische Fehlanwendungen
Ein Port allein garantiert weder Unveränderbarkeit noch Vollständigkeit. Ereignisschema, Zeitsynchronisation, Mandantenbezug, Datenschutz und fehlgeschlagene Übertragung müssen verbindlich geregelt werden.
Enterprise-Java-Beispiel
Ein Schadenservice schreibt die Freigabe einer Hochrisikozahlung als typisiertes AuditEvent. Ein Adapter speichert den Datensatz revisionssicher und liefert ihn parallel an das SIEM, ohne dass der Fachservice Transportdetails kennt.
Abgrenzung und verwandte Muster
Ports and Adapters, Domain Event, Append-only Log, Outbox, Audit Evidence.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
import java.util.Map;
// Refactoring: Extract Audit Trail Port
// Pattern: Port / Adapter - Audit-Ziele bleiben austauschbare Infrastruktur.
public interface AuditTrailPort {
record AuditEvent(String type, String actor, String subject, Instant occurredAt, Map<String,String> attributes) {
public AuditEvent { if(type.isBlank() || actor.isBlank()) throw new IllegalArgumentException(); attributes=Map.copyOf(attributes); }
}
void append(AuditEvent event);
}
Teststrategie: Tests prüfen Pflichtfelder, defensive Kopien, Reihenfolge, Adapterfehler und dass kein technischer Logger Teil des fachlichen Vertrags ist.
282. Introduce Evidence Bundle Value Object / Composite
Fachliche Einordnung
Prüfnachweise verlieren ihren Wert, wenn Dateien, Prüfsummen, Herkunft und fachlicher Bezug getrennt abgelegt werden. Ein Evidence Bundle bündelt die zusammengehörigen Artefakte als unveränderliche Einheit und macht sichtbar, welche Belege eine konkrete Entscheidung tragen.
Erkennungsmerkmale
Nachweise liegen in verschiedenen Speichern ohne gemeinsame Identität
Dateinamen dienen als einzige Zuordnung
Prüfsummen oder Erfassungszeitpunkte fehlen
ein Bericht kann seine Quellen nicht vollständig benennen
Sicherer Refactoring-Ablauf
fachlichen Evidenzgegenstand und erforderliche Artefakttypen festlegen
Metadaten, Herkunft und kryptografische Prüfsumme typisieren
Bundle unveränderlich und vollständig validierbar modellieren
große Binärdaten über Referenzen statt im Aggregat führen
Erzeugung, Signatur und spätere Verifikation getrennt testen
Wann sinnvoll - und wann nicht?
Sinnvoll für Audits, Freigaben, Kontrollnachweise und Untersuchungen mit mehreren zusammengehörigen Belegen. Für ein einzelnes kurzlebiges Dokument ohne Nachweischarakter reicht eine einfache Dateiablage.
Risiken und typische Fehlanwendungen
Ein Bundle kann personenbezogene oder vertrauliche Daten bündeln und dadurch den Schutzbedarf erhöhen. Zugriff, Aufbewahrung, Löschung und Integritätsprüfung müssen deshalb gemeinsam entworfen werden.
Enterprise-Java-Beispiel
Für die Genehmigung eines Sonderzugriffs enthält das Bundle Antrag, Vier-Augen-Freigabe, Policy-Entscheidung, Systemzustand und Prüfsummen. Der Auditbericht referenziert genau diese unveränderliche Version.
Abgrenzung und verwandte Muster
Value Object, Composite, Content Addressing, Digital Signature, Chain of Custody.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.List;
// Refactoring: Introduce Evidence Bundle
// Pattern: Value Object / Composite - zusammengehörige Evidenz wird atomar behandelt.
public record EvidenceBundle(String controlId, String checksum, List<EvidenceItem> items) {
public record EvidenceItem(String name, String source, String mediaType) {}
public EvidenceBundle { if(controlId.isBlank() || checksum.isBlank() || items.isEmpty()) throw new IllegalArgumentException(); items=List.copyOf(items); }
public int size(){ return items.size(); }
}
Teststrategie: Tests prüfen Vollständigkeit, stabile Reihenfolge, unveränderliche Metadaten, Prüfsummenabweichungen und den Umgang mit fehlenden Artefakten.
Regulatorische Berichte ändern Format, Übertragungsweg und Empfängerspezifikation häufiger als die Fachlogik. Erzeugen Fachservices CSV-Spalten, XML-Strukturen oder API-Payloads selbst, wird jede Formatänderung zu einer Domänenänderung. Ein Reporting Adapter trennt fachlichen Bericht von äußerer Repräsentation.
Erkennungsmerkmale
Domänenservices kennen CSV-, XML- oder Behördenfelder
Mappingregeln sind über mehrere Services verteilt
Transportfehler verändern fachliche Transaktionen
Formatversionen lassen sich nicht parallel betreiben
Sicherer Refactoring-Ablauf
stabilen fachlichen Reporting-Port und Berichtstyp definieren
externes Schema in einem expliziten Mapper isolieren
Transport, Authentisierung und Wiederholung im Adapter kapseln
Formatversionen über Vertragstests absichern
fachliche Erzeugung und technische Zustellung getrennt beobachten
Wann sinnvoll - und wann nicht?
Sinnvoll bei Behördenmeldungen, Prüfberichten und externen Compliance-Schnittstellen. Für rein interne, stabile Projektionen kann ein eigener Adapter überdimensioniert sein.
Risiken und typische Fehlanwendungen
Der Adapter darf keine fachlichen Entscheidungen erfinden. Mappingverluste, Rundung, Zeitzonen, Zeichensätze und Versionierung sind besonders kritisch und brauchen nachvollziehbare Fehlerberichte.
Enterprise-Java-Beispiel
Der Kern erzeugt einen typisierten Geldwäschebericht. Ein Adapter mappt ihn wahlweise auf CSV-Version 2 oder eine REST-Schnittstelle und protokolliert die technische Zustellung separat.
Datenherkunft ist eine fachliche Eigenschaft, sobald Entscheidungen, Berichte oder Modelle auf transformierten Daten beruhen. Ohne Lineage lässt sich weder erklären, wo ein Wert entstand, noch welche Verarbeitung ihn verändert hat. Eine typisierte Lineage-Struktur macht Quelle und Transformationskette explizit.
Erkennungsmerkmale
Berichte enthalten Werte ohne nachvollziehbare Quelle
Transformationen sind nur in ETL-Skripten dokumentiert
Verantwortliche Systeme sind nicht eindeutig
Fehler lassen sich nicht bis zum Ursprung zurückverfolgen
Sicherer Refactoring-Ablauf
Quelle, Transformation, Zeitpunkt und Verantwortlichkeit als Knoten/Kanten modellieren
stabile Identitäten für Datensätze und Verarbeitungsschritte vergeben
Lineage beim Übergang zwischen Systemgrenzen fortschreiben
unveränderliche Historie statt überschreibbarer Metadaten verwenden
Vollständigkeit und Zyklen automatisiert prüfen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Reporting, ML, regulatorischen Datenflüssen und komplexen Integrationen. Für lokale, kurzlebige Berechnungen ohne Weitergabe kann vollständige Lineage zu teuer sein.
Risiken und typische Fehlanwendungen
Lineage darf nicht zur unkontrollierten Kopie sensibler Nutzdaten werden. Metadatenumfang, Aufbewahrung, Pseudonymisierung und Performance müssen bewusst begrenzt werden.
Enterprise-Java-Beispiel
Eine Risikokennzahl referenziert Quellsystem, Rohdatensatz, Normalisierung und Aggregation. Bei einer Abweichung kann der Prüfer den Wert bis zur ursprünglichen Buchung zurückverfolgen.
Abgrenzung und verwandte Muster
Graph, Provenance, Event Sourcing, Data Product, Audit Trail.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Instant;
import java.util.List;
// Refactoring: Introduce Data Lineage
// Pattern: Graph / Value Object - Datenherkunft wird explizit und nachvollziehbar.
public record DataLineage(String datasetId, List<Step> steps) {
public record Step(String operation, String sourceId, Instant at) {}
public DataLineage { if(datasetId.isBlank()) throw new IllegalArgumentException(); steps=List.copyOf(steps); }
public DataLineage append(Step step){ var copy=new java.util.ArrayList<>(steps); copy.add(step); return new DataLineage(datasetId,copy); }
}
Teststrategie: Tests prüfen lückenlose Ketten, stabile IDs, verbotene Zyklen, fehlende Quellen und dass Transformationen die Vorgängerreferenz erhalten.
285. Extract Retention Scheduler Policy / Scheduler Port
Fachliche Einordnung
Aufbewahrungsregeln sind fachliche Policies, Cron-Ausführung ist Infrastruktur. Werden Fristberechnung, Kalenderlogik, Auswahl und Löschung in einem Scheduler vermischt, sind Regeln kaum testbar und Betriebsfehler gefährlich. Die Trennung macht die Entscheidung deterministisch und die Ausführung steuerbar.
Erkennungsmerkmale
Cron-Code enthält Fristen und Ausnahmen als Konstanten
Zeitzonen und Geschäftskalender sind implizit
ein Job löscht direkt ohne Vorschau oder Freigabe
Tests hängen von Systemzeit und Scheduler ab
Sicherer Refactoring-Ablauf
Retention-Policy mit Clock und expliziten Fristregeln extrahieren
Kandidatenentscheidung von physischer Löschung trennen
Scheduler nur als auslösenden Port modellieren
Dry-Run, Sperren und Legal Holds vor Ausführung prüfen
Ergebnis, Umfang und Fehler pro Lauf auditieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei rechtlichen Aufbewahrungs- und Löschpflichten sowie großen Datenbeständen. Für rein technische Cache-TTLs genügt oft eine einfache Ablaufkonfiguration.
Risiken und typische Fehlanwendungen
Falsche Zeitzone, fehlende Sperren oder unvollständige Replikatkenntnis können unzulässige Löschung verursachen. Entscheidungen müssen reproduzierbar und vorab sichtbar sein.
Enterprise-Java-Beispiel
Die Policy bestimmt zum Stichtag alle löschbaren Schadenakten. Der Scheduler startet einen kontrollierten Lauf, der Legal Holds prüft, zuerst simuliert und anschließend batchweise löscht.
286. Introduce Erasure Workflow Workflow / State Machine
Fachliche Einordnung
Eine Löschanfrage ist selten ein einzelner Datenbankbefehl. Identitätsprüfung, gesetzliche Ausnahmen, verteilte Datenspeicher, Bestätigung und Nachweis bilden einen langlebigen Geschäftsprozess. Ein expliziter Workflow verhindert unsichtbare Teilerfolge und ermöglicht Wiederaufnahme.
Erkennungsmerkmale
Löschung erfolgt synchron über mehrere Systeme
Teilfehler bleiben ohne Status zurück
Ausnahmen und Sperren sind verstreute if-Abfragen
Betroffene erhalten keine belastbare Abschlussbestätigung
Sicherer Refactoring-Ablauf
Zustände und Übergänge von Eingang bis Abschluss modellieren
Prüfung, Freigabe, Ausführung und Verifikation als getrennte Schritte definieren
idempotente Kommandos und Wiederaufnahme vorsehen
Ausnahmen, Holds und nicht löschbare Daten explizit dokumentieren
Abschlussnachweis und Benachrichtigung an den Workflow koppeln
Wann sinnvoll - und wann nicht?
Sinnvoll für DSGVO-Löschung und verteilte Datenlandschaften. Für rein lokale Testdaten ohne rechtliche Relevanz ist eine State Machine meist unnötig.
Risiken und typische Fehlanwendungen
Ein Workflow kann fälschlich abgeschlossen melden, obwohl Replikate, Backups oder Drittsysteme fehlen. Vollständigkeitskriterien und Verantwortlichkeiten müssen fachlich definiert sein.
Enterprise-Java-Beispiel
Eine Kundenanfrage durchläuft Identitätsprüfung, Hold-Prüfung, Löschung in CRM, Archiv und Data Lake sowie abschließende Verifikation. Jeder Schritt ist wiederholbar und auditierbar.
Abgrenzung und verwandte Muster
Workflow, State Machine, Saga, Idempotent Command, Process Manager.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
// Refactoring: Introduce Erasure Workflow
// Pattern: Workflow / State Machine - Löschanforderungen erhalten kontrollierte Übergänge.
public final class ErasureWorkflow {
public enum State { REQUESTED, VERIFIED, BLOCKED, EXECUTED, CONFIRMED }
private State state=State.REQUESTED;
public void verify(boolean legalHold){ if(state!=State.REQUESTED) throw new IllegalStateException(); state=legalHold?State.BLOCKED:State.VERIFIED; }
public void execute(){ if(state!=State.VERIFIED) throw new IllegalStateException(); state=State.EXECUTED; }
public void confirm(){ if(state!=State.EXECUTED) throw new IllegalStateException(); state=State.CONFIRMED; }
public State state(){ return state; }
}
Teststrategie: Tests prüfen erlaubte Übergänge, Wiederholung, Teilausfälle, Holds, Fristüberschreitung und dass Abschluss erst nach Verifikation möglich ist.
Ob ein Vorfall meldepflichtig ist, hängt von Risiko, Datenarten, Betroffenen, Schutzmaßnahmen und Fristen ab. Verstreute Schwellenwerte führen zu widersprüchlichen Entscheidungen. Eine Breach Notification Policy liefert ein erklärbares Ergebnis mit Begründung und Empfängern.
Erkennungsmerkmale
Meldepflicht wird in mehreren Tickets oder Tabellen entschieden
Fristen sind als Magic Numbers implementiert
Risikoargumente werden nicht gespeichert
Empfängerlisten hängen von manueller Erinnerung ab
Sicherer Refactoring-Ablauf
Entscheidungskriterien und erforderliche Fakten typisieren
Policy als reine, versionierte Funktion formulieren
Ergebnis mit Begründungen, Frist und Empfängergruppe modellieren
fehlende Informationen als Unsicherheit statt false behandeln
Policy-Version und Evidenz im Incident hinterlegen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Datenschutz- und Sicherheitsvorfällen mit regulatorischer Bewertung. Für interne Betriebsstörungen ohne Personen- oder Meldebezug ist die Policy nicht zuständig.
Risiken und typische Fehlanwendungen
Automatisierung ersetzt keine rechtliche Bewertung. Die Policy muss konservativ mit Unsicherheit umgehen und eine Eskalation an Datenschutz, Recht und Incident Management ermöglichen.
Enterprise-Java-Beispiel
Ein Incident enthält Datenkategorie, Anzahl Betroffener, Verschlüsselungsstatus und Entdeckungszeit. Die Policy entscheidet meldepflichtig, berechnet die Frist und nennt Aufsicht sowie Betroffenenkommunikation.
288. Introduce Regulatory Deadline Value Object / Clock Policy
Fachliche Einordnung
Regulatorische Fristen sind mehr als eine Zahl von Stunden. Startauslöser, Zeitzone, Hemmung, Geschäftskalender und Überschreitung müssen gemeinsam modelliert werden. Ein Value Object verhindert uneinheitliche Datumsarithmetik und macht Fristentscheidungen reproduzierbar.
Erkennungsmerkmale
plusHours-Aufrufe sind über den Code verteilt
lokale Zeit und UTC werden vermischt
Startzeitpunkt ist fachlich unklar
Warn- und Eskalationspunkte fehlen
Sicherer Refactoring-Ablauf
fachlichen Fristbeginn und Zeitbasis festlegen
Dauer, Deadline und Warnschwellen in einem Value Object bündeln
Clock injizieren und lokale Systemzeit vermeiden
Hemmung oder Kalenderregeln explizit modellieren
Berechnung und Darstellung getrennt testen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Melde-, Antwort-, Aufbewahrungs- und Eskalationsfristen. Für rein technische Timeouts ist ein einfaches Duration-Objekt ausreichend.
Risiken und typische Fehlanwendungen
Ein korrektes Value Object kann eine falsch verstandene Rechtsregel nicht reparieren. Regelquelle, Version und Zuständigkeit müssen dokumentiert und überprüfbar sein.
Enterprise-Java-Beispiel
Für einen Datenschutzvorfall wird aus Kenntniszeitpunkt und Policy eine Deadline erzeugt. Das System zeigt verbleibende Zeit, Warnstufe und Überschreitung unabhängig von UI oder Scheduler.
Abgrenzung und verwandte Muster
Value Object, Clock, Temporal Policy, Deadline, Escalation.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.Duration;
import java.time.Instant;
// Refactoring: Introduce Regulatory Deadline
// Pattern: Value Object / Clock Policy - Fristlogik wird reproduzierbar und testbar.
public record RegulatoryDeadline(Instant startedAt, Duration allowed) {
public RegulatoryDeadline { if(allowed.isNegative() || allowed.isZero()) throw new IllegalArgumentException(); }
public Instant dueAt(){ return startedAt.plus(allowed); }
public boolean overdue(Instant now){ return now.isAfter(dueAt()); }
}
Teststrategie: Tests prüfen UTC/Lokalzeit, Sommerzeitwechsel, Grenzsekunden, Hemmung, bereits überschrittene Fristen und deterministische Clock-Nutzung.
289. Extract Control Effectiveness Evaluator Strategy / Scorecard
Fachliche Einordnung
Kontrollwirksamkeit darf nicht aus frei interpretierten Prozentwerten entstehen. Messmethode, Evidenz, Gewichtung und Schwellenwert müssen nachvollziehbar sein. Ein Evaluator kapselt die Bewertungsstrategie und liefert neben dem Score eine erklärbare Einstufung.
Erkennungsmerkmale
Kontrollbewertungen stammen aus manuellen Tabellen
verschiedene Teams verwenden andere Skalen
fehlende Evidenz wird als null oder 0 behandelt
ein Prozentwert enthält keine Begründung
Sicherer Refactoring-Ablauf
Bewertungsdimensionen und Skalen fachlich definieren
Evidenzanforderungen pro Kontrolle festlegen
Evaluator als austauschbare Strategie extrahieren
Score und qualitative Einstufung gemeinsam zurückgeben
Version, Datenstand und Begründung im Ergebnis speichern
Wann sinnvoll - und wann nicht?
Sinnvoll für interne Kontrollen, Security Controls und Compliance-Reife. Für binäre technische Health Checks wäre eine Scorecard unnötig komplex.
Risiken und typische Fehlanwendungen
Ein mathematisch präziser Score kann Scheingenauigkeit erzeugen. Gewichtungen, Unsicherheit und fehlende Evidenz müssen sichtbar bleiben und regelmäßig fachlich validiert werden.
Enterprise-Java-Beispiel
Der Evaluator bewertet MFA-Abdeckung, Ausnahmerate, Testnachweise und Aktualität. Das Ergebnis lautet teilweise wirksam und enthält die konkreten Defizite statt nur 72 Prozent.
package com.aydinsude.workbench.catalog;
import java.util.List;
// Refactoring: Extract Control Effectiveness Evaluator
// Pattern: Strategy / Scorecard - Kontrollwirksamkeit wird einheitlich berechnet.
public final class ControlEffectivenessEvaluator {
public record Observation(boolean passed, int weight) { public Observation { if(weight<=0) throw new IllegalArgumentException(); } }
public record Score(int percent, String rating) {}
public Score evaluate(List<Observation> observations){ int total=observations.stream().mapToInt(Observation::weight).sum(); int passed=observations.stream().filter(Observation::passed).mapToInt(Observation::weight).sum(); int pct=total==0?0:(passed*100/total); return new Score(pct,pct>=90?"effective":pct>=70?"partial":"ineffective"); }
}
Teststrategie: Tests prüfen Gewichtungen, fehlende Evidenz, Grenzwerte, Strategiewechsel und dass gleiche Eingangsdaten reproduzierbare Begründungen liefern.
Eine Compliance-Bewertung über Live-Systeme kann während der Abfrage ihren Zustand ändern. Ein versionierter Snapshot friert Datenstand, Policy-Version und Ergebnisse als belastbare Sicht ein. Dadurch lassen sich Berichte und Entscheidungen später exakt rekonstruieren.
Erkennungsmerkmale
Dashboards zeigen bei Wiederholung andere Zahlen
Abfragen kombinieren unterschiedliche Zeitstände
Policy-Versionen sind nicht am Ergebnis sichtbar
Audits können historische Bewertungen nicht reproduzieren
Sicherer Refactoring-Ablauf
Scope, Stichtag und erforderliche Datenquellen festlegen
Snapshot unveränderlich und versioniert modellieren
Datenaufnahme orchestrieren und unvollständige Quellen markieren
Bewertungen gegen genau diesen Snapshot ausführen
Hash, Erfassungszeit und Policy-Version im Bericht referenzieren
Wann sinnvoll - und wann nicht?
Sinnvoll für Audits, Managementberichte und Stichtagsbewertungen. Für rein operative Echtzeitüberwachung bleibt eine Live-Sicht geeigneter.
Risiken und typische Fehlanwendungen
Ein Snapshot ist nur so belastbar wie seine Vollständigkeit und Aktualität. Fehlende Quellen, verspätete Daten und nachträgliche Korrekturen müssen explizit gekennzeichnet werden.
Enterprise-Java-Beispiel
Zum Quartalsende werden Kontrollstatus, Ausnahmen, offene Maßnahmen und Evidenzstände in einem Snapshot gebündelt. Der Bericht bleibt auch nach späteren Systemänderungen reproduzierbar.
package com.aydinsude.workbench.catalog;
import java.time.Instant;
import java.util.Map;
// Refactoring: Introduce Compliance Snapshot
// Pattern: Immutable Snapshot / Aggregate - Berichte beruhen auf konsistentem Bewertungsstand.
public record ComplianceSnapshot(String scope, long version, Instant capturedAt, Map<String,String> controlRatings) {
public ComplianceSnapshot { if(scope.isBlank() || version<1) throw new IllegalArgumentException(); controlRatings=Map.copyOf(controlRatings); }
public boolean compliant(){ return controlRatings.values().stream().noneMatch("ineffective"::equals); }
}
Teststrategie: Tests prüfen defensive Kopien, stabile Version, fehlende Quellen, Hash-Stabilität und dass spätere Live-Änderungen den Snapshot nicht verändern.
Kapitel 30 Characterization Tests und sichere Migrations-Seams10 Refactorings
Refactorings 291–300 schließen den Master-Katalog mit Sicherheitsnetz, Baseline, Migration, Kompatibilität, Deprecation, Rollback, Datenplan, Feature-Cleanup, Architektur-Fitness und einer erklärbaren Readiness-Entscheidung ab.
Refactoring-Katalog · Abschnitt 15
Sichere Migration und Refactoring-Readiness
Refactorings 291–300 schließen den Master-Katalog mit Sicherheitsnetz, Baseline, Migration, Kompatibilität, Deprecation, Rollback, Datenplan, Feature-Cleanup, Architektur-Fitness und einer erklärbaren Readiness-Entscheidung ab.
300 von 300 Refactorings abgeschlossen
0 Refactorings offen · Katalog vollständig.
291. Extract Characterization Test Seam Characterization Test / Seam
Fachliche Einordnung
Vor einem Legacy-Refactoring muss das tatsächlich beobachtbare Verhalten eingefroren werden, besonders wenn Spezifikation und Tests fehlen. Ein Characterization Test Seam schafft einen kontrollierbaren Zugang zu statischen, zeitabhängigen oder externen Pfaden, ohne das Verhalten vorschnell umzubauen.
Erkennungsmerkmale
Legacy-Verhalten ist nur über produktive Infrastruktur erreichbar
niemand kann Soll- und Ist-Verhalten sicher unterscheiden
erste Umbauten ändern gleichzeitig Struktur und Ergebnis
Sicherer Refactoring-Ablauf
kritische Ein- und Ausgaben des bestehenden Pfads auswählen
kleinsten möglichen Seam um Zeit, Datei, Netzwerk oder statischen Aufruf legen
aktuelles Verhalten als Charakterisierungstest festhalten
bekannte Fehler bewusst als Baseline dokumentieren
erst danach schrittweise Struktur ändern und Tests grün halten
Wann sinnvoll - und wann nicht?
Sinnvoll vor riskanten Änderungen an schlecht getestetem Legacy-Code. Bei gut spezifiziertem Code mit belastbaren Unit- und Vertragstests ist ein zusätzlicher Characterization Seam oft nicht nötig.
Risiken und typische Fehlanwendungen
Charakterisierungstests konservieren auch falsches Verhalten. Sie sind Sicherheitsnetz, keine fachliche Freigabe; bekannte Abweichungen müssen sichtbar markiert und später gezielt geändert werden.
Enterprise-Java-Beispiel
Ein statischer Tarifrechner wird über einen kleinen Adapter erreichbar gemacht. Tests erfassen repräsentative Altverträge inklusive Rundungsbesonderheiten, bevor die Berechnung intern zerlegt wird.
package com.aydinsude.workbench.catalog;
import java.util.Objects;
// Refactoring: Extract Characterization Test Seam
// Pattern: Characterization Test / Seam - Legacy-Verhalten wird vor Änderungen beobachtbar.
public final class CharacterizationTestSeam {
@FunctionalInterface public interface LegacyOperation { String execute(String input); }
private final LegacyOperation operation;
public CharacterizationTestSeam(LegacyOperation operation){ this.operation=Objects.requireNonNull(operation); }
public Observation observe(String input){
try { return new Observation(input, operation.execute(input), null); }
catch(RuntimeException ex){ return new Observation(input, null, ex.getClass().getSimpleName()); }
}
public record Observation(String input, String output, String failureType) {
public Observation { if(input==null) throw new IllegalArgumentException("input"); }
public boolean successful(){ return failureType==null; }
}
}
Teststrategie: Tests prüfen reproduzierbare Eingaben, deterministische Zeit/IO, bekannte Sonderfälle und dass der Seam keine fachliche Logik hinzufügt.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class CharacterizationTestSeamTest { @Test void capturesCurrentBehavior(){ var o=new CharacterizationTestSeam(String::toUpperCase).observe("legacy"); org.junit.jupiter.api.Assertions.assertEquals("LEGACY",o.output()); } }
292. Introduce Behavioral Baseline Golden Master / Value Object
Fachliche Einordnung
Bei komplexen Ausgaben reichen einzelne Assertions häufig nicht aus. Eine Behavioral Baseline speichert die kanonische beobachtete Ausgabe samt Version und Prüfsumme, sodass strukturelle Änderungen gegen einen stabilen Golden Master verglichen werden können.
Erkennungsmerkmale
Vorher/Nachher wird manuell über Dateien verglichen
Ausgaben enthalten nicht deterministische Reihenfolge oder Zeitwerte
Baseline-Dateien haben keine Version oder Herkunft
Änderungen werden pauschal akzeptiert statt fachlich geprüft
Sicherer Refactoring-Ablauf
repräsentative Eingaben und beobachtbare Ausgaben festlegen
nicht fachliche Volatilität kanonisieren oder maskieren
Baseline mit Schema-, Daten- und Generatorversion versehen
Prüfsumme und Freigabeprozess etablieren
Abweichungen semantisch prüfen statt blind neu aufzuzeichnen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Berichten, Serialisierung, Parsern und Legacy-Berechnungen mit breiter Ausgabe. Für kleine reine Funktionen bleiben präzise Assertions verständlicher.
Risiken und typische Fehlanwendungen
Golden Masters können große, unverständliche Snapshots erzeugen. Unkontrolliertes Aktualisieren macht sie wertlos; Baseline-Änderungen brauchen Review und fachliche Begründung.
Enterprise-Java-Beispiel
Für 200 repräsentative Schadenfälle wird das kanonische Berechnungsergebnis versioniert. Nach dem Refactoring zeigt der Vergleich nur echte fachliche Abweichungen, nicht Zeitstempel oder Sortierreihenfolge.
Abgrenzung und verwandte Muster
Golden Master, Snapshot Test, Value Object, Approval Test, Characterization Test.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
// Refactoring: Introduce Behavioral Baseline
// Pattern: Golden Master / Value Object - Verhalten wird kanonisch und prüfbar festgehalten.
public record BehavioralBaseline(String scenario, String canonicalOutput, String sha256) {
public BehavioralBaseline {
if(scenario==null || scenario.isBlank() || canonicalOutput==null) throw new IllegalArgumentException();
String expected=hash(canonicalOutput); if(!expected.equals(sha256)) throw new IllegalArgumentException("checksum");
}
public static BehavioralBaseline capture(String scenario,String output){ String canonical=output.strip().replace("\r\n","\n"); return new BehavioralBaseline(scenario,canonical,hash(canonical)); }
public boolean matches(String output){ return sha256.equals(hash(output.strip().replace("\r\n","\n"))); }
private static String hash(String value){ try { var d=MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8)); return java.util.HexFormat.of().formatHex(d); } catch(Exception e){ throw new IllegalStateException(e); } }
}
Teststrategie: Tests prüfen Kanonisierung, Prüfsumme, Versionswechsel, gezielte Freigabe und aussagekräftige Diffs bei Abweichungen.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class BehavioralBaselineTest { @Test void comparesCanonicalOutput(){ var b=BehavioralBaseline.capture("invoice","A\nB"); org.junit.jupiter.api.Assertions.assertTrue(b.matches(" A\nB ")); } }
Während einer Migration dürfen Aufrufer nicht selbst entscheiden, ob Alt- oder Neusystem zuständig ist. Eine Migration Facade bündelt Routing, Fallback, Vergleich und Telemetrie hinter einem stabilen Vertrag und ermöglicht schrittweise Verlagerung nach dem Strangler-Prinzip.
Erkennungsmerkmale
jeder Client enthält eigene Alt/Neu-Schalter
Fallback-Regeln unterscheiden sich je Aufrufer
Vergleichsergebnisse werden nicht zentral gemessen
Rückbau des Altsystems erfordert Änderungen in vielen Clients
Sicherer Refactoring-Ablauf
stabilen fachlichen Fassadenvertrag definieren
Routingregel und Migrationskohorte zentralisieren
Shadow Read oder Ergebnisvergleich optional kapseln
Fallback nur für klar definierte Fehler zulassen
Metriken und Abschaltkriterien für den Altpfad festlegen
Wann sinnvoll - und wann nicht?
Sinnvoll bei schrittweiser Ablösung eines Legacy-Systems. Für einen atomaren Big-Bang-Wechsel ohne Parallelbetrieb ist die zusätzliche Fassade oft nicht erforderlich.
Risiken und typische Fehlanwendungen
Eine Fassade kann zum dauerhaften Dual-Run-Monolithen werden. Eigentümer, Enddatum, beobachtbare Qualitätskriterien und klare Entfernungsschritte sind notwendig.
Enterprise-Java-Beispiel
Alle Vertragsabfragen laufen über eine Fassade. Bestimmte Kundengruppen werden bereits aus dem neuen Service bedient; bei Abweichungen werden Ergebnisse verglichen und kontrolliert zurückgeroutet.
Abgrenzung und verwandte Muster
Facade, Strangler Fig, Branch by Abstraction, Routing Policy, Anti-Corruption Layer.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.util.Objects;
// Refactoring: Extract Migration Facade
// Pattern: Facade / Strangler - Migration bleibt hinter einem stabilen Vertrag kontrollierbar.
public final class MigrationFacade {
public interface Service { String process(String request); }
public enum Route { LEGACY, MODERN, SHADOW_COMPARE }
private final Service legacy, modern;
public MigrationFacade(Service legacy, Service modern){ this.legacy=Objects.requireNonNull(legacy); this.modern=Objects.requireNonNull(modern); }
public Result process(String request, Route route){
return switch(route){
case LEGACY -> new Result(legacy.process(request),true);
case MODERN -> new Result(modern.process(request),true);
case SHADOW_COMPARE -> { String a=legacy.process(request), b=modern.process(request); yield new Result(b,a.equals(b)); }
};
}
public record Result(String response, boolean equivalent) {}
}
Teststrategie: Tests prüfen Kohortenrouting, erlaubte Fallbacks, identische Verträge, Telemetrie und vollständige Abschaltbarkeit des Legacy-Pfads.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import org.junit.jupiter.api.Test;
class MigrationFacadeTest { @Test void shadowCompares(){ var f=new MigrationFacade(x->"ok",x->"ok"); org.junit.jupiter.api.Assertions.assertTrue(f.process("x",MigrationFacade.Route.SHADOW_COMPARE).equivalent()); } }
Ein neues Domänenmodell verliert seine Qualität, wenn es Legacy-Feldnamen, Nullkonventionen und Statuscodes übernimmt. Ein Compatibility Adapter übersetzt den alten Vertrag an einer klaren Grenze und schützt das neue Modell als Anti-Corruption Layer.
Erkennungsmerkmale
neue Klassen tragen alte technische Feldnamen
Null und Sondercodes sickern in die Domäne
Legacy-Enums werden überall importiert
jede Fachregel enthält Kompatibilitätsabfragen
Sicherer Refactoring-Ablauf
Legacy-Vertrag und neue Fachsprache explizit gegenüberstellen
Übersetzung in einem Adapter mit typisierten Fehlern kapseln
Null-, Default- und Statusregeln dokumentieren
bidirektionale Abbildung nur dort erlauben, wo nötig
Vertragstests mit realistischen Legacy-Nachrichten aufbauen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Migrationen, Fremdsystemen und langlebigen Altverträgen. Wenn beide Modelle bewusst dieselbe Sprache und Semantik besitzen, wäre ein Adapter nur zusätzliche Schicht.
Risiken und typische Fehlanwendungen
Ein Adapter darf Semantikverluste nicht verbergen. Nicht abbildbare Zustände brauchen explizite Fehler oder Quarantäne statt stiller Defaults.
Enterprise-Java-Beispiel
Der Adapter übersetzt alte Statuscodes 0, 4 und 9 in fachliche Zustände und behandelt leere Datumswerte explizit. Das neue Vertragsmodell bleibt frei von Altcode-Konventionen.
Abkündigung ist ein zeitlich gesteuerter Produkt- und Vertragsprozess. Verstreute Versions- und Datumsprüfungen führen zu uneinheitlichen Warnungen und überraschenden Sperren. Eine Deprecation Policy bündelt Ersatz, Fristen, Zielgruppen und erlaubte Übergangsphasen.
Erkennungsmerkmale
jede API prüft Sunset-Daten anders
Warnungen enthalten keinen Ersatzpfad
Sperren werden ohne Telemetrie aktiviert
Ausnahmen sind als dauerhafte Sonderfälle eingebaut
Sicherer Refactoring-Ablauf
abgekündigte Fähigkeit, Ersatz und betroffene Verträge benennen
Warn-, Sunset- und Sperrphase typisieren
Policy mit Clock und Versionen deterministisch machen
Nutzung und verbleibende Verbraucher messen
Ausnahmen befristen und Entfernungskriterium definieren
Wann sinnvoll - und wann nicht?
Sinnvoll für APIs, Events, Features und Datenformate mit Übergangszeit. Für sofort zu entfernenden internen toten Code ist eine Policy nicht nötig.
Risiken und typische Fehlanwendungen
Eine Deprecation Policy ohne Durchsetzung verlängert Altlasten. Umgekehrt kann zu aggressive Sperrung Verbraucher brechen; Telemetrie und Kommunikation sind Teil der Lösung.
Enterprise-Java-Beispiel
Version 1 einer Partner-API liefert zunächst Warnheader, später nur noch für genehmigte Ausnahmen und wird nach gemessener Nullnutzung gesperrt. Die Policy liefert für jede Anfrage eine erklärbare Phase.
Rollback darf nicht von Erinnerungen und implizitem Zustand abhängen. Ein Rollback Command kapselt die rückwärts gerichtete Aktion, ihren Ausgangssnapshot und ein idempotentes Ergebnis. Dadurch wird Rückabwicklung planbar, prüfbar und automatisierbar.
Erkennungsmerkmale
Betriebshandbuch enthält manuelle SQL-Schritte
Rollback benötigt unbekannten Vorzustand
Wiederholung kann zusätzlichen Schaden erzeugen
Erfolg wird nur anhand fehlender Fehlermeldung angenommen
Sicherer Refactoring-Ablauf
rücksetzbaren Umfang und notwendigen Vorzustand bestimmen
Snapshot oder Memento vor der Änderung erzeugen
Rollback als idempotentes Command modellieren
Vorbedingungen und irreversible Schritte explizit markieren
Ergebnis verifizieren und auditieren
Wann sinnvoll - und wann nicht?
Sinnvoll bei riskanten Releases, Migrationen und Konfigurationsänderungen. Für vollständig additive, folgenlose Änderungen kann ein explizites Rollback Command unnötig sein.
Risiken und typische Fehlanwendungen
Nicht jede Änderung ist reversibel. Datenverlust, externe Effekte und parallele Änderungen müssen erkannt werden; ein Rollback darf keinen falschen Sicherheitsanspruch erzeugen.
Enterprise-Java-Beispiel
Vor einer Tarifmigration wird der relevante Zustand versioniert. Das Rollback Command stellt nur unveränderte Datensätze zurück, ist wiederholbar und liefert eine Liste nicht sicher rücksetzbarer Konflikte.
297. Extract Data Migration Plan Pipeline / Migration Object
Fachliche Einordnung
Datenmigrationen sind Produktionssoftware und brauchen einen expliziten Plan. Lose Skripte ohne Vorbedingungen, Reihenfolge und Verifikation sind schwer reproduzierbar. Ein Data Migration Plan modelliert Schritte, Checkpoints und Abnahmekriterien als geordnete Pipeline.
Erkennungsmerkmale
Skripte werden manuell in unbekannter Reihenfolge ausgeführt
Vorbedingungen und Datenvolumen sind nicht dokumentiert
es gibt keine Checkpoints oder Wiederaufnahme
Erfolg wird nur über Exit-Code bewertet
Sicherer Refactoring-Ablauf
Quell- und Zielzustand sowie Invarianten festlegen
Schritte mit Vorbedingung, Aktion und Verifikation typisieren
idempotente Checkpoints und Wiederaufnahme definieren
Dry-Run, Stichprobe und Mengenabgleich integrieren
Rollback- oder Kompensationsstrategie pro Schritt dokumentieren
Wann sinnvoll - und wann nicht?
Sinnvoll für produktive Schema- und Bestandsdatenmigrationen. Für kleine lokale Entwicklungsdaten genügt ein einfaches einmaliges Skript.
Risiken und typische Fehlanwendungen
Ein sauberer Plan ersetzt keine Kapazitäts- und Laufzeitprüfung. Sperren, Replikation, Teilfortschritt und unerwartete Datenqualität müssen vorab getestet werden.
Enterprise-Java-Beispiel
Die Migration normalisiert Kundenschlüssel in fünf Schritten. Jeder Schritt prüft Vorbedingungen, schreibt einen Checkpoint und verifiziert Anzahl, Prüfsummen sowie fachliche Invarianten.
package com.aydinsude.workbench.catalog;
import java.util.List;
// Refactoring: Extract Data Migration Plan
// Pattern: Pipeline / Migration Object - Datenänderungen werden geordnet und verifizierbar.
public record DataMigrationPlan(String planId,List<Step> steps) {
public interface Step { String name(); boolean precondition(MigrationContext context); void apply(MigrationContext context); boolean verify(MigrationContext context); }
public interface MigrationContext { long count(String dataset); }
public DataMigrationPlan { if(planId.isBlank()||steps.isEmpty()) throw new IllegalArgumentException(); steps=List.copyOf(steps); }
public Result execute(MigrationContext context){ for(Step s:steps){ if(!s.precondition(context)) return new Result(false,s.name()+":precondition"); s.apply(context); if(!s.verify(context)) return new Result(false,s.name()+":verify"); } return new Result(true,"complete"); }
public record Result(boolean successful,String detail) {}
}
Teststrategie: Tests prüfen Reihenfolge, Wiederaufnahme, Idempotenz, Dry-Run, Mengenabgleich, Dateninvarianten und Verhalten nach Teilfehlern.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.util.List; import org.junit.jupiter.api.Test;
class DataMigrationPlanTest { @Test void executesVerifiedStep(){ var s=new DataMigrationPlan.Step(){ public String name(){return "copy";} public boolean precondition(DataMigrationPlan.MigrationContext c){return true;} public void apply(DataMigrationPlan.MigrationContext c){} public boolean verify(DataMigrationPlan.MigrationContext c){return true;} }; org.junit.jupiter.api.Assertions.assertTrue(new DataMigrationPlan("p",List.of(s)).execute(x->1).successful()); } }
Feature Toggles sind temporäre Migrationswerkzeuge, keine dauerhafte Architektur. Ohne Eigentümer, Ablaufdatum und Entfernungskriterium bleiben beide Codepfade bestehen. Eine Cleanup Policy macht technische Schuld sichtbar und entscheidet nachvollziehbar, wann Flag und Altpfad entfernt werden dürfen.
Erkennungsmerkmale
Toggles besitzen kein Ablaufdatum
niemand kennt den verantwortlichen Owner
Altpfade werden trotz 100-Prozent-Rollout weiter gepflegt
Tests müssen dauerhaft alle Kombinationen abdecken
Sicherer Refactoring-Ablauf
Owner, Zweck, Erzeugungsdatum und Zieltermin erfassen
Messkriterium für stabilen Rollout definieren
Abhängigkeiten und notwendige Rückfallfrist dokumentieren
Policy für warnen, überfällig und entfernbar implementieren
Entfernung als eigener geprüfter Refactoring-Schritt planen
Wann sinnvoll - und wann nicht?
Sinnvoll bei Release-, Experiment- und Migrations-Toggles. Für langfristige fachliche Konfigurationen ist ein Feature Toggle meist das falsche Modell.
Risiken und typische Fehlanwendungen
Zu frühes Entfernen nimmt die Rückfallmöglichkeit, zu spätes Entfernen vervielfacht Komplexität. Messdaten und klarer fachlicher Eigentümer sind entscheidend.
Enterprise-Java-Beispiel
Nach vier Wochen stabilem Vollrollout, null Fallbacks und abgeschlossener Datenmigration bewertet die Policy das Flag als entfernbar. Ein Ticket enthält Altpfad, Tests und Konfiguration, die gemeinsam gelöscht werden.
Abgrenzung und verwandte Muster
Policy, Feature Toggle, Technical Debt, Branch by Abstraction, Cleanup Refactoring.
Vorhandener Java-21-Zielcode
Vorhandener Java-21-Zielcode
package com.aydinsude.workbench.catalog;
import java.time.LocalDate;
// Refactoring: Introduce Feature Cleanup Policy
// Pattern: Policy / Feature Toggle - temporäre Varianten erhalten ein verbindliches Ende.
public record FeatureCleanupPolicy(String feature,String owner,LocalDate removeAfter) {
public FeatureCleanupPolicy { if(feature.isBlank()||owner.isBlank()||removeAfter==null) throw new IllegalArgumentException(); }
public Decision evaluate(LocalDate today,double enabledShare,int rollbackIncidents){
boolean ready=!today.isBefore(removeAfter)&&enabledShare>=1.0&&rollbackIncidents==0;
return new Decision(ready, ready?"remove-toggle":"retain-until-safe");
}
public record Decision(boolean removable,String reason) {}
}
Teststrategie: Tests prüfen Ablaufdatum, fehlenden Owner, Rolloutmetriken, Rückfallfrist und dass fachliche Langzeitkonfiguration nicht versehentlich als Toggle behandelt wird.
Vorhandener JUnit-Testvertrag
Vorhandener JUnit-Testvertrag
package com.aydinsude.workbench.catalog;
import java.time.LocalDate; import org.junit.jupiter.api.Test;
class FeatureCleanupPolicyTest { @Test void removesOnlyAfterStableRollout(){ var p=new FeatureCleanupPolicy("new-price","team",LocalDate.of(2026,1,1)); org.junit.jupiter.api.Assertions.assertTrue(p.evaluate(LocalDate.of(2026,2,1),1.0,0).removable()); } }
299. Extract Architecture Fitness Rule Specification / Fitness Function
Fachliche Einordnung
Architekturregeln, die nur in Dokumenten stehen, verlieren gegen den täglichen Code. Eine ausführbare Fitness Rule beschreibt erlaubte Abhängigkeiten oder Qualitätsgrenzen als Specification und liefert konkrete Verstöße im Build.
Erkennungsmerkmale
Schichten werden trotz Dokumentation zyklisch abhängig
eine präzise, technisch prüfbare Architekturabsicht auswählen
Scope und erlaubte Ausnahmen explizit definieren
Regel als deterministische Specification implementieren
Verstöße mit betroffenen Elementen und Begründung ausgeben
Regel in CI betreiben und Ausnahmen befristen
Wann sinnvoll - und wann nicht?
Sinnvoll für Modulgrenzen, Abhängigkeitsrichtungen, Namensregeln und messbare Qualitätsziele. Für subjektive Designurteile bleibt menschliches Review notwendig.
Risiken und typische Fehlanwendungen
Zu viele starre Regeln können sinnvolle Entwicklung blockieren. Fitness Functions müssen Architekturabsicht schützen, nicht Stilpräferenzen automatisieren.
Enterprise-Java-Beispiel
Eine Regel stellt sicher, dass Domänenmodule keine Spring- oder Persistenzpakete importieren. Der Build nennt jede verletzende Klasse und erlaubt nur dokumentierte, befristete Ausnahmen.
Ein großes Refactoring sollte erst starten, wenn Sicherheitsnetz, Beobachtbarkeit, Rückfall und begrenzter Änderungsumfang vorhanden sind. Eine Readiness Decision bündelt diese Faktoren als erklärbares Quality Gate statt einer diffusen Bauchentscheidung.
Erkennungsmerkmale
Refactorings beginnen ohne belastbare Tests
Rollback und Migrationspfad sind ungeklärt
Erfolgskriterien und Telemetrie fehlen
mehrere riskante Änderungen werden gleichzeitig geplant
Sicherer Refactoring-Ablauf
notwendige Kriterien für Tests, Scope, Rollback und Observability definieren
Evidenz pro Kriterium statt bloßer Selbstauskunft verlangen
Ergebnis als ready, conditional oder blocked modellieren
Auflagen und fehlende Nachweise im Decision Object zurückgeben
Entscheidung vor Start und vor Rollout erneut bewerten
Wann sinnvoll - und wann nicht?
Sinnvoll vor umfangreichen Legacy-, Daten- oder Architekturrefactorings. Für kleine lokale Umbenennungen wäre das Gate unverhältnismäßig.
Risiken und typische Fehlanwendungen
Ein Gate kann zur Bürokratie werden, wenn Kriterien nicht risikobasiert sind. Es darf fehlende technische Sicherheit nicht durch formale Häkchen ersetzen.
Enterprise-Java-Beispiel
Vor der Ablösung eines Kernmoduls bewertet das Gate Charakterisierungstests, Baseline, Migrationsfassade, Rollback, Metriken und Eigentümer. Das Ergebnis conditional nennt zwei konkrete Auflagen vor dem ersten Produktionsanteil.
Der Refactoring Catalog umfasst jetzt 300 nachvollziehbare Refactorings. Der Katalog ist vollständig. Weitere Arbeiten betreffen nur noch fachliche Konsolidierung, Navigation und Qualitätsprüfung.