Buchorientierung: fünf Teile, ein durchgehender Lernweg
Das Werk trennt jetzt Grundlagen, technische Spezialgebiete, durchgehende Fallstudien, Systemmodernisierung sowie Betrieb und Verantwortung. Die Nummerierung folgt ausschließlich dieser fachlichen Leserfolge.
Direkte Leserwege
Wähle das beobachtbare Problem. Der erste Link führt zur Entscheidungshilfe, der zweite direkt zur passenden großen Fallstudie.
Große Klasse oder unklare Geschäftslogik
Beginne mit Sicherheitsnetz, Ablaufzerlegung und fachlichen Typen.
Vorgehen bestimmenClaims-FallstudieFehlerhafter Wiederanlauf oder Checkpoints
Beginne mit Restart-Vertrag, Idempotenz und Reihenfolge der Seiteneffekte.
Vorgehen bestimmenBatch-FallstudieVersteckte Netzabhängigkeit oder SOAP-Vertrag
Beginne mit Characterization Tests, Port und Fehlerklassifikation.
Vorgehen bestimmenSOAP-FallstudieUnklar, welches Refactoring zuerst kommt
Vergleiche Risikotreiber, Pattern-Schwellen und bewusste Stopppunkte.
EntscheidungsatlasTechnikkatalogRefactoring verstehen
Vom beobachtbaren Verhalten über kleine Strukturverbesserungen bis zu stabilen fachlichen Grenzen.
Kapitel 1 Refactoring-Grundlagen8 Lerneinheiten
Große, klappbare Seite mit sicheren Transformationen und klaren Einsatzgrenzen.
Refactoring Master Workbench
Große, klappbare Seite mit sicheren Transformationen und klaren Einsatzgrenzen.
01. Refactoring-Katalog
| Smell | Erster Schritt | Mögliches Ziel |
|---|---|---|
| Long Method | Extract Method | klarer Ablauf / Command |
| Primitive Obsession | Value Object | fachliche Invarianten |
| Switch Statements | Decompose Conditional | Strategy oder State |
| Feature Envy | Move Method | kohärentes Fachobjekt |
02. Extract Method mit fachlichen Namen
if (customer.active && order.total.compareTo(limit) < 0 && !order.blocked) {
approve(order);
}if (isEligibleForAutomaticApproval(customer, order)) {
approve(order);
}Der neue Methodenname erklärt die Geschäftsentscheidung. Er versteckt nicht nur Syntax.
03. Guard Clauses
if (order.isEmpty()) return Rejection.emptyOrder(); if (!customer.isActive()) return Rejection.inactiveCustomer(); if (order.exceeds(limit)) return Rejection.creditLimit(); return approve(order);
04. Replace Primitive with Value Object
public record OrderId(String value) {
public OrderId {
if (value == null || value.isBlank()) throw new IllegalArgumentException("OrderId");
}
}05. Separate Query from Modifier
Eine Methode soll entweder Informationen liefern oder Zustand ändern. Die Trennung vereinfacht Tests und verhindert überraschende Doppelaufrufe.
Money total = pricingService.price(order); // Query repository.save(order.withTotal(total)); // Command
06. Replace Conditional with Polymorphism
Erst anwenden, wenn Zweige eigenständig wachsen oder unterschiedliche Abhängigkeiten besitzen. Für zwei stabile Einzeiler ist Polymorphismus oft zu teuer.
DiscountStrategy strategy = strategies.get(customer.tier()); return strategy.apply(subtotal);
07. Extract Interface / Port
public interface NotificationPort {
void orderConfirmed(Order order, Money total);
}Der Port wird aus einem konkreten Bedarf extrahiert. Er ist keine technische Universalabstraktion.
08. Refactoring-Sicherheitsnetz
Priorität: Characterization Tests für bestehendes Verhalten, fokussierte Unit Tests für extrahierte Regeln, wenige Integrationstests für Adapter.
@ParameterizedTest
@CsvSource({"999.99,false", "1000.00,true"})
void recognizesTierThreshold(String amount, boolean expected) {
assertEquals(expected, rule.matches(Money.eur(amount)));
}Kapitel 2 Code Smells erkennen und sicher beheben12 Praxislabore
Jedes Lab zeigt einen problematischen Enterprise-Java-Ausschnitt, das konkrete Refactoring, den Pattern-Bezug und eine kompakte themenspezifische SVG.
12 reale Code-Smell-Labs
Jedes Lab zeigt einen problematischen Enterprise-Java-Ausschnitt, das konkrete Refactoring, den Pattern-Bezug und eine kompakte themenspezifische SVG.
12 von 55 Labs abgeschlossen, 43 offen.
01. God Class Lab 01/12
Geruch
OrderBackofficeService bündelt Preis, Lager, Persistenz und Benachrichtigung.
Refactoring
Extract Class + Application Service
Pattern-Bezug
Facade
public Receipt submit(Order order) {
BigDecimal total = calculate(order);
jdbc.update("insert into orders ...");
inventory.reserve(order.lines());
mail.send(order.customerEmail(), "Bestellt");
return new Receipt(order.id(), total);
}public Receipt submit(Order order) {
Money total = pricing.calculate(order);
repository.save(order.withTotal(total));
inventory.reserve(order.lines());
events.publish(new OrderSubmitted(order.id(), total));
return Receipt.from(order, total);
}02. Long Method Lab 02/12
Geruch
Eine 180-Zeilen-Methode validiert, berechnet und persistiert einen Schadenfall.
Refactoring
Extract Method + Compose Method
Pattern-Bezug
Template Method
public Decision decide(Claim claim) {
if (claim.documents() == null || claim.documents().isEmpty()) {
return Decision.reject("DOCUMENTS_MISSING");
}
// 170 weitere Zeilen mit Risiko, Limits und Speicherung
}public Decision decide(Claim claim) {
validateDocuments(claim);
RiskScore risk = assessRisk(claim);
Decision decision = policy.decide(claim, risk);
audit.record(claim.id(), decision);
return decision;
}03. Primitive Obsession Lab 03/12
Geruch
IBAN, Betrag und Kundentyp werden als freie Strings übergeben.
Refactoring
Replace Primitive with Value Object
Pattern-Bezug
Value Object
void pay(String iban, BigDecimal amount, String currency, String customerType) {
if (!iban.startsWith("AT")) throw new IllegalArgumentException();
// ...
}void pay(Iban iban, Money amount, CustomerSegment segment) {
paymentPort.transfer(new PaymentInstruction(iban, amount, segment));
}04. Switch Explosion Lab 04/12
Geruch
Jedes neue Dokumentformat erweitert mehrere switch-Blöcke.
Refactoring
Replace Conditional with Polymorphism
Pattern-Bezug
Strategy + Factory
return switch (format) {
case "JSON" -> parseJson(payload);
case "CSV" -> parseCsv(payload);
case "XML" -> parseXml(payload);
default -> throw new UnsupportedOperationException(format);
};DocumentReader reader = readerRegistry.forFormat(envelope.format()); ParsedDocument document = reader.read(envelope); return pipeline.process(document);
05. Feature Envy Lab 05/12
Geruch
Der Service greift wiederholt auf fremde Felder zu, um Rabattfähigkeit zu bestimmen.
Refactoring
Move Method
Pattern-Bezug
Specification
boolean eligible(Order order) {
return order.customer().yearsActive() > 3
&& order.customer().complaintCount() == 0
&& order.total().compareTo(new BigDecimal("500")) > 0;
}boolean eligible(Order order) {
return loyaltySpecification.isSatisfiedBy(order);
}06. Data Clumps Lab 06/12
Geruch
Adresse, Land, Postleitzahl und Ort reisen immer gemeinsam durch APIs.
Refactoring
Introduce Parameter Object
Pattern-Bezug
Value Object
ShippingQuote quote(String street, String zip, String city, String country,
BigDecimal weight, String unit) { ... }ShippingQuote quote(PostalAddress destination, Weight weight) {
return shippingPort.quote(destination, weight);
}07. Shotgun Surgery Lab 07/12
Geruch
Eine neue Priorität verlangt Änderungen in Routing, SLA, UI und Benachrichtigung.
Refactoring
Encapsulate Change
Pattern-Bezug
Strategy + Policy
if (priority.equals("CRITICAL")) team = "MAJOR_INCIDENT";
if (priority.equals("CRITICAL")) due = now.plusMinutes(15);
if (priority.equals("CRITICAL")) notifyOnCall();SupportPolicy policy = policies.forPriority(ticket.priority()); Assignment assignment = policy.assign(ticket, clock.instant()); events.publishAll(assignment.events());
08. Divergent Change Lab 08/12
Geruch
PaymentOrchestrator ändert sich für Provider, Fraud-Regeln und Auditformat zugleich.
Refactoring
Separate Responsibilities
Pattern-Bezug
Adapter + Chain
class PaymentOrchestrator {
// Stripe HTTP, Fraud-Regeln, Retry, SQL und Audit-JSON
}FraudDecision decision = fraudChain.evaluate(command); PaymentProvider provider = providers.resolve(command.provider()); PaymentResult result = provider.charge(command); auditPort.record(PaymentAudit.from(command, result, decision));
09. Hidden Dependencies Lab 09/12
Geruch
Statische Uhr, UUID und SMTP-Aufruf machen Tests instabil.
Refactoring
Introduce Dependency
Pattern-Bezug
Ports and Adapters
String id = UUID.randomUUID().toString(); Instant now = Instant.now(); Mail.sendStatic(customer.email(), "Done");
String id = idGenerator.nextId(); Instant now = clock.instant(); notificationPort.send(new CompletionNotice(customer.id(), id, now));
10. Boolean Blindness Lab 10/12
Geruch
Mehrere boolean-Parameter machen Aufrufe unverständlich.
Refactoring
Replace Flag Argument
Pattern-Bezug
Command
process(ticket, true, false, true);
process(new TicketProcessingCommand(
ticket.id(), EscalationMode.IMMEDIATE, NotificationMode.CUSTOMER_AND_TEAM));11. Inappropriate Intimacy Lab 11/12
Geruch
Zwei Aggregate ändern gegenseitig interne Collections.
Refactoring
Move Method + Domain Service
Pattern-Bezug
Domain Service
order.mutableLines().remove(line); inventory.internalReservations().put(line.sku(), line.quantity());
reservationService.replaceLine(order, line, replacement);
12. Exception Control Flow Lab 12/12
Geruch
Fachliche Ablehnungen werden als technische Exceptions transportiert.
Refactoring
Replace Exception with Result
Pattern-Bezug
Result / Policy
try { approve(claim); } catch (LimitExceededException ex) {
return "MANUAL_REVIEW";
}ClaimDecision decision = approvalPolicy.evaluate(claim);
return switch (decision.status()) {
case APPROVED -> workflow.approve(claim);
case MANUAL_REVIEW -> workflow.queueForReview(claim);
case REJECTED -> workflow.reject(claim, decision.reason());
};Kapitel 3 SOLID und fachliche Grenzen10 Lerneinheiten
Zehn umfangreiche Java-Labs zeigen SOLID nicht als Merksatz, sondern als konkrete Refactoring-Entscheidung in Order, Payment, Claims, Support und Dokumentverarbeitung.
SOLID Masterclass
Zehn umfangreiche Java-Labs zeigen SOLID nicht als Merksatz, sondern als konkrete Refactoring-Entscheidung in Order, Payment, Claims, Support und Dokumentverarbeitung.
7 Labs offen · anschließend: Pattern Decision Workbench.
01. SRP: OrderApplicationService entlasten Lab 01/10
Verletzung
Eine Klasse validiert, berechnet, speichert und benachrichtigt.
Refactoring
Extract Class + Ports
Pattern-Bezug
Facade + Domain Service
public final class LegacyOrderService {
public Receipt place(OrderRequest request) {
validate(request);
BigDecimal total = calculate(request);
database.insert(request, total);
mail.send(request.customerEmail(), "Order accepted");
return new Receipt(request.id(), total);
}
}// Design Pattern: Application Service / Facade
public final class PlaceOrderUseCase {
private final OrderValidator validator;
private final PricingService pricing;
private final OrderRepository orders;
private final NotificationPort notifications;
public Receipt place(OrderRequest request) {
validator.validate(request);
Money total = pricing.totalFor(request.lines());
orders.save(Order.from(request, total));
notifications.orderAccepted(request.customerId(), total);
return new Receipt(request.id(), total);
}
}02. SRP: Audit aus der Fachlogik lösen Lab 02/10
Verletzung
Fachentscheidung und Auditformat ändern aus unterschiedlichen Gründen.
Refactoring
Extract Adapter
Pattern-Bezug
Decorator / Audit Port
public Decision approve(Claim claim) {
Decision result = rules.evaluate(claim);
Files.writeString(Path.of("audit.json"), toJson(claim, result));
return result;
}public Decision approve(Claim claim) {
Decision result = rules.evaluate(claim);
auditPort.record(ClaimAudit.from(claim, result));
return result;
}03. OCP: Rabattarten erweiterbar machen Lab 03/10
Verletzung
Jede neue Kundengruppe erweitert einen switch-Block.
Refactoring
Replace Conditional with Polymorphism
Pattern-Bezug
Strategy + Registry
return switch (customer.segment()) {
case STANDARD -> subtotal;
case BUSINESS -> subtotal.multiply(new BigDecimal("0.95"));
case VIP -> subtotal.multiply(new BigDecimal("0.90"));
};DiscountPolicy policy = policies.resolve(customer.segment());
return policy.apply(subtotal, customer);
// Neue Variante ohne Änderung am Aufrufer:
final class PartnerDiscountPolicy implements DiscountPolicy { ... }04. OCP: Dokumentformate als Plugin Lab 04/10
Verletzung
CSV, JSON und XML werden in einer zentralen Klasse verzweigt.
Refactoring
Introduce Registry
Pattern-Bezug
Strategy + Factory
if (type.equals("CSV")) return readCsv(bytes);
if (type.equals("JSON")) return readJson(bytes);
if (type.equals("XML")) return readXml(bytes);
throw new UnsupportedFormatException(type);DocumentReader reader = registry.readerFor(mediaType); ParsedDocument document = reader.read(envelope); registry.register(new PdfDocumentReader());
05. LSP: Zahlungsprovider substituierbar halten Lab 05/10
Verletzung
Ein Provider wirft bei capture immer UnsupportedOperationException.
Refactoring
Split Interface
Pattern-Bezug
Capability Interfaces
interface PaymentProvider {
Authorization authorize(Payment p);
Capture capture(Authorization a);
}
final class PrepaidProvider implements PaymentProvider {
public Capture capture(Authorization a) {
throw new UnsupportedOperationException();
}
}interface AuthorizingProvider {
Authorization authorize(Payment payment);
}
interface CapturingProvider {
Capture capture(Authorization authorization);
}
final class PrepaidProvider implements AuthorizingProvider { ... }06. LSP: Repository-Vertrag präzisieren Lab 06/10
Verletzung
Eine Implementierung liefert null, eine andere Optional.
Refactoring
Strengthen Contract
Pattern-Bezug
Repository
interface CustomerRepository {
Customer find(String id); // null oder Exception?
}interface CustomerRepository {
Optional<Customer> find(CustomerId id);
}
Customer customer = customers.find(id)
.orElseThrow(() -> new CustomerNotFound(id));07. ISP: Support-Ports aufteilen Lab 07/10
Verletzung
Ein breites Gateway zwingt Clients zu unbenutzten Methoden.
Refactoring
Extract Interface
Pattern-Bezug
Role Interfaces
interface SupportGateway {
Ticket load(TicketId id);
void save(Ticket ticket);
void sendSms(String phone, String text);
void exportCsv(LocalDate day);
}interface TicketRepository {
Ticket load(TicketId id);
void save(Ticket ticket);
}
interface NotificationPort { void notify(Notice notice); }
interface SupportExportPort { Export export(LocalDate day); }08. ISP: Query und Command trennen Lab 08/10
Verletzung
Lesende Clients hängen an mutierenden Methoden.
Refactoring
Separate Query from Modifier
Pattern-Bezug
CQRS-light
interface ClaimService {
ClaimDetails load(ClaimId id);
Decision decide(ClaimId id);
void reopen(ClaimId id);
void delete(ClaimId id);
}interface ClaimQuery {
ClaimDetails load(ClaimId id);
}
interface DecideClaimUseCase {
Decision decide(ClaimId id);
}09. DIP: SMTP und Datenbank hinter Ports Lab 09/10
Verletzung
Der Use Case erzeugt konkrete Infrastrukturklassen selbst.
Refactoring
Introduce Dependency
Pattern-Bezug
Ports and Adapters
public final class RegisterCustomerService {
private final JdbcCustomerDao dao = new JdbcCustomerDao();
private final SmtpClient mail = new SmtpClient();
}public final class RegisterCustomerUseCase {
private final CustomerRepository customers;
private final NotificationPort notifications;
public RegisterCustomerUseCase(
CustomerRepository customers,
NotificationPort notifications) { ... }
}10. DIP: Zeit und IDs kontrollierbar machen Lab 10/10
Verletzung
Instant.now und UUID.randomUUID sind versteckte Abhängigkeiten.
Refactoring
Introduce Parameter / Port
Pattern-Bezug
Clock + IdGenerator
Ticket ticket = new Ticket(
UUID.randomUUID().toString(),
Instant.now(),
request.subject());Ticket ticket = new Ticket(
ticketIds.nextId(),
clock.instant(),
request.subject());Kapitel 4 Effective Java: sichere Refactorings10 Lerneinheiten
Zehn große Labs übersetzen bewährte Java-API- und Objektmodell-Regeln in nachvollziehbare Refactorings mit realem Java-21-Code, Tests und kompakten SVGs.
Effective Java Refactorings
Zehn große Labs übersetzen bewährte Java-API- und Objektmodell-Regeln in nachvollziehbare Refactorings mit realem Java-21-Code, Tests und kompakten SVGs.
7 Labs offen · anschließend: Pattern Decision Workbench.
01. Statische Fabrikmethoden statt unklarer Konstruktoren Lab 01/10
Ausgangsproblem
Mehrere Konstruktoren mit booleschen Flags verschleiern die Bedeutung.
Refactoring
Introduce Static Factory Method
Pattern-Bezug
Value Object / Factory
new CustomerId(UUID.fromString(raw))
CustomerId id = CustomerId.parse(raw);
02. Builder für viele optionale Parameter Lab 02/10
Ausgangsproblem
Ein Teleskop-Konstruktor ist schwer lesbar und vertauscht gleichartige Werte.
Refactoring
Introduce Builder
Pattern-Bezug
Builder
new PaymentRequest(id, amount, EUR, "PAY-42")
PaymentRequest.builder()
.customer(id)
.amount(amount)
.reference("PAY-42")
.build();03. Dependency Injection statt versteckter Singletons Lab 03/10
Ausgangsproblem
Globale Service-Locator machen Tests und Abhängigkeiten unsichtbar.
Refactoring
Inject Dependencies
Pattern-Bezug
Dependency Injection
Repository repo = GlobalServices.repository();
var service = new InvoiceService(repository, clock);
04. try-with-resources statt manuellem close Lab 04/10
Ausgangsproblem
Fehlerpfade überspringen close() und verlieren Ressourcen.
Refactoring
Replace finally with try-with-resources
Pattern-Bezug
Resource Acquisition Is Initialization
BufferedReader r = Files.newBufferedReader(path);
try { return r.lines().toList(); } finally { r.close(); }try (BufferedReader r = Files.newBufferedReader(path)) {
return r.lines().toList();
}05. equals und hashCode als fachliche Einheit Lab 05/10
Ausgangsproblem
Manuell inkonsistente Implementierungen zerstören HashMap- und Set-Verhalten.
Refactoring
Replace Class with Record
Pattern-Bezug
Value Object
class OrderKey { String tenant; String number; /* equals fehlt */ }record OrderKey(String tenant, String number) {
OrderKey { /* Invarianten */ }
}06. Unveränderlichkeit und defensive Kopien Lab 06/10
Ausgangsproblem
Mutable Collections lecken aus dem Aggregat und ändern Zustand von außen.
Refactoring
Encapsulate Collection
Pattern-Bezug
Immutable Object
this.items = items;
this.items = List.copyOf(items);
07. Enum statt int-Konstanten Lab 07/10
Ausgangsproblem
Magische Zahlen erlauben ungültige Werte und verteilen Verhalten.
Refactoring
Replace Type Code with Enum
Pattern-Bezug
Enum Strategy
static final int HIGH = 10;
enum Priority { LOW(1), NORMAL(5), HIGH(10); }08. Lambdas gezielt für kleine Strategien Lab 08/10
Ausgangsproblem
Anonyme Klassen erzeugen Rauschen; komplexe Lambdas verstecken dagegen Fachlogik.
Refactoring
Replace Anonymous Class with Lambda
Pattern-Bezug
Strategy
rules.add(new Predicate<>() { public boolean test(PaymentRequest p) { return p.amount().signum() > 0; }});rules.add(PaymentRules::hasPositiveAmount);
09. Optional nur als Rückgabewert Lab 09/10
Ausgangsproblem
null zwingt jeden Aufrufer zu impliziten Annahmen; Optional-Felder erschweren Modelle.
Refactoring
Introduce Optional Return
Pattern-Bezug
Null Object Alternative
String name = names.get(id);
Optional<String> name = directory.findName(id);
10. Exceptions übersetzen und Kontext bewahren Lab 10/10
Ausgangsproblem
Technische Provider-Exceptions sickern in Fachschichten oder werden verschluckt.
Refactoring
Translate Exception
Pattern-Bezug
Facade / Exception Translator
catch (Exception ex) { throw new SchritttimeException(ex); }catch (Exception ex) {
throw new PaymentException(reference, "Provider failed", ex);
}Kapitel 5 Clean Code: Struktur und Lesbarkeit8 Lerneinheiten
Acht große Labs zeigen, wie aus schwer lesbarem Enterprise-Java-Code eine klare, fachlich benannte und testbare Struktur entsteht – ohne Clean Code als starre Regelreligion zu behandeln.
Clean Code Refactorings
Acht große Labs zeigen, wie aus schwer lesbarem Enterprise-Java-Code eine klare, fachlich benannte und testbare Struktur entsteht – ohne Clean Code als starre Regelreligion zu behandeln.
7 Labs offen · anschließend: Pattern Decision Workbench.
01. Sprechende Namen statt Abkürzungen Lab 01/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Rename Variable / Method
Pattern-Bezug
Intention-Revealing Interface
int d; boolean f;
int overdueDays; boolean fraudSuspected;
02. Kleine Methoden mit einer Aufgabe Lab 02/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Extract Method
Pattern-Bezug
Single Level of Abstraction
validate(); calculate(); persist(); notify();
validateOrder(); Money total = calculateTotal(); save(order);
03. Guard Clauses statt tiefer Verschachtelung Lab 03/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Replace Nested Conditional with Guard Clauses
Pattern-Bezug
Fail Fast
if (customer != null) { if (customer.isActive()) { ... } }if (customer == null) return; if (!customer.isActive()) return;
04. Fachobjekte statt Primitive Obsession Lab 04/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Replace Primitive with Value Object
Pattern-Bezug
Value Object
String customerName; String status;
CustomerName name; CustomerStatus status;
05. Kommentare durch klare Struktur ersetzen Lab 05/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Extract Method / Rename
Pattern-Bezug
Self-Documenting Code
// calculate 20 percent tax
var t = n.multiply(new BigDecimal("0.20"));BigDecimal tax = calculateVat(netAmount);
06. Seiteneffekte explizit an Ports geben Lab 06/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Extract Side Effect
Pattern-Bezug
Ports and Adapters
repository.save(order); mail.send(...);
save(order); notifier.orderAccepted(order);
07. Fehler als fachliches Ergebnis modellieren Lab 07/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Replace Exception Control Flow
Pattern-Bezug
Result Type
try { validate(amount); } catch (Exception e) { return false; }Result<BigDecimal> result = validator.validate(amount);
08. Boolean Blindness durch Entscheidungen ersetzen Lab 08/08
Ausgangsproblem
Komplexer Code zwingt Leser, technische Details mental in Fachabsicht zu übersetzen.
Refactoring
Replace Boolean with Result Object
Pattern-Bezug
Decision Object
return amount.compareTo(limit) <= 0;
return OrderDecision.reject("Express limit exceeded");Technische Spezialgebiete
Nebenläufigkeit und Performance werden nur auf Basis von Tests, Messungen und expliziten Risiken verändert.
Kapitel 6 Nebenläufigkeit sicher refaktorieren8 Lerneinheiten
Acht große Labs zeigen, wie gemeinsam genutzter Zustand, atomare Operationen, Lock-Grenzen, Async-Fehler und Java-21-Virtual-Threads sicher und verständlich refaktoriert werden.
Concurrency Refactorings
Acht große Labs zeigen, wie gemeinsam genutzter Zustand, atomare Operationen, Lock-Grenzen, Async-Fehler und Java-21-Virtual-Threads sicher und verständlich refaktoriert werden.
7 Labs offen · anschließend: Pattern Decision Workbench.
01. Shared Mutable State durch ConcurrentHashMap kapseln Lab 01/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Replace Shared HashMap
Pattern-Bezug
Thread-Safe Repository
HashMap<String, Ticket> tickets = new HashMap<>();
return tickets.computeIfAbsent(id, key -> new SupportTicket(key, subject));
02. Check-then-act atomar ausführen Lab 02/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Replace Check-Then-Act
Pattern-Bezug
Compare-and-Set Loop
if (stock.get(sku) >= qty) stock.put(sku, stock.get(sku) - qty);
if (available.compareAndSet(current, current - quantity)) return true;
03. Hot Counters mit LongAdder entlasten Lab 03/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Replace Contended Atomic Counter
Pattern-Bezug
Contention-Friendly Counter
synchronized void accepted() { accepted++; }private final LongAdder accepted = new LongAdder();
04. Konfiguration als immutable Snapshot veröffentlichen Lab 04/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Copy Mutable Configuration
Pattern-Bezug
Copy-on-Write Snapshot
routes.clear(); routes.putAll(next);
routes.set(Map.copyOf(next));
05. Lock-Bereich auf Zustandsänderung begrenzen Lab 05/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Move I/O Outside Lock
Pattern-Bezug
Narrow Critical Section
synchronized long allocate() { audit.write(next); return next++; }lock.lock(); try { number = next++; } finally { lock.unlock(); }06. Executor-Lebenszyklus explizit besitzen Lab 06/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Inject Executor
Pattern-Bezug
Managed Executor Boundary
var pool = Executors.newFixedThreadPool(20);
new NotificationDispatcher(applicationExecutor);
07. Asynchrone Fehler als fachliche Entscheidung behandeln Lab 07/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Compose CompletableFuture
Pattern-Bezug
Async Aggregator
future.join(); // CompletionException entweicht ungeplant
future.exceptionally(error -> 100).thenApply(this::toDecision);
08. Blockierende I/O mit Virtual Threads skalieren Lab 08/08
Risiko
Race Conditions, verlorene Updates oder unnötige Blockierung machen das Verhalten unter Last nicht deterministisch.
Refactoring
Replace Platform Thread Pool
Pattern-Bezug
Task-per-Request
Executors.newFixedThreadPool(200)
Executors.newVirtualThreadPerTaskExecutor()
Kapitel 7 Performance evidenzbasiert refaktorieren7 Lerneinheiten
Messbare Optimierungen an Datenzugriffen, Traversierungen, Speicher, CPU und Regression-Schutz. Jeder Schritt zeigt Problem, Entscheidung, Java-Code und eine kompakte themenspezifische SVG.
Performance Refactorings
Messbare Optimierungen an Datenzugriffen, Traversierungen, Speicher, CPU und Regression-Schutz. Jeder Schritt zeigt Problem, Entscheidung, Java-Code und eine kompakte themenspezifische SVG.
Offen: 0 Labs · anschließend: Pattern Decision Workbench
Lab 01 · Mehrfache Traversierungen bündeln Single-Pass Aggregator
Ausgangspunkt
Mehrere Streams berechnen Summe, Stückzahl und Zeilenanzahl getrennt. Das wirkt elegant, liest dieselbe Liste aber mehrfach.
var total = lines.stream().map(OrderLine::subtotal).reduce(ZERO, BigDecimal::add); var units = lines.stream().mapToInt(OrderLine::quantity).sum(); var count = lines.stream().count();
Refactoring
Pattern: Single-Pass Aggregator
O(n) bleibt O(n), aber konstante Kosten, temporäre Streams und Datenzugriffe sinken. Wichtig ist, dass Lesbarkeit und Fachbedeutung erhalten bleiben.
BigDecimal total = BigDecimal.ZERO;
int units = 0;
for (OrderLine line : lines) {
total = total.add(line.subtotal());
units += line.quantity();
}
return new OrderSummary(total, units, lines.size());Lab 02 · Remote Reads mit Cache-Aside begrenzen Cache-Aside Decorator
Ausgangspunkt
Ein Kundenprofil wird innerhalb mehrerer Use Cases immer wieder vom entfernten System geladen.
CustomerProfile profile = remoteClient.load(customerId); // derselbe Zugriff wiederholt sich in Pricing, Risk und Support
Refactoring
Pattern: Cache-Aside Decorator
Die TTL ist explizit, der Port bleibt austauschbar und die Fachlogik kennt den Cache nicht. Kein Cache ohne Invalidierungsstrategie.
return delegate.findById(id).map(value -> {
cache.put(id, new Entry(value, now.plus(ttl)));
return value;
});Lab 03 · Große Ergebnismengen seitenweise verarbeiten Cursor Pagination
Ausgangspunkt
Ein Export lädt alle Rechnungen in eine Liste und erzeugt dadurch lange Pausen und hohen Heap-Verbrauch.
List<InvoiceRow> all = repository.findAllOpenInvoices(); return csvWriter.write(all);
Refactoring
Pattern: Cursor Pagination
Die Datenquelle liefert stabile Seiten, der Consumer verarbeitet jede Zeile sofort. Dadurch bleibt der Speicherbedarf annähernd konstant.
String cursor = null;
do {
InvoicePage page = query.fetchAfter(cursor, pageSize);
page.rows().forEach(sink);
cursor = page.nextCursor();
} while (cursor != null);Lab 04 · N+1-Zugriffe durch Batch Fetch ersetzen DataLoader / Batch Fetch
Ausgangspunkt
Für jede Bestellposition wird einzeln ein Preis geladen. Bei 500 Positionen entstehen bis zu 500 Remote- oder Datenbankaufrufe.
for (String sku : skus) {
total = total.add(pricePort.findOne(sku).amount());
}Refactoring
Pattern: DataLoader / Batch Fetch
Ein Port akzeptiert die eindeutigen Schlüssel gesammelt. Die Zuordnung geschieht anschließend lokal und deterministisch.
Set<String> unique = new HashSet<>(skus);
Map<String, ProductPrice> bySku = prices.findAll(unique);
return skus.stream().map(bySku::get).map(ProductPrice::amount)
.reduce(BigDecimal.ZERO, BigDecimal::add);Lab 05 · String-Allokationen im Report reduzieren Pre-sized Buffer
Ausgangspunkt
CSV-Ausgabe entsteht mit wiederholter String-Verkettung in einer Schleife.
String csv = "";
for (ReportRow row : rows) {
csv += row.customerId() + "," + row.orders() + "\n";
}Refactoring
Pattern: Pre-sized Buffer
StringBuilder macht die Akkumulation explizit. Eine grobe Anfangskapazität reduziert Reallokationen, ohne komplexe Mikrooptimierung.
StringBuilder out = new StringBuilder(Math.max(64, rows.size() * 32));
for (ReportRow row : rows) {
out.append(row.customerId()).append(',').append(row.orders()).append('\n');
}
return out.toString();Lab 06 · Teure reine Berechnung memoizen Memoization Decorator
Ausgangspunkt
Eine deterministische Risikobewertung wird für identische Requests mehrfach ausgeführt.
RiskDecision first = engine.evaluate(request); RiskDecision second = engine.evaluate(request); // erneut teuer
Refactoring
Pattern: Memoization Decorator
Memoization ist nur korrekt, wenn der Schlüssel stabil ist und die Funktion keine versteckten zeit- oder zustandsabhängigen Effekte besitzt.
private final ConcurrentHashMap<RiskRequest, RiskDecision> memo = new ConcurrentHashMap<>();
public RiskDecision evaluate(RiskRequest request) {
return memo.computeIfAbsent(request, delegate::evaluate);
}Lab 07 · Performance als Budget und Testgrenze behandeln Executable Performance Budget
Ausgangspunkt
Optimierungen werden ohne Zielwert umgesetzt und spätere Regressionen bleiben lange unbemerkt.
// "Die Suche sollte schnell sein" service.search(criteria);
Refactoring
Pattern: Executable Performance Budget
Erst messen, dann optimieren. Das Budget ist eine Policy, die im Benchmark oder Integrationstest gegen echte Messwerte geprüft wird.
var budget = new PerformanceBudget(Duration.ofMillis(100)); Duration actual = measuredSearchDuration(); budget.verify(actual, "customer-search");
Durchgehende Code-Fallstudien
Dieselben Klassen, Tests und Entscheidungen entwickeln sich vom Legacy-Ausgangscode bis zum begründeten Endzustand.
Kapitel 8 Fallstudie: Order Pricing10 Lernschritte
Eine zusammenhängende Seite vom komplexen Legacy-Code bis zur begründeten Strategy-Lösung.
Order Pricing – 7 Refactoring-Schritte
Eine zusammenhängende Seite vom komplexen Legacy-Code bis zur begründeten Strategy-Lösung.
01. Zielbild und Arbeitsweise
Die Fallstudie beginnt mit einer überladenen Preisroutine und führt sie in kleinen, verifizierbaren Schritten zu einem klaren Fachmodell. Ein Pattern wird erst eingeführt, wenn das zugrunde liegende Änderungsproblem sichtbar ist.
02. Schritt 0 – Komplexer Ausgangscode
public BigDecimal calculate(OrderDto order) {
BigDecimal total = BigDecimal.ZERO;
for (ItemDto item : order.items) {
total = total.add(item.price.multiply(new BigDecimal(item.quantity)));
}
if (order.customerType.equals("VIP")) {
total = total.multiply(new BigDecimal("0.90"));
} else if (order.customerType.equals("PARTNER")) {
total = total.multiply(new BigDecimal("0.85"));
}
if (order.country.equals("AT")) {
total = total.multiply(new BigDecimal("1.20"));
}
emailService.send(order.email, "Total: " + total);
database.save(order.id, total);
return total;
}| Problem | Konsequenz |
|---|---|
| Berechnung, Persistenz und E-Mail in einer Methode | Jede Änderung berührt mehrere Verantwortlichkeiten. |
| Strings als Typcodes | Tippfehler werden erst zur Laufzeit sichtbar. |
| BigDecimal ohne Währung | Fachlich ungültige Operationen bleiben möglich. |
| if-/else-Kette | Neue Kundentypen verändern vorhandenen Code. |
03. Schritt 1 – Characterization Tests
Vor strukturellen Änderungen wird das beobachtete Verhalten festgehalten. Diese Tests bestätigen zunächst auch unerwünschte Altlogik, damit Refactoring und fachliche Korrektur getrennt bleiben.
@ParameterizedTest
@CsvSource({"REGULAR,100.00", "VIP,90.00", "PARTNER,85.00"})
void preservesCurrentDiscountBehaviour(String type, String expected) {
assertEquals(new BigDecimal(expected), legacy.calculate(order(type)));
}04. Schritt 2 – Seiteneffekte abtrennen
Separate Query from Modifier: Die Preisberechnung liefert nur noch einen Wert. Speicherung und Benachrichtigung werden von einem Anwendungsservice koordiniert.
public Receipt submit(Order order) {
Money total = pricingService.price(order);
repository.save(order, total);
notifier.sendConfirmation(order, total);
return new Receipt(order.id(), total);
}05. Schritt 3 – Primitive durch Fachtypen ersetzen
Money, CustomerTier und OrderLine machen ungültige Zustände schwerer darstellbar.
public record Money(BigDecimal amount, Currency currency) {
public Money {
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
public Money add(Money other) { /* currency guard */ }
}06. Schritt 4 – Strategy aus dem Änderungsdruck ableiten
Die Rabattvarianten ändern sich unabhängig voneinander. Strategy kapselt genau diese Variabilität. Sie wird nicht eingesetzt, nur um ein kleines if zu beseitigen, sondern um neue Regeln ohne Eingriff in die Preisberechnung zu ermöglichen.
// Design Pattern: Strategy
@FunctionalInterface
public interface DiscountStrategy {
Money apply(Money subtotal);
}
public final class PricingService {
private final Map<CustomerTier, DiscountStrategy> strategies;
public Money price(Order order) {
DiscountStrategy strategy = strategies.get(order.tier());
return strategy.apply(order.subtotal());
}
}07. Schritt 5 – Auswahl über Registry
private final EnumMap<CustomerTier, DiscountStrategy> strategies;
public PricingService() {
strategies = new EnumMap<>(CustomerTier.class);
strategies.put(REGULAR, DiscountStrategies.regular());
strategies.put(VIP, DiscountStrategies.vip());
strategies.put(PARTNER, DiscountStrategies.partner());
}08. Schritt 6 – Testbarer Zielcode
@Test
void appliesVipDiscount() {
var order = new Order("O-1", CustomerTier.VIP,
List.of(new OrderLine("SKU-1", 2, Money.eur("50"))));
assertEquals(Money.eur("90.00"), pricingService.price(order));
}Der Test kennt weder Datenbank noch E-Mail-System. Damit prüft er ausschließlich die fachliche Regel.
09. Schritt 7 – Bewertung und offene Schulden
| Verbessert | Noch offen |
|---|---|
| Fachtypen, reine Preisberechnung, austauschbare Regeln, isolierte Tests | Steuerlogik, Rundungsdifferenzsrichtlinie, externe Konfiguration, Audit-Anforderungen |
Pattern-Bilanz: Strategy löst das Variantenproblem. Repository und Notifier bleiben Ports; sie werden erst in einer späteren Architektur-Schritt konkretisiert.
10. Übung – Staffelrabatt ergänzen exercise ·
Ergänze einen Staffelrabatt für Bestellungen ab 1.000 EUR. Entscheide, ob er eine eigene Strategy oder ein Decorator um eine bestehende Strategy sein soll. Begründe die Entscheidung und schreibe Grenzwerttests für 999,99 EUR und 1.000,00 EUR.
Übungslösung: Staffelrabatt sicher ergänzen
Decorator-Lösung mit Begründung und Grenzwerttests.
Lösung – Staffelrabatt
Decorator-Lösung mit Begründung und Grenzwerttests.
01. Lösung: Staffelrabatt
Die Lösung nutzt einen Decorator, weil der Staffelrabatt eine bestehende Kundensegment-Strategy ergänzt und nicht ersetzt.
// Design Pattern: Decorator
public final class TieredDiscount implements DiscountStrategy {
private final DiscountStrategy delegate;
private final Money threshold;
private final BigDecimal additionalRate;
public Money apply(Money subtotal) {
Money base = delegate.apply(subtotal);
return subtotal.compareTo(threshold) >= 0
? base.multiply(BigDecimal.ONE.subtract(additionalRate))
: base;
}
}@ParameterizedTest
@CsvSource({"999.99,899.99", "1000.00,855.00"})
void appliesAdditionalDiscountAtThreshold(String subtotal, String expected) {
var rule = new TieredDiscount(vip(), Money.eur("1000"), new BigDecimal("0.05"));
assertEquals(Money.eur(expected), rule.apply(Money.eur(subtotal)));
}Kapitel 9 Fallstudie: Payment Orchestration10 Lernschritte
Vom rekursiven Legacy-Payment-Service zur expliziten Orchestrierung mit Adapter, Chain of Responsibility, Strategy Registry, Facade und atomarer Idempotenz-Reservierung.
Payment Orchestration Refactoring Workbench
Vom rekursiven Legacy-Payment-Service zur expliziten Orchestrierung mit Adapter, Chain of Responsibility, Strategy Registry, Facade und atomarer Idempotenz-Reservierung.
00 - Fachlicher Auftrag und Grenzen auf-/zuklappen
Eine Zahlung darf bei Wiederholung nicht doppelt ausgeführt werden. Betrugsregeln, Providerwahl und externe Gateway-Details müssen getrennt sein.
01 - Legacy-Ausgangscode auf-/zuklappen
public String pay(String type, String orderId, BigDecimal amount, String cardToken) {
if (amount == null || amount.signum() <= 0) return "ERROR";
if (blockedOrders.contains(orderId)) return "FRAUD";
if (type.equals("CARD")) {
CardResponse response = cardGateway.authorize(orderId, amount, cardToken);
auditDao.insert(orderId, response.code());
if (response.timeout()) { Thread.sleep(1000); return pay(type, orderId, amount, cardToken); }
return response.approved() ? "OK:" + response.reference() : "DECLINED";
} else if (type.equals("BANK")) {
return bankClient.transfer(orderId, amount) ? "OK" : "ERROR";
}
return "UNKNOWN";
}Long MethodPrimitive ObsessionRecursive RetryProvider Coupling
02 - Schritt 1: Verhalten durch Characterization Tests sichern auf-/zuklappen
Vor dem Umbau werden Doppelaufruf, Ablehnung, unbekannter Provider und Timeout-Verhalten als beobachtbares Verhalten fixiert. Erst dann darf Struktur verändert werden.
03 - Schritt 2: PaymentRequest und PaymentResult auf-/zuklappen
public record PaymentRequest(String orderId, Money amount, PaymentMethod method, String idempotencyKey) {}String-Rückgaben und lose Parameter werden durch fachliche Typen ersetzt. Dadurch verschwinden ungültige Kombinationen aus dem öffentlichen API.
04 - Schritt 3: Adapter für externe Provider auf-/zuklappen
public interface PaymentProvider { PaymentResult charge(PaymentRequest request); }
public final class CardProviderAdapter implements PaymentProvider {
public PaymentResult charge(PaymentRequest request) { /* Gateway übersetzen */ }
}Pattern: Adapter. Nicht der Orchestrator kennt die proprietäre Gateway-Antwort, sondern nur der Adapter.
05 - Schritt 4: Fraud Chain auf-/zuklappen
for (FraudRule rule : rules) {
var decision = rule.evaluate(request);
if (!decision.allowed()) return decision;
}
return Decision.allow();Pattern: Chain of Responsibility. Die Reihenfolge ist explizit und testbar; sie wird nicht über Annotationen versteckt.
06 - Schritt 5: Idempotenz vor Provideraufruf auf-/zuklappen
var cached = store.find(request.idempotencyKey()); if (cached.isPresent()) return cached.get(); var result = provider.charge(request); store.save(request.idempotencyKey(), result);
07 - Schritt 6: Facade und Strategy Registry auf-/zuklappen
Der PaymentOrchestrator ist eine schmale Facade. Die Map aus PaymentMethod und PaymentProvider bildet eine Strategy Registry und ersetzt wachsende Verzweigungen.
08 - Schritt 8: Atomare Reservierung für parallele Aufrufe auf-/zuklappen
Das einfache find → charge → save besitzt ein Race Condition: Zwei parallele Requests können beide noch keinen Eintrag sehen und denselben Provider zweimal aufrufen. Die letzte Schritt führt deshalb eine explizite Reservierung ein.
public interface PaymentExecutionStore {
Reservation reserve(String idempotencyKey);
void complete(String idempotencyKey, PaymentResult result);
void release(String idempotencyKey);
enum Reservation { ACQUIRED, IN_PROGRESS, COMPLETED }
}var reservation = executions.reserve(request.idempotencyKey());
if (reservation == COMPLETED) return executions.completedResult(key).orElseThrow();
if (reservation == IN_PROGRESS) return PaymentResult.retryable("already in progress");
try {
var result = provider.charge(request);
executions.complete(key, result);
return result;
} catch (SchritttimeException failure) {
executions.release(key);
throw failure;
}Patterns: Idempotent Consumer und Repository. Die In-Memory-Implementierung verwendet ConcurrentHashMap.putIfAbsent. In Produktion übernimmt eine Datenbanktransaktion mit Unique Constraint die atomare Reservierung.
ResilientPaymentOrchestratorTest beweist, dass ein wiederholter Schlüssel nur einen Provideraufruf auslöst.09 - Tests und Lernaufgabe auf-/zuklappen
assertEquals(APPROVED, sut.pay(request).status()); assertEquals(APPROVED, sut.pay(request).status()); assertEquals(1, provider.calls);
Aufgabe: Ergänze einen Reservation-Status für parallele Requests und beschreibe, welche Transaktionsgrenze erforderlich ist.
Kapitel 10 Praxislabor: Claims Management10 Lerneinheiten
Von einer Map-basierten Prozedur zu Fachobjekt, Specification, expliziter Zustandsmaschine, testbarem Domain Service und austauschbarer Entscheidungs-Policy.
Claims Management Refactoring Workbench
Von einer Map-basierten Prozedur zu Fachobjekt, Specification, expliziter Zustandsmaschine, testbarem Domain Service und austauschbarer Entscheidungs-Policy.
00 - Fachlicher Auftrag auf-/zuklappen
Ein Schadenfall besitzt erlaubte Zustandsübergänge und prüfbare Einreichungsregeln. Dokumente, Betrag und Status sollen nicht länger in einer untypisierten Map liegen.
01 - Legacy-Ausgangscode auf-/zuklappen
public void processClaim(Map<String,Object> data) {
String status=(String)data.get("status");
BigDecimal amount=(BigDecimal)data.get("amount");
if(status.equals("DRAFT") && amount.signum()>0 && data.containsKey("invoice")) {
data.put("status","SUBMITTED");
claimDao.update(data);
}
if(status.equals("SUBMITTED")) { data.put("status","UNDER_REVIEW"); }
if(amount.compareTo(new BigDecimal("5000"))<0) data.put("status","APPROVED");
else mailService.send("manual-review", data.toString());
}Data ClumpTemporal CouplingInvalid StateMixed I/O
02 - Schritt 1: Characterization Tests auf-/zuklappen
Tests dokumentieren, welche Fälle bisher automatisch genehmigt wurden und wann E-Mail-Versand erfolgt. Fehlerhaftes Altverhalten wird sichtbar markiert, nicht unbemerkt übernommen.
03 - Schritt 2: Claim als Fachobjekt auf-/zuklappen
public final class Claim {
private final String id;
private final Money amount;
private ClaimStatus status;
private final Set<String> documents = new HashSet<>();
}Die Map wird durch ein Objekt mit Invarianten ersetzt. Der Statussetter bleibt paketintern, damit nur die Zustandsmaschine Übergänge ausführt.
04 - Schritt 3: Specification für Einreichungsregeln auf-/zuklappen
var rules = positiveAmount().and(hasDocument("INVOICE"));
var result = rules.evaluate(claim);Pattern: Specification. Regeln sind benannt, kombinierbar und liefern einen fachlichen Ablehnungsgrund.
05 - Schritt 4: Explizite State Machine auf-/zuklappen
DRAFT -> SUBMITTED SUBMITTED -> UNDER_REVIEW UNDER_REVIEW -> APPROVED | REJECTED APPROVED -> PAID
Pattern: State in tabellarischer Form. Für diesen Umfang ist eine Übergangstabelle einfacher als eine Klasse je Status.
06 - Schritt 5: Domain Service trennt Entscheidung und Übergang auf-/zuklappen
var result = rules.evaluate(claim); if (result.valid()) states.transition(claim, SUBMITTED); return result;
Der Service koordiniert Regeln und Zustand, ohne Datenbank oder E-Mail direkt anzusprechen.
07 - Schritt 6: Infrastruktur nach außen auf-/zuklappen
Repository und Benachrichtigung werden Ports. Die Fallstudie hält den Kern bewusst frameworkfrei; Spring- oder Jakarta-Adapter können später ergänzt werden.
08 - Schritt 7: Entscheidungs-Policy statt Betragslogik in der State Machine auf-/zuklappen
Die automatische Freigabegrenze ist eine änderbare Geschäftsentscheidung, kein Zustandsübergang. Deshalb bleibt die State Machine unverändert und eine Policy entscheidet zwischen automatischer Genehmigung, manueller Prüfung und Ablehnung.
@FunctionalInterface
public interface ClaimDecisionPolicy {
ClaimDecision decide(Claim claim);
}ClaimDecision decision = policy.decide(claim);
switch (decision.outcome()) {
case AUTO_APPROVE -> states.transition(claim, APPROVED);
case REJECT -> states.transition(claim, REJECTED);
case MANUAL_REVIEW -> { /* UNDER_REVIEW bleibt bestehen */ }
}Patterns: Policy/Strategy plus Domain Service. Die konkrete AmountBasedClaimDecisionPolicy kapselt die Grenze von 5.000 EUR und kann durch andere Produkt-, Risiko- oder Länder-Policies ersetzt werden.
09 - Tests, Grenzen und nächste Vertiefung auf-/zuklappen
assertFalse(sut.submit(claim).valid());
claim.addDocument("INVOICE");
assertTrue(sut.submit(claim).valid());
assertEquals(SUBMITTED, claim.status());Aufgabe: Ergänze eine Policy für Beträge über 5.000 EUR, ohne die State Machine mit Betragslogik zu vermischen.
Kapitel 11 Enterprise-Fallstudie: Claims-Verarbeitung10 Lernschritte
Diese Fallstudie begleitet dieselbe Schadenverarbeitung vom schwer testbaren Legacy-Prozessor bis zu einem fachlich lesbaren Use Case. Jeder Schritt löst genau ein zuvor belegtes Problem. Der Endzustand wird nicht vorweggenommen.
01. Das echte Problem lesen, bevor Code verschoben wird
9 sichtbare Codeblöcke und 10 Lernschritte; Entscheidung, Seiteneffekte und Transaktion werden nacheinander getrennt.
Diese Geschichte zeigt, wie eine fachlich dichte God Class zerlegt wird, ohne Transaktionsgrenzen, Audit, Fremdsystemaufrufe und Ergebnisgleichheit zu verlieren.
Der Ausgangscode validiert Eingaben, ruft einen Fraud-Service auf, berechnet Selbstbehalte, entscheidet über Freigabe, schreibt Daten, Audit und Nachrichten. Das Problem ist nicht nur die Länge: fachliche Entscheidung und technische Wirkungen sind untrennbar.
| Beobachtung | Risiko | Erster Nachweis |
|---|---|---|
| Datum wird direkt gelesen | Tests sind nicht reproduzierbar | Audit-Ausgabe charakterisieren |
| Fraud-Aufruf mitten in der Methode | Netzfehler verändert Fachentscheidung | Timeout-Fall sichern |
| Persistenz und Nachricht unmittelbar | partielle Seiteneffekte | Reihenfolge beobachten |
| Strings und Booleans | ungültige Zustände bleiben darstellbar | Grenzfälle sammeln |
package com.aydinsude.workbench.claims.complexrefactoring;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Bewusst problematischer Ausgangscode der Fallstudie.
* Er verbindet Validierung, Regeln, Persistenz, Fremdsystem, Audit und Benachrichtigung.
*/
public final class LegacyClaimProcessor {
private final Map<String, ClaimRow> database;
private final List<String> auditLog;
private final List<String> messages;
private final FraudClient fraudClient;
public LegacyClaimProcessor(
Map<String, ClaimRow> database,
List<String> auditLog,
List<String> messages,
FraudClient fraudClient) {
this.database = Objects.requireNonNull(database);
this.auditLog = Objects.requireNonNull(auditLog);
this.messages = Objects.requireNonNull(messages);
this.fraudClient = Objects.requireNonNull(fraudClient);
}
public Result process(String id, String customerId, BigDecimal amount,
String currency, String type, boolean vip,
int customerYears, boolean completeDocuments) {
if (id == null || id.isBlank()) return new Result("REJECTED", BigDecimal.ZERO, "missing id");
if (amount == null || amount.signum() <= 0) return new Result("REJECTED", BigDecimal.ZERO, "invalid amount");
if (!completeDocuments) return new Result("MANUAL_REVIEW", BigDecimal.ZERO, "documents incomplete");
if (!List.of("EUR", "USD").contains(currency)) return new Result("REJECTED", BigDecimal.ZERO, "unsupported currency");
int fraudScore;
try {
fraudScore = fraudClient.score(customerId, amount, type);
} catch (RuntimeException failure) {
auditLog.add(LocalDate.now() + " FRAUD_UNAVAILABLE " + id);
return new Result("MANUAL_REVIEW", BigDecimal.ZERO, "fraud service unavailable");
}
BigDecimal deductible = BigDecimal.ZERO;
if ("DAMAGE".equals(type)) deductible = amount.multiply(new BigDecimal("0.10"));
else if ("LIABILITY".equals(type)) deductible = amount.multiply(new BigDecimal("0.05"));
else if ("TRAVEL".equals(type)) deductible = new BigDecimal("75.00");
String status;
String reason;
if (fraudScore > 75 && !vip && customerYears < 2 && amount.compareTo(new BigDecimal("10000")) > 0) {
status = "MANUAL_REVIEW";
reason = "high fraud risk";
} else if (amount.compareTo(new BigDecimal("25000")) > 0) {
status = "MANUAL_REVIEW";
reason = "amount above automatic limit";
} else {
status = "APPROVED";
reason = "automatic approval";
}
BigDecimal reserve = amount.subtract(deductible).max(BigDecimal.ZERO);
ClaimRow row = new ClaimRow(id, customerId, amount, currency, type, status, reserve, reason);
database.put(id, row);
auditLog.add(LocalDate.now() + " CLAIM_PROCESSED " + id + " " + status);
messages.add("claim." + status.toLowerCase() + ":" + id + ":" + reserve);
return new Result(status, reserve, reason);
}
public interface FraudClient {
int score(String customerId, BigDecimal amount, String type);
}
public record ClaimRow(String id, String customerId, BigDecimal amount,
String currency, String type, String status,
BigDecimal reserve, String reason) {}
public record Result(String status, BigDecimal reserve, String reason) {}
}02. Sicherheitsnetz: Verhalten statt Struktur einfrieren
Vor dem ersten Strukturumbau werden Hauptpfad, Grenzwert, unvollständige Dokumente und Fraud-Ausfall festgehalten. Die Tests beschreiben beobachtbares Verhalten; sie legitimieren noch keine neue Architektur.
package com.aydinsude.workbench.claims.complexrefactoring;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.*;
import static com.aydinsude.workbench.claims.complexrefactoring.ClaimCommand.*;
final class ComplexClaimRefactoringTest {
@Test
void golden_master_high_risk_case_remains_manual_review() {
var legacy = new LegacyClaimProcessor(new HashMap<>(), new ArrayList<>(), new ArrayList<>(),
(customerId, amount, type) -> 90);
var oldResult = legacy.process("CL-4711", "CU-9", new BigDecimal("12500.00"),
"EUR", "DAMAGE", false, 1, true);
var command = new ClaimCommand(new ClaimId("CL-4711"), new CustomerId("CU-9"),
new Money(new BigDecimal("12500.00"), "EUR"), ClaimType.DAMAGE, false, 1, true);
var modern = new ClaimDecisionEngine(new DeductiblePolicy(), new ManualReviewPolicy())
.decide(command, new FraudAssessmentPort.Assessment(90, true));
assertEquals("MANUAL_REVIEW", oldResult.status());
assertEquals(oldResult.status(), modern.status().name());
assertEquals(0, oldResult.reserve().compareTo(modern.reserve().amount()));
}
@Test
void unavailable_fraud_service_is_not_silently_approved() {
var command = new ClaimCommand(new ClaimId("CL-5"), new CustomerId("CU-2"),
new Money(new BigDecimal("900.00"), "EUR"), ClaimType.DAMAGE, true, 10, true);
var decision = new ClaimDecisionEngine(new DeductiblePolicy(), new ManualReviewPolicy())
.decide(command, FraudAssessmentPort.Assessment.unavailable());
assertEquals(ClaimDecision.Status.MANUAL_REVIEW, decision.status());
}
}03. Versteckte Abhängigkeiten sichtbar machen
Der erste Eingriff ist klein: externe Bewertung, Speicherung, Ereignis und Audit werden als explizite Rollen benannt. Dadurch kann jede Wirkung in Tests kontrolliert werden. Noch wird keine Microservice-Grenze behauptet.
public interface FraudAssessmentPort {
Assessment assess(ClaimCommand command);
}
public interface ClaimRepository {
void save(ClaimCommand command, ClaimDecision decision);
}
public interface ClaimEventPort {
void append(ClaimProcessed event);
}
public interface ClaimAuditPort {
void record(String claimId, String action, String detail);
}Warum zuerst? Ohne kontrollierbare Abhängigkeiten würden spätere fachliche Refactorings ständig durch Infrastrukturfehler verfälscht.
04. Primitive Obsession durch fachliche Typen ersetzen
Erst nach dem Sicherheitsnetz werden Identität, Geld, Schadenart, Priorität und Dokumentstatus typisiert. Die Typen verhindern ungültige Kombinationen an der Systemgrenze, statt sie tief in Bedingungen abzufangen.
package com.aydinsude.workbench.claims.complexrefactoring;
import java.math.BigDecimal;
import java.util.Objects;
public record ClaimCommand(
ClaimId id,
CustomerId customerId,
Money amount,
ClaimType type,
boolean vip,
int customerYears,
boolean completeDocuments) {
public ClaimCommand {
Objects.requireNonNull(id);
Objects.requireNonNull(customerId);
Objects.requireNonNull(amount);
Objects.requireNonNull(type);
if (customerYears < 0) throw new IllegalArgumentException("customerYears must not be negative");
}
public record ClaimId(String value) {
public ClaimId { if (value == null || value.isBlank()) throw new IllegalArgumentException("claim id required"); }
}
public record CustomerId(String value) {
public CustomerId { if (value == null || value.isBlank()) throw new IllegalArgumentException("customer id required"); }
}
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount); Objects.requireNonNull(currency);
if (amount.signum() <= 0) throw new IllegalArgumentException("amount must be positive");
if (!currency.equals("EUR") && !currency.equals("USD")) throw new IllegalArgumentException("unsupported currency");
}
public Money subtract(Money other) {
if (!currency.equals(other.currency)) throw new IllegalArgumentException("currency mismatch");
return new Money(amount.subtract(other.amount).max(BigDecimal.ZERO), currency);
}
}
public enum ClaimType { DAMAGE, LIABILITY, TRAVEL }
}05. Die lange Methode als fachlichen Ablauf lesbar machen
Nun wird nicht wahllos extrahiert. Die Methode wird entlang des Geschäftsprozesses geordnet: validieren, Risiko bewerten, entscheiden, speichern, veröffentlichen, auditieren.
public ClaimDecision process(ClaimCommand command) {
var assessment = safeAssessment(command);
var decision = engine.decide(command, assessment);
repository.save(command, decision);
events.append(toEvent(command, decision));
audit.record(command.id().value(), "CLAIM_PROCESSED", decision.status().name());
return decision;
}Dieser Schritt verbessert Lesbarkeit, ohne schon Strategy, Outbox oder Modulgrenzen einzuführen.
06. Fachliche Entscheidung von Seiteneffekten trennen
Die Entscheidung wird als reine Berechnung isoliert. Sie erhält alle benötigten Fakten und liefert ein Ergebnis zurück. Datenbank, Nachricht und Audit bleiben außerhalb.
package com.aydinsude.workbench.claims.complexrefactoring;
import static com.aydinsude.workbench.claims.complexrefactoring.ClaimDecision.Status.*;
/** Reine fachliche Query: keine Persistenz, keine Nachricht und kein Audit. */
public final class ClaimDecisionEngine {
private final DeductiblePolicy deductiblePolicy;
private final ManualReviewPolicy manualReviewPolicy;
public ClaimDecisionEngine(DeductiblePolicy deductiblePolicy, ManualReviewPolicy manualReviewPolicy) {
this.deductiblePolicy = deductiblePolicy;
this.manualReviewPolicy = manualReviewPolicy;
}
public ClaimDecision decide(ClaimCommand command, FraudAssessmentPort.Assessment fraud) {
var deductible = deductiblePolicy.calculate(command);
var reserve = command.amount().subtract(deductible);
if (manualReviewPolicy.isRequired(command, fraud)) {
return new ClaimDecision(MANUAL_REVIEW, reserve, reviewReason(command, fraud));
}
return new ClaimDecision(APPROVED, reserve, "automatic approval");
}
private String reviewReason(ClaimCommand command, FraudAssessmentPort.Assessment fraud) {
if (!command.completeDocuments()) return "documents incomplete";
if (!fraud.available()) return "fraud service unavailable";
if (fraud.score() > 75) return "high fraud risk";
return "amount above automatic limit";
}
}Nutzen: Grenzwerte und Sonderfälle können jetzt ohne Datenbank, Uhr oder Netzwerk getestet werden. Erst hier wird sichtbar, welche Regeln wirklich variieren.
07. Policy und Strategy nur an stabilen Varianten einsetzen
Selbstbehalt und manuelle Prüfung werden nur deshalb als Policies modelliert, weil Schadenarten und Risikoregeln unabhängig variieren und separat fachlich verantwortet werden. Ein einmaliger Grenzwert bleibt dagegen lokale Logik.
var deductible = deductiblePolicy.calculate(command);
if (manualReviewPolicy.requiresReview(command, assessment)) {
return ClaimDecision.manualReview(deductible.reserveFor(command.amount()), "high fraud risk");
}
return ClaimDecision.approved(deductible.reserveFor(command.amount()));08. Transaktionsgrenze und Ereignisreihenfolge bewusst festlegen
Die fachliche Entscheidung ist rein; der Application Service koordiniert die Wirkungen. Persistenz muss vor dem Ereignis erfolgen. Für eine produktive Datenbank wird das Ereignis über eine Outbox in derselben Transaktion gespeichert.
package com.aydinsude.workbench.claims.complexrefactoring;
/**
* Design Patterns: Application Service, Ports & Adapters, Transaction Script auf Use-Case-Ebene.
* Zweck: fachliche Entscheidung und Seiteneffekte in einer nachvollziehbaren Reihenfolge koordinieren.
*/
public final class ProcessClaimUseCase {
private final FraudAssessmentPort fraud;
private final ClaimDecisionEngine engine;
private final ClaimRepository repository;
private final ClaimEventPort events;
private final ClaimAuditPort audit;
public ProcessClaimUseCase(FraudAssessmentPort fraud, ClaimDecisionEngine engine,
ClaimRepository repository, ClaimEventPort events,
ClaimAuditPort audit) {
this.fraud = fraud;
this.engine = engine;
this.repository = repository;
this.events = events;
this.audit = audit;
}
public ClaimDecision process(ClaimCommand command) {
var assessment = safeAssessment(command);
var decision = engine.decide(command, assessment);
repository.save(command, decision);
events.append(new ClaimEventPort.ClaimProcessed(
command.id().value(), decision.status().name(),
decision.reserve().amount().toPlainString(), decision.reason()));
audit.record(command.id().value(), "CLAIM_PROCESSED", decision.status().name());
return decision;
}
private FraudAssessmentPort.Assessment safeAssessment(ClaimCommand command) {
try {
return fraud.assess(command);
} catch (RuntimeException failure) {
audit.record(command.id().value(), "FRAUD_UNAVAILABLE", failure.getClass().getSimpleName());
return FraudAssessmentPort.Assessment.unavailable();
}
}
}| Wirkung | Garantie | Fehlerstrategie |
|---|---|---|
| Entscheidung | deterministisch | erneut berechenbar |
| Claim speichern | atomar mit Outbox | Rollback |
| Ereignis publizieren | mindestens einmal | idempotenter Consumer |
| Audit | nachvollziehbar | technisch getrennte Wiederholung |
09. Alt- und Neupfad parallel vergleichen
Strukturelle Schönheit ist kein Freigabenachweis. Deshalb werden beide Pfade mit denselben Eingaben ausgeführt und fachlich verglichen. Unterschiede bei Status, Reserve oder Begründung werden klassifiziert.
package com.aydinsude.workbench.claims.complexrefactoring;
import java.util.Objects;
/** Design Pattern: Parallel Run / Reconciliation. Zweck: Alt- und Neupfad fachlich vergleichen. */
public final class ReconciliationService {
public Difference compare(LegacyClaimProcessor.Result legacy, ClaimDecision modern) {
boolean sameStatus = Objects.equals(legacy.status(), modern.status().name());
boolean sameReserve = legacy.reserve().compareTo(modern.reserve().amount()) == 0;
boolean sameReason = Objects.equals(legacy.reason(), modern.reason());
return new Difference(sameStatus, sameReserve, sameReason);
}
public record Difference(boolean sameStatus, boolean sameReserve, boolean sameReason) {
public boolean equivalent() { return sameStatus && sameReserve && sameReason; }
}
}Der Neupfad wird erst führend, wenn Grenzfälle und Produktionsstichproben über einen vereinbarten Zeitraum erklärbar übereinstimmen.
10. Endzustand, Messwerte und bewusster Stopppunkt
Der finale Code ist nicht einfach kürzer. Seine Verantwortungen sind überprüfbar: Der Use Case koordiniert, die Engine entscheidet, Policies modellieren stabile Varianten und Ports kapseln technische Wirkungen.
Vorher
- eine Methode mit sechs Verantwortungen
- Strings und Booleans
- Netzfehler mitten in Fachlogik
- Seiteneffekte ohne klare Reihenfolge
Nachher
- reine fachliche Entscheidung
- validierte Fachtypen
- explizite Ports
- nachweisbare Transaktionsgrenze
- Alt-/Neuvergleich
Bewusster Stopppunkt: Kein Microservice-Schnitt, keine universelle Rule Engine und kein Event-Sourcing, solange die fachliche Änderungsrate und Betriebsanforderungen dies nicht belegen.
Weiterlesen: Symptom-zu-Vorgehen-Matrix · Batch-Fallstudie · SOAP-Fallstudie
Zwischenstand – reine Entscheidung, koordinierte Seiteneffekte
Die God Class ist bereits zerlegt, aber die Architektur ist noch bewusst kompakt. Fachentscheidung, Persistenz und Outbox sind sichtbar getrennt, ohne vorschnell weitere Services oder Microservices einzuführen.
package com.aydinsude.workbench.intermediate;
import java.math.BigDecimal;
import java.util.Objects;
/** Zwischenstand: Fachentscheidung ist rein; Persistenz und Nachricht bleiben koordiniert. */
public final class ClaimsIntermediate {
private final ClaimDecisionPolicy decisionPolicy;
private final ClaimRepository repository;
private final EventOutbox outbox;
public ClaimsIntermediate(ClaimDecisionPolicy decisionPolicy,
ClaimRepository repository,
EventOutbox outbox) {
this.decisionPolicy = Objects.requireNonNull(decisionPolicy);
this.repository = Objects.requireNonNull(repository);
this.outbox = Objects.requireNonNull(outbox);
}
public ClaimDecision process(ClaimCommand command) {
Claim claim = new Claim(command.claimId(), new Money(command.amount()), command.riskScore());
ClaimDecision decision = decisionPolicy.decide(claim);
repository.save(claim, decision);
outbox.append(new ClaimDecided(claim.id(), decision.status(), decision.reason()));
return decision;
}
public interface ClaimDecisionPolicy { ClaimDecision decide(Claim claim); }
public interface ClaimRepository { void save(Claim claim, ClaimDecision decision); }
public interface EventOutbox { void append(ClaimDecided event); }
public record ClaimCommand(String claimId, BigDecimal amount, int riskScore) {}
public record Claim(String id, Money amount, int riskScore) {}
public record Money(BigDecimal value) {
public Money { Objects.requireNonNull(value); if (value.signum() < 0) throw new IllegalArgumentException(); }
}
public record ClaimDecision(Status status, String reason) {}
public record ClaimDecided(String claimId, Status status, String reason) {}
public enum Status { APPROVED, MANUAL_REVIEW, REJECTED }
}
Diff: gemischte Verarbeitung → reine Entscheidung plus Koordination
- Decision decision = calculateAndPersistAndNotify(command); - return decision; + ClaimDecision decision = decisionService.decide(command); + repository.save(decision); + outbox.append(ClaimDecided.from(decision)); + audit.record(decision); + return decision;
Vom Diff zur sicheren nächsten Aktion – Claims
Nächster sicherer Schritt
Die reine Entscheidung zuerst extrahieren und Seiteneffekte nur über einen koordinierenden Use Case ausführen.
Passender Nachweis
Characterization- und Transaktionstest: Entscheidung bleibt identisch; Persistenz und Outbox werden gemeinsam oder gar nicht wirksam.
Bewusster Stopppunkt
Stoppen, wenn Fachentscheidung, Transaktionsgrenze und Vergleich Alt/Neu nachweisbar stabil sind; kein Microservice-Schnitt ohne Betriebsnutzen.
Praxis-Checkliste – Claims
Vor dem Einsatz im realen Projekt kurz gemeinsam abhaken. Die Liste ergänzt die Fallstudie und ersetzt weder Tests noch fachliche Freigaben.
- Vor dem ersten Eingriff: Grenzwerte, Sonderfälle, externe Aufrufe und Reihenfolge aller Seiteneffekte dokumentieren.↗ Testbeleg öffnen
- Sicherheitsnetz: Golden Master, Grenzwerttests und Alt-/Neuvergleich für fachliche Entscheidungen sichern.↗ Sicherheitsnetz öffnen
- Kleiner erster Schritt: Zuerst die reine Entscheidung extrahieren; Persistenz und Nachrichten noch koordiniert lassen.↗ Zwischenstand öffnen
- Freigabenachweis: Transaktionstest für Persistenz und Outbox sowie fachliche Ergebnisgleichheit ausführen.↗ Endzustand öffnen
- Stoppbedingung: Kein Microservice-Schnitt, solange ein modularer Prozess die Risiken bereits beherrscht.↗ Stopppunkt öffnen
Kapitel 12 Praxislabor: Customer Support22 Lerneinheiten
Von Controller-SQL, String-Arrays und direktem SMTP zu einem testbaren Support-Kern. Jeder Schritt besitzt eine kompakte themenspezifische SVG.
Legacy Customer Support: vom riskanten Monolithen zur kontrollierten Abschaltung
Eine zusammenhängende Lernreise zeigt denselben Support-Pfad vom beobachteten Altsystem über Sicherheitsnetz, Modernisierungsentscheidung, Strangler und Parallelvergleich bis zur Betriebsübernahme.
Die vorhandenen Einzeltechniken bleiben erhalten und werden durch einen roten Faden verbunden.
Legacy-Modernisierung als vollständige Geschichte
Die Modernisierungsentscheidung bleibt vom technischen Umbau getrennt; Parallelbetrieb und Abschaltung besitzen eigene Nachweise.
Hier steht die Entscheidung im Mittelpunkt, wann stabilisiert, gekapselt oder stranguliert wird und welche Evidenz für Parallelbetrieb, Betriebsübergabe und Abschaltung nötig ist.
Das Beispiel begleitet einen kritischen Support-Pfad. Der Ausgangscode liest untypisierte Datensätze, entscheidet über das zuständige Team, schreibt SQL, versendet E-Mails und protokolliert Auditdaten in einer Methode. Die Modernisierung beginnt deshalb nicht mit Microservices oder einem Rewrite, sondern mit einer Frage: Welches Verhalten darf auf keinen Fall unbemerkt verändert werden?
Ausgangsrisiko
Eine Änderung an Teamzuordnung, SQL oder Mail kann gleichzeitig Fachentscheidung, Datenzustand und Kommunikation verändern.
Leitentscheidung
Erst Verhalten sichern und Grenzen schaffen. Strangler, Parallelbetrieb und Abschaltung folgen erst, wenn Evidenz vorhanden ist.
Ergebnis
Ein stabiler fachlicher Port, kontrollierte Kohorten, Reconciliation, Betriebsnachweis und ein messbarer Cutover.
Zwischenstand – Legacy bleibt führend, Shadow-Vergleich wird explizit
Der Strangler ist hier noch nicht abgeschlossen. Der Zwischenstand zeigt den sicheren Übergang: Der Altpfad liefert weiter das Ergebnis, während der neue Pfad ohne fachliche Nebenwirkung verglichen wird.
package com.aydinsude.workbench.intermediate;
import java.util.Objects;
/** Zwischenstand: Der Monolith bleibt führend, Routing und Vergleich sind bereits explizit. */
public final class LegacyModernizationIntermediate {
private final SupportCasePort legacy;
private final SupportCasePort modern;
private final CohortPolicy cohorts;
private final DifferenceSink differences;
public LegacyModernizationIntermediate(SupportCasePort legacy,
SupportCasePort modern,
CohortPolicy cohorts,
DifferenceSink differences) {
this.legacy = Objects.requireNonNull(legacy);
this.modern = Objects.requireNonNull(modern);
this.cohorts = Objects.requireNonNull(cohorts);
this.differences = Objects.requireNonNull(differences);
}
public SupportDecision handle(SupportCommand command) {
SupportDecision legacyDecision = legacy.handle(command);
if (!cohorts.shadowEnabled(command.customerId())) {
return legacyDecision;
}
try {
SupportDecision modernDecision = modern.handle(command);
if (!legacyDecision.equals(modernDecision)) {
differences.record(command.caseId(), legacyDecision, modernDecision);
}
} catch (RuntimeException shadowFailure) {
differences.recordShadowFailure(command.caseId(), shadowFailure.getClass().getSimpleName());
}
return legacyDecision;
}
public interface SupportCasePort { SupportDecision handle(SupportCommand command); }
public interface CohortPolicy { boolean shadowEnabled(String customerId); }
public interface DifferenceSink {
void record(String caseId, SupportDecision legacy, SupportDecision modern);
void recordShadowFailure(String caseId, String failureType);
}
public record SupportCommand(String caseId, String customerId, String text) {}
public record SupportDecision(String team, String priority, String status) {}
}
Diff: direkter Altpfad → explizites Routing mit Vergleich
- SupportResult result = legacyService.handle(command);
- return result;
+ SupportResult legacy = legacyService.handle(command);
+ if (cohortPolicy.isShadowEligible(command.customerId())) {
+ ModernResult modern = modernService.evaluate(command);
+ reconciliation.compare(legacy, modern);
+ }
+ return legacy; // Altpfad bleibt führendVom Diff zur sicheren nächsten Aktion – Legacy-Modernisierung
Nächster sicherer Schritt
Den Routing-Schalter zunächst nur für eine kleine, deterministische Kohorte aktivieren und den Altpfad weiterhin als führend behandeln.
Passender Nachweis
Characterization Test plus Reconciliation: Legacy- und Neupfad müssen für dieselbe Anfrage fachlich erklärbar übereinstimmen.
Bewusster Stopppunkt
Stoppen, sobald Routing, Vergleich, Rückschaltung und Betriebsverantwortung belastbar sind; keine zusätzliche Plattform ohne neuen Risikotreiber.
Praxis-Checkliste – Legacy-Modernisierung
Vor dem Einsatz im realen Projekt kurz gemeinsam abhaken. Die Liste ergänzt die Fallstudie und ersetzt weder Tests noch fachliche Freigaben.
- Vor dem ersten Eingriff: Geschäftskritische Pfade, Eigentümer, Änderungsdruck und bekannte Ausfallfolgen erfassen.↗ Testbeleg öffnen
- Sicherheitsnetz: Characterization Tests und beobachtbare Produktionskennzahlen für den Altpfad etablieren.↗ Sicherheitsnetz öffnen
- Kleiner erster Schritt: Eine stabile fachliche Grenze als Port einführen, ohne Routing oder Besitz sofort umzuschalten.↗ Zwischenstand öffnen
- Freigabenachweis: Kohortenweise Alt-/Neuvergleiche, Rollback und Betriebsbereitschaft nachweisen.↗ Endzustand öffnen
- Stoppbedingung: Abschaltung nur bei messbarer fachlicher Gleichheit und geklärter Betriebsverantwortung.↗ Stopppunkt öffnen
1. Das reale Problem: eine Änderung berührt alles
Der folgende Ausgangscode ist bewusst unangenehm, aber realistisch. Datenformat, Fachregel, Speicherung, Nachricht und Audit sind miteinander verklebt. Ein Team kann die Methode lesen, aber nicht sicher verändern.
public String process(String ticketId, String actor, Instant now) {
String[] row = rows.get(ticketId);
String priority = row[2] == null ? "NORMAL" : row[2].trim().toUpperCase();
String team;
if ("CRITICAL".equals(priority)
|| ("HIGH".equals(priority) && "PAYMENTS".equals(row[3]))) {
team = "MAJOR_INCIDENT";
} else if ("HIGH".equals(priority)) {
team = "SENIOR_SUPPORT";
} else {
team = "SERVICE_DESK";
}
row[4] = "ASSIGNED";
effects.execute("UPDATE ticket SET status='ASSIGNED',team='" + team + "' ...");
effects.mail(row[1], "Ticket " + ticketId + " assigned to " + team);
effects.audit(ticketId, actor, now, team);
return team;
}| Beobachtung | Risiko bei Änderung | Erster Nachweis |
|---|---|---|
| String-Array als Datenmodell | Spaltenpositionen und Null-Konventionen werden verwechselt. | repräsentative Legacy-Zeilen und Mapping-Tests |
| Regel und Seiteneffekt in einer Methode | fachlich richtige Entscheidung kann technisch doppelt wirken. | Snapshot von Team, Status, Mail und Audit |
| SQL und SMTP direkt im Ablauf | Unit-Tests brauchen Infrastruktur oder bleiben unvollständig. | beobachtbare Ports/Test Doubles |
2. Sicherheitsnetz: Verhalten einfrieren, nicht Struktur bestätigen
Characterization Tests dokumentieren das vorhandene Verhalten - einschließlich historischer Sonderfälle. Sie behaupten noch nicht, dass dieses Verhalten fachlich ideal ist. Sie machen spätere Änderungen sichtbar.
BehaviorSnapshot snapshot = new BehaviorSnapshot(
returnedTeam,
persistedStatus,
sentMails.size(),
auditEntries.size());
assertEquals(new BehaviorSnapshot(
"MAJOR_INCIDENT", "ASSIGNED", 1, 1), snapshot);3. Modernisierungsentscheidung: stabilisieren, kapseln oder schrittweise ersetzen
Die Strategie folgt aus Kritikalität, Änderungsdruck, Kopplung, Testevidenz und einer ersetzbaren fachlichen Grenze. Ein hochkritisches System mit geringer Testevidenz wird zuerst stabilisiert - selbst wenn langfristig ein Strangler geplant ist.
public Strategy recommendation() {
if (businessCriticality >= 8 && testEvidence <= 3) return STABILIZE;
if (replaceableBoundary && changePressure >= 7) return STRANGLE;
if (coupling >= 8) return ENCAPSULATE;
return REFACTOR_IN_PLACE;
}| Entscheidung | Wann | Nächster kleiner Schritt |
|---|---|---|
| Stabilisieren | kritisch, wenig Evidenz | Characterization Tests, Observability, Fehlerpfade |
| Kapseln | hohe Kopplung, aber kein sicherer Ersatzschnitt | Facade/Port um Legacy-Grenze |
| Strangler | stabile fachliche Grenze und hoher Änderungsdruck | Kohorte, Routing, Reconciliation |
4. Die erste sichere Grenze: ein fachlicher Port
Alt- und Neupfad erhalten denselben fachlichen Vertrag. Der Vertrag beschreibt keine SQL-Spalten, SMTP-Details oder Legacy-Statuscodes.
@FunctionalInterface
public interface SupportProcessingPort {
SupportOutcome process(SupportCommand command);
}
public record SupportOutcome(
String ticketId,
String status,
String team,
String diagnosticCode) {
public boolean businessEquals(SupportOutcome other) {
return other != null
&& status.equals(other.status)
&& team.equals(other.team);
}
}Erst an dieser Grenze kann der alte Pfad gekapselt, der neue Pfad gebaut und das Ergebnis fachlich verglichen werden.
5. Strangler-Schnitt: kleine Kohorten statt globaler Umschaltung
Die Migration beginnt mit einer deterministischen Kohorte. Derselbe Vorgang bleibt bei Wiederholungen im selben Pfad. Der neue Pfad ist für diese Kohorte führend; der Altpfad liefert einen Schattenvergleich.
public SupportOutcome process(SupportCommand command) {
if (!cohort.includes(command.ticketId())) {
return legacy.process(command);
}
SupportOutcome primary = modern.process(command);
SupportOutcome shadow = legacy.process(command);
reconciliation.record(command.ticketId(), primary, shadow);
return primary;
}6. Parallelbetrieb: Unterschiede erklären statt nur zählen
Reconciliation vergleicht fachliche Merkmale. Technisch unterschiedliche Diagnosecodes können akzeptabel sein; unterschiedliche Teams oder Statuswerte benötigen eine fachliche Erklärung.
| Vergleich | Bewertung | Aktion |
|---|---|---|
| Status und Team gleich | fachlich äquivalent | Kohorte vorsichtig erhöhen |
| Diagnosecode anders, Entscheidung gleich | erklärbare technische Differenz | Mapping dokumentieren |
| Team oder Status anders | fachliche Abweichung | Kohorte stoppen, Fall analysieren |
| Shadow technisch nicht verfügbar | keine Vergleichsevidenz | Primärpfad nicht automatisch zurückrollen; Evidenzlücke markieren |
7. Betriebsübernahme: Verantwortung praktisch beweisen
Der neue Pfad gilt nicht als fertig, weil Entwicklung und Tests abgeschlossen sind. Operations muss degraded mode, Alarmierung und Rückfall praktisch beherrschen.
public boolean ready() {
return runbooks.containsAll(Set.of(
"start", "degraded-mode", "rollback"))
&& owners.containsAll(Set.of(
"product", "operations", "data"))
&& alertsVerified
&& gameDayPassed
&& rollbackDrillPassed;
}- Runbook enthält Entscheidungspunkte, nicht nur Befehle.
- Game Day simuliert Altpfad-Ausfall, Abweichung und fehlerhafte Umschaltung.
- Produkt-, Betriebs- und Datenverantwortung besitzen benannte Stellvertretungen.
8. Kontrollierte Abschaltung: Evidenz statt Termin
Ein Stichtag kann organisatorisch notwendig sein, ersetzt aber keine Abschaltkriterien. Das Beispiel verlangt ausreichende Fallzahl, erklärte fachliche Gleichheit, geringe technische Fehlerquote, stabile Betriebszeit, bewiesenen Rollback und akzeptierte Betriebsverantwortung.
public boolean permitsCutover() {
return comparedCases >= 10_000
&& explainedDifferenceRate >= 0.999
&& technicalErrorRate <= 0.001
&& stableDays >= 14
&& rollbackProven
&& operationsAccepted;
}Bewusster Stopppunkt
Die Modernisierung endet, wenn der fachliche Pfad testbar, der Fremdvertrag gekapselt, der Parallelvergleich stabil, der Betrieb übernommen und der Altpfad kontrolliert entfernt ist. Ein zusätzlicher Microservice, Event Bus oder universelles Regelwerk ist ohne neue Änderungsachse kein Qualitätsgewinn.
01 - Schritt 01: Characterization Tests auf-/zuklappen
Das beobachtbare Verhalten des alten Ticket-Services wird vor jeder Strukturänderung mit Tests eingefroren.
Schritt 01/09Legacy Support
02 - Schritt 02: Use Case aus Controller extrahieren auf-/zuklappen
HTTP, Session, SQL und Geschäftsablauf werden getrennt. Der neue Anwendungsdienst erhält nur fachliche Eingaben.
Schritt 02/09Legacy Support
03 - Schritt 03: String-Arrays durch SupportTicket ersetzen auf-/zuklappen
Das untypisierte Legacy-Datenformat wird an der Grenze in ein valides Fachobjekt übersetzt.
Schritt 03/09Legacy Support
04 - Schritt 04: Repository Adapter einführen auf-/zuklappen
Der Use Case kennt keine SQL-Spalten und keine Legacy-DAO-Methoden mehr.
Schritt 04/09Legacy Support
05 - Schritt 05: Fachregeln als Specifications auf-/zuklappen
Sofortbearbeitung und Schließbarkeit werden als benannte, kombinierbare Regeln formuliert.
Schritt 05/09Legacy Support
06 - Schritt 06: Teamzuordnung als Strategy auf-/zuklappen
Die Auswahl von Service Desk, Senior Support oder Major Incident ist austauschbar.
Schritt 06/09Legacy Support
07 - Schritt 07: Ports and Adapters für Benachrichtigungen auf-/zuklappen
SMTP, Chat und Ticketing-Webhooks liegen hinter einem fachlichen Ausgangsport.
Schritt 07/09Legacy Support
08 - Schritt 08: Domain Events für Folgeaktionen auf-/zuklappen
Zuweisung und Lösung veröffentlichen Ereignisse; Audit und Benachrichtigung reagieren getrennt.
Schritt 08/09Legacy Support
09 - Schritt 09: Strangler Facade für schrittweise Migration auf-/zuklappen
Alte Aufrufer bleiben stabil, während einzelne Aktionen kontrolliert an den neuen Kern delegiert werden.
Schritt 09/09Legacy Support
10 - Reale Java-Dateien und Test auf-/zuklappen
Das Maven-Modul modules/legacy-support-case-study enthält den Repository Adapter, Specifications, Assignment Strategy, Facade, Strangler Gateway und einen JUnit-Test.
public String triageAndAssign(String id) {
var ticket = repository.findById(id).orElseThrow();
var team = assignment.assign(ticket.withStatus(TRIAGED));
repository.save(ticket.withStatus(ASSIGNED));
events.publish(new TicketAssigned(id, team));
return team;
}11 - Modernisierungsinventar: erst Fakten, dann Zielarchitektur
Die Modernisierung beginnt nicht mit einer Technologieentscheidung, sondern mit einer belastbaren Bestandsaufnahme. Für jeden fachlichen Pfad werden Kritikalität, Änderungsdruck, Kopplung, Datenverantwortung, Betriebsrisiko und vorhandene Testevidenz erfasst. Dadurch wird sichtbar, welche Teile zuerst stabilisiert, gekapselt, schrittweise abgelöst oder bewusst weiterbetrieben werden.
| Prüffeld | Leitfrage | Nachweis |
|---|---|---|
| Fachliche Kritikalität | Welche Geschäftsentscheidung darf nicht ausfallen oder unbemerkt abweichen? | Prozesskennzahl, SLO, Compliance-Vorgabe |
| Änderungsdruck | Wo häufen sich fachliche Änderungen, Hotfixes oder Sonderfälle? | Change-Historie, Incident- und Ticketdaten |
| Kopplung | Welche Aufrufer, Tabellen, Batchläufe und Fremdsysteme hängen tatsächlich am Pfad? | Schritttime-Traces, SQL- und Schnittstelleninventar |
| Beobachtbarkeit | Kann aktuelles Verhalten reproduzierbar verglichen werden? | Characterization Tests, Golden Master, Telemetrie |
| Eigentum | Wer entscheidet über Regeln, Daten und Abschaltkriterien? | benannte fachliche und technische Verantwortliche |
12 - Entscheidungsbaum: stabilisieren, kapseln, Strangler oder Rewrite
Der Entscheidungsbaum verhindert reflexartige Big-Bang-Rewrites. Fehlt Beobachtbarkeit, ist der nächste Schritt immer ein Sicherheitsnetz. Ist die Domäne stabil, genügt häufig eine saubere Fassade mit Adaptern und Architekturregeln. Bei starkem Änderungsdruck und möglichem Parallelbetrieb ist der Strangler-Ansatz vorzuziehen. Ein gezielter Rewrite bleibt auf klar begrenzte, beweisbar ersetzbare Teile beschränkt.
public Approach decide(Evidence evidence) {
if (!evidence.behaviorObservable()) return STABILIZE_FIRST;
if (!evidence.domainChangesFrequently()) return ENCAPSULATE;
if (evidence.parallelSchrittPossible()) return STRANGLER;
if (evidence.replacementBoundaryClear()) return TARGETED_REWRITE;
return ENCAPSULATE;
}Die Entscheidung ist kein einmaliges Architektururteil. Sie wird nach jedem Migrationsschnitt mit aktuellen Messwerten erneut getroffen.
13 - Anti-Patterns und konkrete Gegenmaßnahmen
| Anti-Pattern | Warum es scheitert | Gegenmaßnahme |
|---|---|---|
| Big-Bang Rewrite | Feedback kommt spät; Alt- und Neulogik driften lange unbemerkt auseinander. | Vertikale Migrationsscheiben, Kohortenrouting, Differential Tests und klare Abschaltkriterien. |
| Shared Database Forever | Neue Module bleiben über Tabellen, Trigger und implizite Schreibrechte gekoppelt. | Datenbesitz benennen, Schnittstellenvertrag einführen, Expand/Contract und Reconciliation verwenden. |
| Distributed Monolith | Services benötigen synchrone Ketten, gemeinsame Releases und zentrale Fehlerbehandlung. | Fachliche Grenzen schärfen, lokale Entscheidungen halten, Events nur für echte Entkopplung einsetzen. |
| Temporary Dual Schritt ohne Ende | Alt und Neu werden dauerhaft parallel betrieben; Kosten und Abweichungen steigen. | Owner, Enddatum, Qualitätsmetriken und automatisierbare Abschaltbedingungen festlegen. |
| Framework-first Modernisierung | Technologie ändert sich, während Begriffe, Regeln und Datenkopplung unverändert bleiben. | Zuerst Domänenmodell, Ports, Verträge und Beweise; Framework erst am Adapterrand. |
14 - Domänenwissen aus Legacy-Code sichern
Legacy-Code enthält nicht nur technische Schulden, sondern oft die einzige vollständige Spur historischer Fachentscheidungen. Bedingungen, Datenkorrekturen, Batch-Reihenfolgen und manuelle Betriebsabläufe werden deshalb nicht ungeprüft entfernt. Sie werden als Fachbegriffe, Invarianten, Entscheidungstabellen, Zustandsübergänge und Ereignisse explizit gemacht.
- Repräsentative Geschäftsfälle und Ausnahmen aus Produktion, Support und Fachbereich sammeln.
- Beobachtetes Verhalten mit Characterization Tests und kanonischen Beispieldaten sichern.
- Regeln nach fachlicher Absicht, historischer Kompatibilität und rein technischem Workaround klassifizieren.
- Entscheidungstabellen und Zustandsmodelle gemeinsam mit benannten Verantwortlichen freigeben.
- Nur bestätigte Regeln in den neuen Kern übernehmen; Altkompatibilität bewusst am Adapterrand halten.
Kapitel 13 Enterprise-Fallstudie: Customer Support9 Lernschritte
Die Fallstudie begleitet denselben Ticketprozess vom prozeduralen Legacy-Skript bis zu einem Fachmodell mit expliziten Statusübergängen, Rollen, SLA-Policies, Audit und Eskalation. Jeder Schritt löst ein zuvor belegtes Risiko.
01. Ausgangscode: Ein Ticketprozessor trägt zu viele Entscheidungen
8 sichtbare Codeblöcke und 9 Lernschritte; Rollen, SLA, Audit und Eskalation werden einzeln sichtbar.
Diese Geschichte modelliert gültige Ticketübergänge, Zuständigkeit, Eskalation, Audit und Benachrichtigung als zusammenhängenden Supportprozess.
Der Legacy-Prozessor validiert Eingaben, interpretiert Rollen, wählt Teams, berechnet SLA-Fristen, schreibt Status, Audit und Benachrichtigung. Die Strings wirken kompakt, verstecken aber ungültige Zustände und unklare Verantwortungen.
package com.aydinsude.workbench.legacy.supportstory;
import java.time.*;
import java.util.*;
/** Problematischer Ausgangscode: Regeln, Status, SLA, Audit und Benachrichtigung sind vermischt. */
public final class LegacyTicketProcessor {
private final List<String> database;
private final List<String> audit;
private final List<String> notifications;
private final Clock clock;
public LegacyTicketProcessor(List<String> database, List<String> audit, List<String> notifications, Clock clock) {
this.database=database; this.audit=audit; this.notifications=notifications; this.clock=clock;
}
public String process(String id, String category, String tier, String message, String actorRole) {
if(id==null || id.isBlank() || message==null || message.isBlank()) return "INVALID";
String team="GENERAL"; int slaHours=48; String status="NEW";
if("SECURITY".equalsIgnoreCase(category)) { team="SECURITY"; slaHours=1; }
else if("BILLING".equalsIgnoreCase(category)) { team="BILLING"; slaHours="GOLD".equalsIgnoreCase(tier)?4:12; }
else if("TECH".equalsIgnoreCase(category)) { team="TECHNICAL"; slaHours="GOLD".equalsIgnoreCase(tier)?2:8; }
if("AGENT".equals(actorRole) || "SUPERVISOR".equals(actorRole)) status="ASSIGNED";
if("SUPERVISOR".equals(actorRole) && message.toLowerCase().contains("urgent")) team="SECOND_LEVEL";
Instant due=Instant.now(clock).plus(Duration.ofHours(slaHours));
database.add(id+"|"+status+"|"+team+"|"+due);
audit.add(id+"|CREATED|"+actorRole+"|"+Instant.now(clock));
notifications.add("MAIL:"+id+":"+team);
return status+":"+team+":"+slaHours;
}
}| Beobachtung | Risiko | Nachweis |
|---|---|---|
| Status als String | unzulässige Übergänge | Übergangstests |
| Rolle in if-Kaskade | verteilte Berechtigung | Rollenfälle charakterisieren |
| SLA und Team gekoppelt | Regeländerung verändert Ablauf | Fristen pro Kategorie sichern |
| Audit direkt geschrieben | partielle Seiteneffekte | Reihenfolge beobachten |
02. Sicherheitsnetz: Beobachtbares Verhalten einfrieren
Vor der Modellierung werden Status, Team, SLA, Audit und Benachrichtigung für Security-, Billing- und Technikfälle gesichert. Diese Tests schützen den Vertrag, nicht die bestehende Struktur.
package com.aydinsude.workbench.legacy.supportstory;
import org.junit.jupiter.api.Test;
import java.time.*;
import java.util.*;
import static org.junit.jupiter.api.Assertions.*;
class CustomerSupportStoryTest {
private static final Instant NOW=Instant.parse("2026-07-11T10:00:00Z");
@Test void legacy_security_ticket_keeps_observed_contract(){
var db=new ArrayList<String>(); var audit=new ArrayList<String>(); var notes=new ArrayList<String>();
var legacy=new LegacyTicketProcessor(db,audit,notes,Clock.fixed(NOW,ZoneOffset.UTC));
assertEquals("ASSIGNED:SECURITY:1",legacy.process("T-1","SECURITY","STANDARD","breach","AGENT"));
assertEquals(1,db.size()); assertEquals(1,audit.size()); assertEquals("MAIL:T-1:SECURITY",notes.get(0));
}
@Test void modern_service_assigns_security_and_calculates_sla(){
var saved=new ArrayList<SupportTicket>(); var audit=new ArrayList<String>(); var notes=new ArrayList<String>();
var service=new SupportTicketService(new AssignmentPolicy(),new SlaPolicy(),saved::add,(id,a,d)->audit.add(a+":"+d),new SupportNotificationPort(){ public void assigned(TicketId id,SupportTeam t){notes.add(t.name());} public void escalated(TicketId id,int level){}},Clock.fixed(NOW,ZoneOffset.UTC));
var result=service.open(new TicketCommand("T-1","SECURITY","STANDARD","breach"));
assertEquals(SupportTeam.SECURITY,result.team()); assertEquals(NOW.plus(Duration.ofHours(1)),result.dueAt()); assertEquals(TicketStatus.ASSIGNED,result.status());
}
@Test void aggregate_rejects_invalid_transition(){
var ticket=new SupportTicket(new TicketId("T-2"),NOW);
assertThrows(IllegalStateException.class,ticket::resolve);
}
}03. Fachtypen und Statusübergänge explizit machen
TicketId, TicketStatus und SupportTeam ersetzen mehrdeutige Strings. Das Ticket-Aggregat erlaubt nur Übergänge, die fachlich Sinn ergeben. Eine direkte Auflösung aus NEW wird nun abgewiesen.
package com.aydinsude.workbench.legacy.supportstory;
import java.time.Instant;
import java.util.Objects;
/** Aggregate Root: schützt Status und Eskalationsinvarianten. */
public final class SupportTicket {
private final TicketId id;
private TicketStatus status;
private SupportTeam assignedTeam;
private final Instant openedAt;
private int escalationLevel;
public SupportTicket(TicketId id, Instant openedAt) {
this.id = Objects.requireNonNull(id);
this.openedAt = Objects.requireNonNull(openedAt);
this.status = TicketStatus.NEW;
this.assignedTeam = SupportTeam.UNASSIGNED;
}
public void assignTo(SupportTeam team) {
if (status == TicketStatus.CLOSED) throw new IllegalStateException("closed ticket cannot be assigned");
assignedTeam = Objects.requireNonNull(team);
status = TicketStatus.ASSIGNED;
}
public void startWork() {
if (status != TicketStatus.ASSIGNED) throw new IllegalStateException("ticket must be assigned");
status = TicketStatus.IN_PROGRESS;
}
public void resolve() {
if (status != TicketStatus.IN_PROGRESS) throw new IllegalStateException("ticket must be in progress");
status = TicketStatus.RESOLVED;
}
public void close() {
if (status != TicketStatus.RESOLVED) throw new IllegalStateException("ticket must be resolved");
status = TicketStatus.CLOSED;
}
public void escalate() { escalationLevel++; }
public TicketId id(){ return id; }
public TicketStatus status(){ return status; }
public SupportTeam assignedTeam(){ return assignedTeam; }
public Instant openedAt(){ return openedAt; }
public int escalationLevel(){ return escalationLevel; }
}04. Team- und SLA-Entscheidungen als Policies isolieren
Teamzuordnung und SLA-Frist ändern sich unabhängig vom Workflow. Deshalb werden sie als Policies modelliert. Die Policy liefert eine Entscheidung; sie speichert nichts und versendet keine Nachricht.
package com.aydinsude.workbench.legacy.supportstory;
/** Policy Pattern: fachliche Team- und SLA-Entscheidung. */
public final class AssignmentPolicy {
public AssignmentDecision decide(TicketCommand command) {
return switch(command.category()) {
case "SECURITY" -> new AssignmentDecision(SupportTeam.SECURITY, 1);
case "BILLING" -> new AssignmentDecision(SupportTeam.BILLING, command.customerTier().equals("GOLD")?4:12);
case "TECH" -> new AssignmentDecision(SupportTeam.TECHNICAL, command.customerTier().equals("GOLD")?2:8);
default -> new AssignmentDecision(SupportTeam.SECOND_LEVEL, 24);
};
}
public record AssignmentDecision(SupportTeam team, int slaHours) {}
}package com.aydinsude.workbench.legacy.supportstory;
import java.time.*;
/** Policy Pattern: SLA-Frist und Eskalation werden unabhängig vom Workflow entschieden. */
public final class SlaPolicy {
public Instant dueAt(Instant openedAt, int hours) { return openedAt.plus(Duration.ofHours(hours)); }
public boolean isBreached(Instant dueAt, Instant now) { return !now.isBefore(dueAt); }
}05. Seiteneffekte über Ports sichtbar machen
Persistenz, Audit und Benachrichtigung werden als Ports benannt. Dadurch lässt sich der fachliche Ablauf testen, ohne Datenbank oder Mailserver zu starten. Die Ports sind keine Einladung zu Microservices, sondern kontrollierbare Grenzen.
package com.aydinsude.workbench.legacy.supportstory;
public interface TicketRepository { void save(SupportTicket ticket); }
package com.aydinsude.workbench.legacy.supportstory;
public interface SupportAuditPort { void record(TicketId id, String action, String detail); }
package com.aydinsude.workbench.legacy.supportstory;
public interface SupportNotificationPort { void assigned(TicketId id, SupportTeam team); void escalated(TicketId id, int level); }06. Application Service: Reihenfolge und Verantwortung
Der Application Service koordiniert Erzeugung, Assignment, Speicherung, Audit und Benachrichtigung. Fachliche Regeln bleiben im Aggregat und in den Policies; technische Wirkungen liegen hinter Ports.
package com.aydinsude.workbench.legacy.supportstory;
import java.time.*;
import java.util.Objects;
/** Application Service: koordiniert Aggregate, Policies und Ports. */
public final class SupportTicketService {
private final AssignmentPolicy assignment; private final SlaPolicy sla; private final TicketRepository repository;
private final SupportAuditPort audit; private final SupportNotificationPort notifications; private final Clock clock;
public SupportTicketService(AssignmentPolicy assignment, SlaPolicy sla, TicketRepository repository, SupportAuditPort audit, SupportNotificationPort notifications, Clock clock) {
this.assignment=Objects.requireNonNull(assignment); this.sla=Objects.requireNonNull(sla); this.repository=Objects.requireNonNull(repository); this.audit=Objects.requireNonNull(audit); this.notifications=Objects.requireNonNull(notifications); this.clock=Objects.requireNonNull(clock);
}
public TicketResult open(TicketCommand command) {
var openedAt=Instant.now(clock);
var decision=assignment.decide(command);
var ticket=new SupportTicket(new TicketId(command.ticketId()), openedAt);
ticket.assignTo(decision.team());
repository.save(ticket);
audit.record(ticket.id(), "ASSIGNED", decision.team().name());
notifications.assigned(ticket.id(), decision.team());
return new TicketResult(ticket.id(), ticket.status(), decision.team(), sla.dueAt(openedAt, decision.slaHours()));
}
public record TicketResult(TicketId id, TicketStatus status, SupportTeam team, Instant dueAt) {}
}| Schritt | Owner | Fehlerstrategie |
|---|---|---|
| Team/SLA entscheiden | Policy | deterministisch erneut berechenbar |
| Status ändern | Aggregate | ungültigen Übergang ablehnen |
| Speichern | Repository-Port | Transaktion/Rollback |
| Audit/Notification | Output-Ports | idempotente Wiederholung |
07. SLA-Verstoß und Eskalation getrennt behandeln
Ein SLA-Verstoß ist eine fachliche Tatsache. Die Eskalation prüft Frist und Ticketzustand, erhöht das Level und protokolliert die Entscheidung. Geschlossene Tickets werden nicht mehr eskaliert.
package com.aydinsude.workbench.legacy.supportstory;
import java.time.*;
import java.util.Objects;
/** Domain Service: Eskalation nur bei belegtem SLA-Verstoß. */
public final class EscalationService {
private final SlaPolicy sla; private final TicketRepository repository; private final SupportAuditPort audit; private final SupportNotificationPort notifications;
public EscalationService(SlaPolicy sla, TicketRepository repository, SupportAuditPort audit, SupportNotificationPort notifications) { this.sla=Objects.requireNonNull(sla); this.repository=Objects.requireNonNull(repository); this.audit=Objects.requireNonNull(audit); this.notifications=Objects.requireNonNull(notifications); }
public boolean escalateIfRequired(SupportTicket ticket, Instant dueAt, Instant now) {
if(!sla.isBreached(dueAt, now) || ticket.status()==TicketStatus.CLOSED) return false;
ticket.escalate(); repository.save(ticket); audit.record(ticket.id(), "ESCALATED", "level="+ticket.escalationLevel()); notifications.escalated(ticket.id(), ticket.escalationLevel()); return true;
}
}08. Audit, Rollen und Benachrichtigung nachvollziehbar betreiben
Audit speichert fachliche Aktionen mit Ticket-ID und Begründung. Benachrichtigungen reagieren auf bestätigte Statusänderungen. Rollenberechtigungen sollten an der Use-Case-Grenze geprüft werden; sie gehören weder in SQL noch in Mailcode.
09. Endzustand und bewusster Stopppunkt
Der Endzustand ist nicht maximal abstrakt. Das Aggregat schützt Status, Policies entscheiden Assignment und SLA, der Application Service koordiniert Ports, und der Eskalationsdienst behandelt Fristverletzungen. Ein Workflow-Framework, Event Sourcing oder eine verteilte Saga werden erst eingeführt, wenn Varianten, Laufzeit oder Integrationsdruck dies messbar erfordern.
- Neue Ticketkategorien verändern die Assignment-Policy, nicht den gesamten Ablauf.
- Neue Benachrichtigungskanäle implementieren den Port.
- Statusänderungen bleiben im Aggregat testbar.
- SLA- und Eskalationsregeln sind unabhängig prüfbar.
Vollständige Quellen: modules/legacy-support-case-study/.../supportstory.
Zwischenstand – Status, Team und SLA sind Fachmodell
Dieser Stand macht implizite Regeln sichtbar: Teamzuordnung und SLA werden entschieden, bevor Persistenz und Benachrichtigung erfolgen. Eskalation und Audit können danach gezielt ergänzt werden.
package com.aydinsude.workbench.intermediate;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.util.Objects;
/** Zwischenstand: Status und SLA sind explizit; Benachrichtigung bleibt ein Port. */
public final class CustomerSupportIntermediate {
private final AssignmentPolicy assignment;
private final TicketRepository repository;
private final NotificationPort notifications;
private final Clock clock;
public CustomerSupportIntermediate(AssignmentPolicy assignment,
TicketRepository repository,
NotificationPort notifications,
Clock clock) {
this.assignment = Objects.requireNonNull(assignment);
this.repository = Objects.requireNonNull(repository);
this.notifications = Objects.requireNonNull(notifications);
this.clock = Objects.requireNonNull(clock);
}
public Ticket open(OpenTicket command) {
Instant openedAt = clock.instant();
Team team = assignment.assign(command.category(), command.customerTier());
Duration responseTarget = command.customerTier() == CustomerTier.PREMIUM
? Duration.ofMinutes(30) : Duration.ofHours(4);
Ticket ticket = new Ticket(command.ticketId(), TicketStatus.OPEN, team,
openedAt, openedAt.plus(responseTarget));
repository.save(ticket);
notifications.ticketOpened(ticket);
return ticket;
}
public interface AssignmentPolicy { Team assign(Category category, CustomerTier tier); }
public interface TicketRepository { void save(Ticket ticket); }
public interface NotificationPort { void ticketOpened(Ticket ticket); }
public record OpenTicket(String ticketId, Category category, CustomerTier customerTier) {}
public record Ticket(String id, TicketStatus status, Team team, Instant openedAt, Instant responseDueAt) {}
public enum TicketStatus { OPEN, IN_PROGRESS, RESOLVED, CLOSED }
public enum Team { GENERAL, BILLING, TECHNICAL, ESCALATION }
public enum Category { GENERAL, BILLING, TECHNICAL }
public enum CustomerTier { STANDARD, PREMIUM }
}
Diff: implizite Strings → explizite Fachentscheidungen
- ticket.status = "ASSIGNED";
- ticket.team = category.equals("VIP") ? "PRIORITY" : "GENERAL";
- ticket.dueAt = now.plusHours(24);
+ TicketStatus next = TicketStatus.ASSIGNED;
+ SupportTeam team = assignmentPolicy.assign(ticket);
+ SlaDeadline deadline = slaPolicy.deadlineFor(ticket, now);
+ TicketAssignment assignment = ticket.assignTo(team, next, deadline);Vom Diff zur sicheren nächsten Aktion – Customer Support
Nächster sicherer Schritt
Den Statusübergang als Fachentscheidung kapseln, danach Team- und SLA-Policy anschließen; Benachrichtigung bleibt noch außerhalb.
Passender Nachweis
Zustands- und Policy-Test: ungültige Übergänge werden abgewiesen, Team und SLA bleiben für denselben Fall deterministisch.
Bewusster Stopppunkt
Stoppen, wenn Status, Rollen, SLA, Audit und Benachrichtigung getrennt testbar sind; keine Workflow-Engine ohne echte Variantenvielfalt.
Praxis-Checkliste – Customer Support
Vor dem Einsatz im realen Projekt kurz gemeinsam abhaken. Die Liste ergänzt die Fallstudie und ersetzt weder Tests noch fachliche Freigaben.
- Vor dem ersten Eingriff: Statuswechsel, Rollenrechte, SLA-Fristen, Eskalationen und Auditpflichten erfassen.↗ Testbeleg öffnen
- Sicherheitsnetz: Zulässige und unzulässige Übergänge sowie Zeitgrenzen mit Characterization Tests sichern.↗ Sicherheitsnetz öffnen
- Kleiner erster Schritt: Status und Übergangsregeln explizit modellieren, bevor Ports oder Frameworks eingeführt werden.↗ Zwischenstand öffnen
- Freigabenachweis: Übergangs-, Rollen-, SLA- und Auditfälle einschließlich Wiederholung prüfen.↗ Endzustand öffnen
- Stoppbedingung: Kein Workflow-Framework, solange Zustandsmodell und Policies den Änderungsdruck tragen.↗ Stopppunkt öffnen
Kapitel 14 Praxislabor: Document Processing8 Lerneinheiten
Von Typcodes, binären Payloads und direkter Archivierung zu einer formatneutralen, testbaren Verarbeitungspipeline. Jeder Schritt besitzt eine kompakte themenspezifische SVG.
Document Processing Refactoring Workbench
Von Typcodes, binären Payloads und direkter Archivierung zu einer formatneutralen, testbaren Verarbeitungspipeline. Jeder Schritt besitzt eine kompakte themenspezifische SVG.
Value Object, Strategy, Factory Registry, Specification, Pipeline, Ports and Adapters und Domain Events.
01 - Schritt 01: Characterization Tests auf-/zuklappen
Das Verhalten des alten Prozessors wird mit repräsentativen JSON-, CSV- und Fehlerfällen eingefroren.
Schritt 01/07Document Processing
02 - Schritt 02: DocumentEnvelope statt String und byte[] auf-/zuklappen
Format, Status, Empfangszeit und Payload werden in einem validierten Fachobjekt gebündelt.
Schritt 02/07Document Processing
03 - Schritt 03: DocumentReader als Strategy auf-/zuklappen
Formatspezifisches Lesen wandert aus der switch-Kaskade in austauschbare Reader.
Schritt 03/07Document Processing
04 - Schritt 04: Reader Registry als Factory auf-/zuklappen
Die Auswahl des passenden Readers erfolgt zentral anhand des Dokumentformats.
Schritt 04/07Document Processing
05 - Schritt 05: Validierung als Specification auf-/zuklappen
Inhalts- und Größenregeln werden benannt, kombiniert und separat getestet.
Schritt 05/07Document Processing
06 - Schritt 06: Verarbeitung als Pipeline auf-/zuklappen
Lesen, Validieren, Transformieren und Archivieren werden als klarer Ablauf sichtbar.
Schritt 06/07Document Processing
07 - Schritt 07: ArchivePort und Domain Events auf-/zuklappen
Das Archiv wird über einen Port angesprochen; Erfolg und Ablehnung werden als Ereignisse veröffentlicht.
Schritt 07/07Document Processing
public String process(DocumentEnvelope envelope) {
var parsed = readers.readerFor(envelope.format()).read(envelope);
if (!rules.isSatisfiedBy(parsed)) {
events.publish(new DocumentRejected(envelope.id(), "validation"));
throw new IllegalArgumentException("Document rejected");
}
var canonical = transformer.transform(parsed);
var archiveId = archive.archive(canonical);
events.publish(new DocumentArchived(envelope.id(), archiveId));
return archiveId;
}08 - Reale Java-Dateien und Test auf-/zuklappen
Das Maven-Modul modules/document-processing-case-study enthält Legacy-Ausgangspunkt, Fachmodell, Reader-Strategien, Factory Registry, Specifications, Pipeline-Facade, Archiv-Port, Domain Events, Strangler Gateway und einen JUnit-Test.
Kapitel 15 Enterprise-Fallstudie: restartfähiger Dokumentenbatch9 Lernschritte
Eine durchgehende Enterprise-Fallstudie: Ein nebenwirkungsreicher Datei-Batch wird ohne Big Bang in einen testbaren, idempotenten und restartfähigen Ablauf überführt.
Ausgangssystem
9 sichtbare Codeblöcke; Restart, Checkpoint, Idempotenz und Dead Letter sind getrennt vergleichbar.
Eine Schleife mischt Reader, Validierung, Speicherung, Fortschritt und Fehlerbehandlung.
Betriebszusage
Vor dem Umbau wird festgelegt, was nach Absturz, Wiederanlauf und fehlerhaften Datensätzen gelten muss.
Schrittweise Entkopplung
Fachtypen, Ports, Policy, Idempotenzschlüssel und Checkpoint-Reihenfolge entstehen nacheinander.
Recovery
Fehlerjournal, Dead-Letter-Entscheidung und reproduzierbare Wiederaufnahme machen den Betrieb nachvollziehbar.
So liest du diese Fallstudie
Diese Geschichte erklärt, wie Reader, Speicherung und Checkpoint so geordnet werden, dass Neustart, Idempotenz, Recovery und Dead Letter nachvollziehbar funktionieren.
Alle Abschnitte entwickeln denselben Dokumentenbatch weiter. Der Code wird nicht in unabhängigen Mini-Beispielen erklärt. Nach jedem Schritt bleibt sichtbar, welche Betriebszusage gewonnen wurde und welches Risiko noch offen ist.
1. Legacy
LegacyBatchProcessor
Speicherung und Checkpoint liegen in derselben Schleife; Fehler werden nur ausgegeben.
2. Vertrag
Characterization- und Restart-Tests
At-least-once, Skip und Abbruch werden explizit.
3. Struktur
Fachtypen, Ports und Validation Policy
Der Ablaufkern kennt keine Datenbank- oder Dateidetails mehr.
4. Recovery
Idempotenz, Checkpoint, Failure Journal und Dead Letter
Wiederanlauf und manuelle Nachbearbeitung sind getrennt.
5. Endzustand
Ein kleiner Application Service mit begründeter Stopplinie
Kein unnötiges Framework, solange ein Batch genügt.
Ausgangslage und Risikoanalyse
Der Ausgangscode liest Typcodes, validiert, schreibt Daten und Fortschritt in derselben Schleife und verschluckt Fehler. Die fachliche Kernfrage lautet deshalb nicht nur „Wie wird der Code schöner?“, sondern „Welche Zusage macht der Batch nach einem Absturz?“
package com.aydinsude.workbench.document.batch;
import java.util.List;
import java.util.Map;
/** Bewusst problematischer Ausgangscode für die Fallstudie. */
public final class LegacyBatchProcessor {
private final Map<String, byte[]> database;
public LegacyBatchProcessor(Map<String, byte[]> database) { this.database = database; }
public int run(String jobId, List<BatchDocument> rows, int startAt) {
int done = 0;
for (int i = startAt; i < rows.size(); i++) {
BatchDocument row = rows.get(i);
try {
if (!"CSV".equals(row.format()) && !"JSON".equals(row.format())) continue;
if (row.payload().length > 1_000_000) continue;
// versteckte Idempotenzannahme, technische Speicherung und Fortschritt in einer Methode
database.put(jobId + ":" + row.id(), row.payload());
database.put(jobId + ":checkpoint", Integer.toString(i + 1).getBytes());
done++;
} catch (RuntimeException ex) {
System.err.println("row=" + row.id() + " failed: " + ex.getMessage());
}
}
return done;
}
}
Schritt 1 - Verhalten mit Characterization Tests sichern
Vor jeder Strukturänderung werden unterstützte Formate, Skip-Verhalten, Fehlerfortsetzung und bestehende Checkpoint-Semantik festgehalten. Unerwünschtes Altverhalten wird zunächst dokumentiert, nicht heimlich korrigiert.
5. Endzustand@Test
void legacy_batch_confirms_each_successful_row() {
int processed = legacy.run("nightly", rows, 0);
assertEquals(2, processed);
assertEquals("2", checkpointValue());
}Schritt 2 - Fachliche Eingabe und Ergebnis explizit machen
BatchDocument schützt Identität, Format und Payload. BatchItemResult unterscheidet verarbeitet, bewusst übersprungen und fehlgeschlagen. Damit verschwinden lose Strings und unklare Exception-Pfade aus der öffentlichen Schnittstelle.
package com.aydinsude.workbench.document.batch;
import java.util.Objects;
/** Fachlicher, unveränderlicher Eingabedatensatz. */
public record BatchDocument(String id, String format, byte[] payload) {
public BatchDocument {
if (id == null || id.isBlank()) throw new IllegalArgumentException("id");
if (format == null || format.isBlank()) throw new IllegalArgumentException("format");
Objects.requireNonNull(payload, "payload");
if (payload.length == 0) throw new IllegalArgumentException("payload");
payload = payload.clone();
}
@Override public byte[] payload() { return payload.clone(); }
}
package com.aydinsude.workbench.document.batch;
/** Explizites Ergebnis statt String- und Exception-Mischung. */
public record BatchItemResult(String documentId, Status status, String detail) {
public enum Status { PROCESSED, SKIPPED, FAILED }
public static BatchItemResult processed(String id) { return new BatchItemResult(id, Status.PROCESSED, "ok"); }
public static BatchItemResult skipped(String id, String reason) { return new BatchItemResult(id, Status.SKIPPED, reason); }
public static BatchItemResult failed(String id, String reason) { return new BatchItemResult(id, Status.FAILED, reason); }
}
Schritt 3 - Infrastruktur als Ports sichtbar machen
Checkpoint-Speicher, Dokumentziel und Fehlerjournal werden als schmale Ports extrahiert. Jeder Port entspricht einem konkreten Test- oder Betriebsbedarf.
package com.aydinsude.workbench.document.batch;
import java.util.Optional;
/** Ports-and-Adapters: Infrastruktur bleibt außerhalb des Ablaufkerns. */
public final class BatchPorts {
private BatchPorts() {}
public interface CheckpointStore {
Optional<BatchCheckpoint> load(String jobId);
void save(BatchCheckpoint checkpoint);
}
public interface DocumentSink {
void store(String idempotencyKey, BatchDocument document);
}
public interface FailureJournal {
void record(String jobId, BatchDocument document, RuntimeException failure);
}
}
Schritt 4 - Fachregel als Policy isolieren
Format- und Größenregeln werden aus der Schleife gelöst. Die Policy liefert eine begründete Entscheidung, damit Skip-Fälle im Betrieb nachvollziehbar bleiben.
package com.aydinsude.workbench.document.batch;
/** Policy Pattern: fachliche Annahmeregeln ändern unabhängig vom Batch-Ablauf. */
@FunctionalInterface
public interface DocumentValidationPolicy {
ValidationDecision evaluate(BatchDocument document);
record ValidationDecision(boolean accepted, String reason) {
public static ValidationDecision accept() { return new ValidationDecision(true, "accepted"); }
public static ValidationDecision reject(String reason) { return new ValidationDecision(false, reason); }
}
}
Schritt 5 - Checkpoint-Reihenfolge festlegen
Der Checkpoint wird erst nach einem bestätigten Sink-Write gespeichert. Daraus folgt bewusst eine At-least-once-Verarbeitung. Der Sink muss deshalb einen stabilen Idempotency Key akzeptieren.
package com.aydinsude.workbench.document.batch;
import com.aydinsude.workbench.document.batch.BatchPorts.CheckpointStore;
import com.aydinsude.workbench.document.batch.BatchPorts.DocumentSink;
import com.aydinsude.workbench.document.batch.BatchPorts.FailureJournal;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Application Service / Template Method im kleinen Sinn:
* Der stabile Ablauf koordiniert Policy und Ports, ohne Infrastrukturdetails zu kennen.
*/
public final class RefactoredDocumentBatchProcessor {
private final CheckpointStore checkpoints;
private final DocumentSink sink;
private final FailureJournal failures;
private final DocumentValidationPolicy validation;
public RefactoredDocumentBatchProcessor(
CheckpointStore checkpoints,
DocumentSink sink,
FailureJournal failures,
DocumentValidationPolicy validation) {
this.checkpoints = Objects.requireNonNull(checkpoints);
this.sink = Objects.requireNonNull(sink);
this.failures = Objects.requireNonNull(failures);
this.validation = Objects.requireNonNull(validation);
}
public BatchRunReport run(String jobId, List<BatchDocument> documents) {
int start = checkpoints.load(jobId).map(BatchCheckpoint::nextIndex).orElse(0);
List<BatchItemResult> results = new ArrayList<>();
for (int index = start; index < documents.size(); index++) {
BatchDocument document = documents.get(index);
BatchItemResult result = processOne(jobId, index, document);
results.add(result);
if (result.status() == BatchItemResult.Status.FAILED) break;
}
return new BatchRunReport(jobId, start, List.copyOf(results));
}
private BatchItemResult processOne(String jobId, int index, BatchDocument document) {
var decision = validation.evaluate(document);
if (!decision.accepted()) {
checkpoints.save(new BatchCheckpoint(jobId, index + 1));
return BatchItemResult.skipped(document.id(), decision.reason());
}
try {
String idempotencyKey = jobId + ":" + document.id();
sink.store(idempotencyKey, document);
// Checkpoint erst nach bestätigtem Sink-Write: at-least-once + idempotenter Sink.
checkpoints.save(new BatchCheckpoint(jobId, index + 1));
return BatchItemResult.processed(document.id());
} catch (RuntimeException failure) {
failures.record(jobId, document, failure);
return BatchItemResult.failed(document.id(), failure.getMessage());
}
}
public record BatchRunReport(String jobId, int resumedAt, List<BatchItemResult> results) {
public long processed() { return results.stream().filter(r -> r.status() == BatchItemResult.Status.PROCESSED).count(); }
public long skipped() { return results.stream().filter(r -> r.status() == BatchItemResult.Status.SKIPPED).count(); }
public long failed() { return results.stream().filter(r -> r.status() == BatchItemResult.Status.FAILED).count(); }
}
}
Schritt 6 - Fehlerpfad und Abbruchgrenze trennen
Ein technischer Fehler wird journalisiert und beendet den Lauf am letzten bestätigten Datensatz. Ein fachlich unzulässiges Dokument wird dagegen bewusst übersprungen und bestätigt. Diese Unterscheidung verhindert Endlosschleifen bei dauerhaft ungültigen Daten.
Schritt 7 - Wiederanlauf testen
5. Endzustandvar report = processor.run("nightly", documents);
assertEquals(1, report.resumedAt());
assertEquals(1, report.skipped());
assertEquals(1, report.failed());
assertEquals(2, checkpoints.value.nextIndex());Der Test beweist nicht nur Methodenergebnisse, sondern die Betriebszusage: Nach einem Fehler beginnt der nächste Lauf beim ersten nicht bestätigten Datensatz.
Schritt 8 - Transaktionsgrenze für Produktion bestimmen
Im Beispiel sind Sink und Checkpoint getrennte Ports. In Produktion muss entschieden werden, ob beide in derselben Datenbanktransaktion liegen, über Outbox/Inbox gekoppelt werden oder der Sink echte Idempotenz garantiert. Diese Entscheidung gehört in ein ADR und nicht in einen zufälligen Retry-Block.
Schritt 9 - Recovery und Dead-Letter-Behandlung trennen
Das Fehlerjournal ist kein versteckter Retry-Mechanismus. Es hält den technischen Abbruch reproduzierbar fest. Erst nach Klassifikation entscheidet eine Recovery Policy, ob ein Datensatz erneut versucht, fachlich korrigiert oder in eine Dead-Letter-Ablage überführt wird.
public record BatchRecoveryDecision(Action action, String reason) {
public enum Action { RETRY_FROM_CHECKPOINT, MANUAL_CORRECTION, DEAD_LETTER }
public static BatchRecoveryDecision classify(FailureType type) {
return switch (type) {
case TEMPORARY_SINK_FAILURE -> new BatchRecoveryDecision(
Action.RETRY_FROM_CHECKPOINT, "technisch wiederholbar");
case INVALID_BUSINESS_CONTENT -> new BatchRecoveryDecision(
Action.MANUAL_CORRECTION, "fachliche Korrektur erforderlich");
case UNSUPPORTED_PERMANENT_FORMAT -> new BatchRecoveryDecision(
Action.DEAD_LETTER, "dauerhaft nicht verarbeitbar");
};
}
}| Fehlerart | Behandlung | Checkpoint |
|---|---|---|
| temporärer Sink-Ausfall | nächster Lauf startet am letzten bestätigten Index | unverändert |
| fachlich korrigierbarer Inhalt | manuelle Korrektur und gezielte Wiedereinspeisung | bewusst nicht automatisch vorziehen |
| dauerhaft unbekanntes Format | Dead Letter mit Ursache und Originalpayload | nach dokumentierter Aussteuerung fortsetzen |
Schritt 10 - Bewusster Stopppunkt
Der Zielzustand führt keine verteilte Workflow-Engine und kein generisches Batch-Framework ein. Für den gezeigten Änderungsdruck reichen ein klarer Application Service, eine Policy und drei Ports. Weitere Abstraktion ist erst sinnvoll, wenn mehrere Batches denselben stabilen Ablauf tatsächlich teilen.
Reader
Liefert geordnete BatchDocument-Datensätze und stabile Positionen.
Policy
Trennt bewusstes Skippen von technischen Fehlern.
Sink
Akzeptiert einen stabilen Idempotenzschlüssel.
Checkpoint
Bestätigt ausschließlich fachlich wirksame oder bewusst ausgesteuerte Datensätze.
Recovery
Retry, Korrektur und Dead Letter sind explizite Betriebsentscheidungen.
| Vorher | Nachher |
|---|---|
| Typcodes, Map-Speicher, versteckte Fortschrittslogik | Fachtypen, explizite Ports, begründeter Checkpoint |
| Fehler werden geloggt und weitergelaufen | Skip und technischer Fehler getrennt |
| Neustartverhalten unklar | At-least-once und Idempotenz dokumentiert und getestet |
Zwischenstand – Idempotente Speicherung vor Checkpoint
Der kritische Unterschied zum Ausgangscode ist die Reihenfolge: Validierung, idempotente Speicherung und erst danach Checkpoint. Recovery und Dead Letter werden erst auf dieser stabilen Basis ergänzt.
package com.aydinsude.workbench.intermediate;
import java.util.List;
import java.util.Objects;
/** Zwischenstand: Speicherung erfolgt vor Checkpoint; Wiederholung bleibt idempotent. */
public final class DocumentBatchIntermediate {
private final DocumentReader reader;
private final DocumentValidator validator;
private final IdempotentDocumentSink sink;
private final CheckpointStore checkpoints;
private final FailureJournal failures;
public DocumentBatchIntermediate(DocumentReader reader,
DocumentValidator validator,
IdempotentDocumentSink sink,
CheckpointStore checkpoints,
FailureJournal failures) {
this.reader = Objects.requireNonNull(reader);
this.validator = Objects.requireNonNull(validator);
this.sink = Objects.requireNonNull(sink);
this.checkpoints = Objects.requireNonNull(checkpoints);
this.failures = Objects.requireNonNull(failures);
}
public void run(String jobId) {
long position = checkpoints.load(jobId);
for (Document document : reader.readAfter(position)) {
Validation validation = validator.validate(document);
if (!validation.valid()) {
failures.record(jobId, document.id(), validation.reason());
checkpoints.save(jobId, document.position());
continue;
}
sink.storeIfAbsent(jobId + ":" + document.id(), document);
checkpoints.save(jobId, document.position());
}
}
public interface DocumentReader { List<Document> readAfter(long position); }
public interface DocumentValidator { Validation validate(Document document); }
public interface IdempotentDocumentSink { void storeIfAbsent(String key, Document document); }
public interface CheckpointStore { long load(String jobId); void save(String jobId, long position); }
public interface FailureJournal { void record(String jobId, String documentId, String reason); }
public record Document(String id, long position, String payload) {}
public record Validation(boolean valid, String reason) {}
}
Diff: falsche Reihenfolge → restartfähiger Commit-Punkt
- checkpointStore.save(document.offset());
- documentRepository.insert(document);
+ if (!documentRepository.exists(document.id())) {
+ documentRepository.save(document);
+ }
+ checkpointStore.advanceTo(document.offset());Vom Diff zur sicheren nächsten Aktion – Dokumentenbatch
Nächster sicherer Schritt
Speicherung idempotent machen und den Checkpoint strikt erst nach fachlich wirksamer Verarbeitung bestätigen.
Passender Nachweis
Restart-Test: Abbruch nach Speicherung vor Checkpoint darf beim Neustart keine Dublette erzeugen und keinen Datensatz verlieren.
Bewusster Stopppunkt
Stoppen, wenn Restart, Skip, Retry, Dead Letter und manuelle Korrektur explizit entschieden und betrieblich geübt sind.
Praxis-Checkliste – Dokumentenbatch
Vor dem Einsatz im realen Projekt kurz gemeinsam abhaken. Die Liste ergänzt die Fallstudie und ersetzt weder Tests noch fachliche Freigaben.
- Vor dem ersten Eingriff: Reader-Reihenfolge, Nebenwirkungen, Checkpoint-Semantik und Fehlerpositionen erfassen.↗ Testbeleg öffnen
- Sicherheitsnetz: Restart-Tests an mehreren Abbruchstellen und Idempotenzfälle vorbereiten.↗ Sicherheitsnetz öffnen
- Kleiner erster Schritt: Speicherung idempotent machen und Checkpoint strikt nach bestätigter Wirkung setzen.↗ Zwischenstand öffnen
- Freigabenachweis: Abbruch zwischen Store und Checkpoint sowie Retry-, Skip- und Dead-Letter-Pfade testen.↗ Endzustand öffnen
- Stoppbedingung: Kein generisches Batch-Framework ohne mehrere nachweislich gleiche Ablaufvarianten.↗ Stopppunkt öffnen
Kapitel 16 Enterprise-Fallstudie: Legacy-SOAP-Integration13 Lernschritte
Ausgangssystem
9 sichtbare Codeblöcke; Vertragsbeispiele, Fehlerübersetzung, Retry und Reconciliation bauen aufeinander auf.
Ein großer Legacy-Client mischt WSDL-Typen, Mapping, Fehlercodes, Statusänderung und Audit in einer Klasse.
Sicherheitsnetz
Characterization Tests und gespeicherte XML-Verträge sichern das beobachtbare Verhalten, bevor Struktur verändert wird.
Entkopplung
Port, Adapter und Anti-Corruption Layer halten den Fremdvertrag aus Domäne und Anwendung heraus.
Betriebsfähige Migration
Fehlerübersetzung, begrenzter Retry, Idempotenz, Shadow-Betrieb und Reconciliation führen zum messbaren Umschaltpunkt.
So liest du diese Fallstudie
Diese Geschichte entkoppelt WSDL-Typen und Partnercodes, unterscheidet fachliche von technischen Fehlern und sichert die Migration über Idempotenz, Shadow-Betrieb und Reconciliation.
Die folgenden Abschnitte sind keine unabhängigen Beispiele. Sie entwickeln denselben Kundenrisiko-Aufruf Schritt für Schritt weiter. Nach jedem Eingriff bleibt das Verhalten durch Tests überprüfbar; erst wenn die lokale Struktur sauber ist, folgen Resilience und Migrationsmechanismen.
1. Legacy
LegacyCustomerRiskSoapClient
Ergebnis, Audit und Request-Mapping bleiben zunächst unverändert.
2. Sicherheitsnetz
Legacy-Client plus Characterization Tests
Verhalten wird messbar; der Produktionspfad bleibt bestehen.
3. Entkopplung
CustomerRiskPort, Adapter und ACL
WSDL-Details bleiben am Rand; die fachliche Entscheidung bleibt gleich.
4. Resilience
Decorator um den fachlichen Port
Retry und Idempotenz werden explizit, ohne den Adapter aufzublähen.
5. Migration
Primär-/Shadow-Port plus Reconciliation
Alt und Neu werden vergleichbar; der Primärpfad bleibt bis zum Nachweis führend.
Diese Fallstudie zeigt die Modernisierung eines großen SOAP-Clients unter realen Vertragszwängen. Der erste Abschnitt verändert die Architektur bewusst noch nicht. Er macht den Ausgangscode, die externen Zusagen und die bisher nur implizit bekannten Fehlerpfade reproduzierbar.
Schritt 0 – Ausgangslage und Veränderungsrisiko verstehen
Der Client baut generierte Requests, normalisiert Eingaben, ruft den SOAP-Port auf, übersetzt Statuscodes, ändert Kundenzustände und schreibt Auditdaten. Ein scheinbar lokales Refactoring kann daher den externen Vertrag, die fachliche Entscheidung oder die Reihenfolge der Seiteneffekte brechen.
| Vertragszwang | Risiko | Im Sicherheitsnetz fixierter Nachweis |
|---|---|---|
Statuscode 00 | Grenzwert 75 könnte unbemerkt verändert werden | Score 23 bleibt APPROVED, Score 75 bleibt MANUAL_REVIEW |
Statuscode B12 | fachliche Ablehnung könnte als technischer Fehler behandelt werden | REJECTED und nicht wiederholbar |
| Timeout | Retry-Fähigkeit und Status könnten verloren gehen | TECHNICAL_REVIEW, Code T01, retryable=true |
| Request-Mapping | Trimmen, Länderformat, Betrag und Zeit könnten driften | vollständig erfasster generierter Request |
Schritt 1 – Den vollständigen Legacy-Client lesen
Die folgende Klasse ist absichtlich kein Zielbild. Sie dient als realistische Ausgangsbasis für die nächsten Schritte und hält die Kopplung sichtbar, statt sie durch ein zu frühes Pattern zu verdecken.
package com.aydinsude.workbench.soap.legacy;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* Bewusst problematischer Ausgangscode der Fallstudie.
* Transport, Mapping, fachliche Entscheidung, Audit und Statusänderung sind vermischt.
*/
public final class LegacyCustomerRiskSoapClient {
private final GeneratedRiskSoapPort soapPort;
private final CustomerRecordStore recordStore;
private final AuditSink auditSink;
private final Clock clock;
public LegacyCustomerRiskSoapClient(GeneratedRiskSoapPort soapPort,
CustomerRecordStore recordStore,
AuditSink auditSink,
Clock clock) {
this.soapPort = Objects.requireNonNull(soapPort);
this.recordStore = Objects.requireNonNull(recordStore);
this.auditSink = Objects.requireNonNull(auditSink);
this.clock = Objects.requireNonNull(clock);
}
public Map<String, Object> checkRisk(String customerId, String country, BigDecimal amount) {
if (customerId == null || customerId.isBlank()) throw new IllegalArgumentException("customerId");
if (country == null || country.length() != 2) throw new IllegalArgumentException("country");
if (amount == null || amount.signum() <= 0) throw new IllegalArgumentException("amount");
RiskRequest request = new RiskRequest();
request.customerNumber = customerId.trim();
request.countryCode = country.toUpperCase();
request.amount = amount.toPlainString();
request.requestTime = Instant.now(clock).toString();
RiskResponse response;
try {
response = soapPort.checkRisk(request);
} catch (SoapTimeoutException ex) {
recordStore.updateStatus(customerId, "TECHNICAL_REVIEW");
auditSink.write(customerId, "SOAP_TIMEOUT", ex.getMessage());
return result("TECHNICAL_REVIEW", "T01", null, true);
} catch (RuntimeException ex) {
recordStore.updateStatus(customerId, "TECHNICAL_REVIEW");
auditSink.write(customerId, "SOAP_FAILURE", ex.getClass().getSimpleName());
return result("TECHNICAL_REVIEW", "T99", null, false);
}
if (response == null || response.statusCode == null) {
recordStore.updateStatus(customerId, "TECHNICAL_REVIEW");
auditSink.write(customerId, "INVALID_RESPONSE", "missing status");
return result("TECHNICAL_REVIEW", "T98", null, false);
}
if ("00".equals(response.statusCode)) {
BigDecimal score = new BigDecimal(response.score == null ? "0" : response.score);
String decision = score.compareTo(new BigDecimal("75")) >= 0 ? "MANUAL_REVIEW" : "APPROVED";
recordStore.updateStatus(customerId, decision);
auditSink.write(customerId, "RISK_CHECKED", "score=" + score);
return result(decision, "00", score, false);
}
if ("B12".equals(response.statusCode)) {
recordStore.updateStatus(customerId, "REJECTED");
auditSink.write(customerId, "BUSINESS_REJECTION", response.message);
return result("REJECTED", "B12", null, false);
}
recordStore.updateStatus(customerId, "TECHNICAL_REVIEW");
auditSink.write(customerId, "UNKNOWN_STATUS", response.statusCode);
return result("TECHNICAL_REVIEW", response.statusCode, null, false);
}
private Map<String, Object> result(String decision, String code, BigDecimal score, boolean retryable) {
Map<String, Object> result = new HashMap<>();
result.put("decision", decision);
result.put("externalCode", code);
result.put("score", score);
result.put("retryable", retryable);
return result;
}
}
Schritt 2 – Verhalten vor jeder Strukturänderung sichern
Die Tests beschreiben nicht, wie der Code intern arbeiten soll. Sie fixieren beobachtbares Verhalten an den Systemgrenzen: Ergebnis, Persistenzstatus, Audit und SOAP-Request. Damit können spätere Schritte klein bleiben und bei Abweichungen sofort gestoppt werden.
package com.aydinsude.workbench.soap.legacy;
import static org.junit.jupiter.api.Assertions.*;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
class LegacyCustomerRiskSoapClientCharacterizationTest {
private static final Clock CLOCK = Clock.fixed(Instant.parse("2026-07-11T12:00:00Z"), ZoneOffset.UTC);
@Test void preservesApprovalForLowScore() {
Fixture f = new Fixture(req -> RiskResponse.of("00", "23", "ok"));
Map<String,Object> result = f.client.checkRisk("C-100", "at", new BigDecimal("120.00"));
assertEquals("APPROVED", result.get("decision"));
assertEquals(List.of("C-100=APPROVED"), f.statuses);
assertEquals("C-100|RISK_CHECKED|score=23", f.audit.getFirst());
}
@Test void preservesManualReviewAtBoundary() {
Fixture f = new Fixture(req -> RiskResponse.of("00", "75", "ok"));
assertEquals("MANUAL_REVIEW", f.client.checkRisk("C-200", "DE", new BigDecimal("900")).get("decision"));
}
@Test void preservesBusinessRejectionWithoutRetry() {
Fixture f = new Fixture(req -> RiskResponse.of("B12", null, "blocked customer"));
Map<String,Object> result = f.client.checkRisk("C-300", "AT", new BigDecimal("42"));
assertEquals("REJECTED", result.get("decision"));
assertEquals(false, result.get("retryable"));
}
@Test void timeoutRemainsTechnicalAndRetryable() {
Fixture f = new Fixture(req -> { throw new SoapTimeoutException("2s"); });
Map<String,Object> result = f.client.checkRisk("C-400", "AT", new BigDecimal("42"));
assertEquals("TECHNICAL_REVIEW", result.get("decision"));
assertEquals(true, result.get("retryable"));
assertEquals("T01", result.get("externalCode"));
}
@Test void requestMappingIsCharacterized() {
List<RiskRequest> captured = new ArrayList<>();
Fixture f = new Fixture(req -> { captured.add(req); return RiskResponse.of("00", "1", "ok"); });
f.client.checkRisk(" C-500 ", "at", new BigDecimal("12.30"));
RiskRequest request = captured.getFirst();
assertEquals("C-500", request.customerNumber);
assertEquals("AT", request.countryCode);
assertEquals("12.30", request.amount);
assertEquals("2026-07-11T12:00:00Z", request.requestTime);
}
private static final class Fixture {
final List<String> statuses = new ArrayList<>();
final List<String> audit = new ArrayList<>();
final LegacyCustomerRiskSoapClient client;
Fixture(GeneratedRiskSoapPort port) {
client = new LegacyCustomerRiskSoapClient(port,
(id,status) -> statuses.add(id + "=" + status),
(id,event,detail) -> audit.add(id + "|" + event + "|" + detail), CLOCK);
}
}
}
Schritt 3 – Reale Vertragsbeispiele als Testdaten verwenden
Zwei gespeicherte XML-Antworten dokumentieren die reale Namensraum- und Feldstruktur. In den folgenden Runs dienen sie als Ausgangspunkt für Mapping- und Adaptertests. Dadurch bleibt die Entkopplung am echten Vertrag ausgerichtet.
5. Migration<risk:checkRiskResponse xmlns:risk="urn:partner:risk:v3"> <risk:statusCode>00</risk:statusCode> <risk:score>23</risk:score> <risk:message>ok</risk:message> </risk:checkRiskResponse>
Schritt 4 – Einen stabilen fachlichen Port einführen
Der Anwendungscode erhält mit CustomerRiskPort einen stabilen fachlichen Vertrag. Generierte SOAP-Typen bleiben vollständig außerhalb von Domäne und Application Service. Damit kann ein späterer Transportwechsel erfolgen, ohne die fachliche Sprache erneut umzubauen.
@FunctionalInterface
public interface CustomerRiskPort {
RiskAssessment assess(CustomerRiskQuery query);
}Schritt 5 – SOAP-Vertrag mit Adapter und ACL einkapseln
Der Adapter kapselt den generierten SOAP-Port. Request Mapper und Response Translator bilden gemeinsam den Anti-Corruption Layer: Trim-Regeln, Länderformat, externe Statuscodes, Null-Konventionen und Score-Grenzen gelangen nicht unkontrolliert in das interne Modell.
public final class SoapCustomerRiskAdapter implements CustomerRiskPort {
private final GeneratedRiskSoapPort soapPort;
private final SoapRiskRequestMapper requestMapper;
private final SoapRiskResponseTranslator responseTranslator;
@Override
public RiskAssessment assess(CustomerRiskQuery query) {
try {
return responseTranslator.translate(
soapPort.checkRisk(requestMapper.toSoap(query)));
} catch (SoapTimeoutException ex) {
return responseTranslator.technical(
new SoapRiskFailure.Timeout(ex.getMessage()));
} catch (RuntimeException ex) {
return responseTranslator.technical(
new SoapRiskFailure.Transport(ex.getClass().getSimpleName()));
}
}
}Schritt 6 – Technische und fachliche Fehler explizit übersetzen
Fachliche Ablehnung, Timeout, Transportfehler, ungültige Antwort und unbekannter Partnerstatus werden jetzt getrennt modelliert. Das ist entscheidend: Nur ein als technisch und wiederholbar klassifizierter Fehler darf im nächsten Run eine Retry-Entscheidung auslösen.
| Externe Situation | Internes Ergebnis | Retry |
|---|---|---|
00 mit Score | APPROVED oder MANUAL_REVIEW | nein |
B12 | REJECTED | nein |
| Timeout | TECHNICAL_REVIEW / T01 | ja |
| Transportfehler | TECHNICAL_REVIEW / T99 | noch nein |
| fehlender Status | TECHNICAL_REVIEW / T98 | nein |
| unbekannter Code | TECHNICAL_REVIEW / Originalcode | nein |
Schritt 7 – Status und Audit aus dem Transport lösen
Der SOAP-Adapter übersetzt nur den externen Vertrag. Lokale Statusänderung und Audit liegen jetzt im AssessCustomerRiskService. Dadurch bleibt der Adapter isoliert testbar und ein späterer Parallelbetrieb kann denselben fachlichen Port verwenden.
Schritt 8 – Die neue Grenze mit Vertrags- und Adaptertests beweisen
Die neuen Tests beweisen Request-Mapping, fachliche Ablehnung, Timeout-Klassifikation und die Eindämmung unbekannter Partnercodes. Die ursprünglichen Characterization Tests bleiben bestehen und sichern weiterhin den Legacy-Pfad.
5. Migrationvar result = adapter(request -> RiskResponse.of("X77", null, "partner extension"))
.assess(new CustomerRiskQuery("C-700", "AT", new BigDecimal("42")));
assertEquals(RiskDecision.TECHNICAL_REVIEW, result.decision());
assertEquals("X77", result.externalCode());
assertFalse(result.retryable());Schritt 9 – Retry als eng begrenzten Decorator ergänzen
Die Retry-Entscheidung liegt nicht im SOAP-Adapter. Ein Decorator wertet ausschließlich das bereits klassifizierte fachliche Ergebnis aus. Wiederholt wird nur ein explizit als retrybar markierter technischer Timeout. Fachliche Ablehnungen, unbekannte Partnercodes und ungültige Antworten werden niemals durch Wiederholung verschleiert.
public final class RetryingCustomerRiskPort implements CustomerRiskPort {
private final CustomerRiskPort delegate;
private final RetryPolicy policy;
@Override
public RiskAssessment assess(CustomerRiskQuery query) {
RiskAssessment result;
int attempts = 0;
do {
attempts++;
result = delegate.assess(query);
} while (policy.shouldRetry(result, attempts));
return result;
}
}Der Test beweist zwei Grenzen: Ein Timeout darf innerhalb des Limits erneut versucht werden; eine fachliche Ablehnung bleibt bei exakt einem Aufruf.
Schritt 10 – Wiederholte fachliche Wirkung verhindern
Retry und Idempotenz lösen unterschiedliche Probleme. Retry entscheidet, ob erneut aufgerufen werden darf. Idempotenz verhindert, dass dieselbe fachliche Anfrage mehrfach wirksam wird. Der Schlüssel wird aus normalisierter Kundennummer, Land und Betrag gebildet. In Produktion muss putIfAbsent atomar durch Datenbank-Constraint oder dedizierten Idempotenzspeicher umgesetzt werden.
public RiskAssessment assess(CustomerRiskQuery query) {
String key = key(query);
return store.find(key).orElseGet(() ->
store.putIfAbsent(key, delegate.assess(query)));
}Nicht gespeichert werden sollten unvollständige technische Zwischenzustände ohne klaren Wiederanlaufvertrag. Die konkrete Aufbewahrungsdauer hängt vom Partnervertrag und der fachlichen Wiederholungsfrist ab.
Schritt 11 – Alt- und Neupfad ohne Doppelwirkung vergleichen
Beim Shadow-Betrieb bleibt ein Pfad führend. Sein Ergebnis wird an den Aufrufer zurückgegeben und bestimmt Status sowie Audit. Der zweite Pfad dient ausschließlich dem Vergleich. Ein Ausfall des Schattenpfads darf den Primärpfad nicht beeinträchtigen.
public RiskAssessment assess(CustomerRiskQuery query) {
RiskAssessment primaryResult = primary.assess(query);
try {
sink.record(query.customerId(),
ReconciliationResult.compare(primaryResult, shadow.assess(query)));
} catch (RuntimeException shadowFailure) {
sink.record(query.customerId(), shadowFailure(primaryResult));
}
return primaryResult;
}Schritt 12 – Abweichungen fachlich erklären und Umschaltung messen
Der Vergleich bewertet nicht rohe SOAP-Nachrichten, sondern fachliche Ergebnisse: Entscheidung, externer Diagnosecode und Score. Ein produktiver Wechsel darf erst erfolgen, wenn die Abweichungen über repräsentative Last und relevante Sonderfälle erklärt sind.
| Kriterium | Beispiel für Freigabe | Abbruchsignal |
|---|---|---|
| fachliche Gleichheit | erklärte Abweichungsquote unter vereinbartem Grenzwert | unbekannte Abweichungen bei Ablehnung oder Freigabe |
| technische Stabilität | Timeout- und Fehlerquote innerhalb SLO | Retry-Sturm oder erhöhte Partnerlast |
| Idempotenz | keine doppelten wirksamen Aufrufe | mehrfache Statusänderung oder Audit-Duplikate |
| Betrieb | Dashboard, Alarmierung und Runbook verfügbar | Abweichungen ohne verantwortlichen Owner |
Schritt 13 – Kontrolliert umschalten und bewusst stoppen
- Legacy-Pfad bleibt führend, neuer Adapter läuft als Shadow.
- Abweichungen werden klassifiziert und fachlich entschieden.
- Der neue Port wird für einen begrenzten Anteil führend; Rückschaltung bleibt möglich.
- Nach stabiler Beobachtung wird der alte SOAP-Client aus dem Anwendungscode entfernt.
- Der externe SOAP-Vertrag selbst wird nicht ohne fachlichen Nutzen neu geschrieben.
Der sinnvolle Stopppunkt ist erreicht, wenn WSDL-Details hinter dem Adapter liegen, Fehler explizit übersetzt werden, Retry begrenzt ist, Idempotenz nachweisbar funktioniert und die Umschaltung durch Reconciliation abgesichert ist. Circuit Breaker, REST-Umbau oder zusätzliche Messaging-Schichten werden nur ergänzt, wenn Messwerte und Betriebsrisiken sie rechtfertigen.
Vorher
LegacyCustomerRiskSoapClient besitzt Transport, Mapping, Fehlerlogik und Seiteneffekte.
Nach lokaler Entkopplung
CustomerRiskPort, SoapCustomerRiskAdapter, Mapper und Translator besitzen klar getrennte Aufgaben.
Nach Betriebsabsicherung
Retry-, Idempotenz- und Shadow-Decorator umgeben denselben fachlichen Port, ohne den Adapter aufzublähen.
Endzustand
Der Anwendungscode bleibt transportneutral; die Migration ist messbar und rückschaltbar.
Vollständige Quellen: modules/soap-integration-case-study.
Zwischenstand – fachlicher Port und explizite Vertragsübersetzung
Der Anwendungscode kennt bereits keine generierten SOAP-Typen mehr. Retry, Idempotenz und Shadow-Betrieb fehlen noch bewusst; zuerst muss die Übersetzung des externen Vertrags stabil sein.
package com.aydinsude.workbench.intermediate;
import java.util.Objects;
/** Zwischenstand: Der Port ist stabil, der Adapter übersetzt Vertrag und Fehler explizit. */
public final class SoapIntermediate {
private final GeneratedRiskSoapPort soapPort;
public SoapIntermediate(GeneratedRiskSoapPort soapPort) {
this.soapPort = Objects.requireNonNull(soapPort);
}
public RiskAssessment assess(RiskQuery query) {
SoapRiskRequest request = new SoapRiskRequest(
query.customerId().trim(),
query.countryCode().toUpperCase(),
query.amountInCents());
try {
SoapRiskResponse response = soapPort.assess(request);
return switch (response.statusCode()) {
case "00" -> response.score() < 75
? RiskAssessment.approved(response.score())
: RiskAssessment.manual(response.score(), "LIMIT");
case "B12" -> RiskAssessment.rejected(response.score(), "PARTNER_REJECTED");
default -> RiskAssessment.technical("UNKNOWN_PARTNER_CODE", false);
};
} catch (SoapTimeoutException ex) {
return RiskAssessment.technical("TIMEOUT", true);
}
}
public interface GeneratedRiskSoapPort { SoapRiskResponse assess(SoapRiskRequest request); }
public record RiskQuery(String customerId, String countryCode, long amountInCents) {}
public record SoapRiskRequest(String customerId, String country, long amountInCents) {}
public record SoapRiskResponse(String statusCode, int score) {}
public record RiskAssessment(String decision, int score, String reason, boolean retryable) {
static RiskAssessment approved(int score) { return new RiskAssessment("APPROVED", score, "OK", false); }
static RiskAssessment manual(int score, String reason) { return new RiskAssessment("MANUAL", score, reason, false); }
static RiskAssessment rejected(int score, String reason) { return new RiskAssessment("REJECTED", score, reason, false); }
static RiskAssessment technical(String reason, boolean retryable) { return new RiskAssessment("TECHNICAL", -1, reason, retryable); }
}
public static final class SoapTimeoutException extends RuntimeException {}
}
Diff: generierte SOAP-Typen → stabiler fachlicher Port
- RiskResponse response = generatedSoapClient.checkRisk(request);
- if ("00".equals(response.getStatus())) return APPROVED;
- throw new RuntimeException(response.getCode());
+ RiskAssessment assessment = customerRiskPort.assess(customerReference);
+ return switch (assessment.decision()) {
+ case APPROVED -> approve(assessment);
+ case MANUAL_REVIEW -> review(assessment);
+ case REJECTED -> reject(assessment);
+ };Vom Diff zur sicheren nächsten Aktion – SOAP-Integration
Nächster sicherer Schritt
Den fachlichen Port stabilisieren und nur Transport, Mapping und Fehlerübersetzung im Adapter belassen; Retry noch nicht pauschal aktivieren.
Passender Nachweis
Adapter- und Vertrags-Test: gespeicherte XML-Antworten werden korrekt übersetzt; fachliche Fehler werden nie als technische Retry-Fälle behandelt.
Bewusster Stopppunkt
Stoppen, wenn WSDL-Details gekapselt, Fehler explizit, Idempotenz nachgewiesen und Umschaltung rückschaltbar ist; kein REST-Rewrite ohne fachlichen Gewinn.
Praxis-Checkliste – SOAP-Integration
Vor dem Einsatz im realen Projekt kurz gemeinsam abhaken. Die Liste ergänzt die Fallstudie und ersetzt weder Tests noch fachliche Freigaben.
- Vor dem ersten Eingriff: WSDL-Version, Statuscodes, XML-Beispiele, Timeouts und Partnerzusagen inventarisieren.↗ Testbeleg öffnen
- Sicherheitsnetz: Request-Capture, gespeicherte Responses und Fehlerklassifikation reproduzierbar testen.↗ Sicherheitsnetz öffnen
- Kleiner erster Schritt: Fachlichen Port einführen und generierte Typen ausschließlich im Adapter belassen.↗ Zwischenstand öffnen
- Freigabenachweis: Retry nur für technische Fehler, Idempotenz und Shadow-Reconciliation nachweisen.↗ Endzustand öffnen
- Stoppbedingung: Kein REST-Rewrite, solange der externe Vertrag stabil gekapselt und betreibbar ist.↗ Stopppunkt öffnen
Legacy- und Systemmodernisierung
Datenbedeutung, Verträge, Parallelbetrieb und Abschaltung werden als kontrollierter Migrationspfad behandelt.
Kapitel 17 Datenmodernisierung: vom widersprüchlichen Legacy-Datensatz zum belegbaren FachmodellModernisierung
9 sichtbare Codeblöcke; Profiling, Mapping, Quarantäne und Reconciliation sind getrennt nachvollziehbar.
Hier lernst du nicht primär Service-Architektur, sondern wie unbekannte Codes, Nullwerte und Dubletten zuerst gemessen, dann in Fachtypen übersetzt und schließlich über Quarantäne und Reconciliation nachgewiesen werden.
Dieses Kapitel begleitet dieselben Kundendaten durch einen vollständigen Modernisierungspfad. Der Leser sieht nicht nur das Zielbild, sondern jeden fachlich begründeten Schritt: Profiling, explizite Bedeutung, Quarantäne, idempotente Übertragung und Reconciliation.
Ausgangslage
Eine 25 Jahre gewachsene Kundentabelle enthält mehrdeutige Codes, Dubletten, unterschiedliche Dezimalformate und historisch überschriebenes Wissen.
Ziel
Nicht bloß Datensätze kopieren, sondern fachliche Bedeutung explizit machen, Abweichungen sichtbar halten und jeden Zielwert erklären können.
Sicherheitsregel
Kein unbekannter Wert wird still auf einen Standardwert gemappt. Ungeklärte Fälle gehen in eine fachlich verantwortete Quarantäne.
Die Quelltabelle erzählt mehrere widersprüchliche Geschichten
Bevor eine Zielarchitektur entworfen wird, betrachten wir einen echten Export. Schon vier Zeilen zeigen, warum ein technisches INSERT INTO ... SELECT die fachliche Wahrheit beschädigen würde.
-- Historischer Export aus CUSTOMER_MASTER
CUSTOMER_ID | EMAIL | STATUS | VIP_FLAG | CREDIT_LIMIT | UPDATED_AT
C-1001 | anna@example.org | A | Y | 005000,00 | 2024-01-12
C-1002 | NULL | 1 | N | 2500.00 | 2024-02-03
C-1002 | support@example.org | A | N | 2500.00 | 2024-02-04
C-1003 | " " | X | ? | -100 | 31.12.1999Mehrdeutiger Status
A und 1 scheinen dasselbe zu bedeuten. X ist ungeklärt.
Doppelter Schlüssel
C-1002 existiert zweimal mit unterschiedlichen E-Mail-Werten.
Formatdrift
Geld und Datum folgen mehreren historischen Formaten.
Profiling ersetzt Annahmen durch messbare Evidenz
Wir zählen nicht nur Zeilen. Wir erfassen Nullquoten, Statusverteilungen, Dubletten und unbekannte Codes. Das Profil wird als fachliches Prüfergebnis modelliert, damit die Freigabe automatisierbar und reproduzierbar bleibt.
public record LegacyCustomerProfile(
long rowCount,
long duplicateBusinessKeys,
long missingEmails,
Map<String, Long> statusDistribution,
Map<String, Long> vipFlagDistribution) {
public boolean requiresManualReview() {
return duplicateBusinessKeys > 0
|| missingEmails > 0
|| statusDistribution.containsKey("X")
|| vipFlagDistribution.containsKey("?");
}
}Das Profil beantwortet eine andere Frage als ein Schema-Validator: Welche Realität liegt tatsächlich vor? Erst danach darf entschieden werden, welche Werte gültig, historisch toleriert oder fachlich zu klären sind.
Aus Codes werden fachliche Typen – unbekannte Werte bleiben sichtbar
Der alte Status wird nicht direkt in ein neues Datenbankfeld kopiert. Ein fachlicher Typ definiert die erlaubten Zustände und dokumentiert bewusst, welche historischen Codes gleichbedeutend sind.
public enum CustomerStatus {
ACTIVE,
INACTIVE,
BLOCKED;
public static CustomerStatus fromLegacy(String raw) {
return switch (raw == null ? "" : raw.trim().toUpperCase()) {
case "A", "1", "ACTIVE" -> ACTIVE;
case "I", "0", "INACTIVE" -> INACTIVE;
case "B", "BLOCKED" -> BLOCKED;
default -> throw new IllegalArgumentException(
"Unbekannter Legacy-Status: " + raw);
};
}
}public record EmailAddress(String value) {
public EmailAddress {
String normalized = value == null ? "" : value.trim().toLowerCase();
if (!normalized.matches("^[^@\s]+@[^@\s]+\.[^@\s]+$")) {
throw new IllegalArgumentException("Ungültige E-Mail-Adresse");
}
value = normalized;
}
}X automatisch als INACTIVE zu speichern wäre technisch bequem, aber fachlich unbelegt. Deshalb wird die Zeile abgewiesen und mit einem Diagnosecode dokumentiert.Das Zielmodell beschreibt Fachlichkeit, nicht die neue Tabelle
Das kanonische Modell ist unabhängig von CSV, JDBC, CDC oder dem späteren Ziel-Store. Es trägt nur bestätigte Bedeutung und Invarianten.
public record CanonicalCustomer(
CustomerId id,
EmailAddress email,
CustomerStatus status,
boolean vip,
Money creditLimit,
Instant sourceUpdatedAt) {
public CanonicalCustomer {
Objects.requireNonNull(id);
Objects.requireNonNull(email);
Objects.requireNonNull(status);
Objects.requireNonNull(creditLimit);
Objects.requireNonNull(sourceUpdatedAt);
}
}Lose Strings, implizite Codes, negative Beträge und Nullwerte.
CustomerId, EmailAddress, CustomerStatus und Money schützen die Domäne.
Der Canonicalizer liefert entweder einen gültigen Kunden oder erklärbare Fehler
Mapping und Validierung werden nicht getrennt in mehreren Batchschritten versteckt. Eine Übersetzung liefert ein Ergebnisobjekt, das akzeptierte und abgewiesene Zeilen gleichwertig repräsentiert.
public final class CustomerCanonicalizer {
public MigrationResult canonicalize(LegacyCustomerRow row) {
List<DataQualityIssue> issues = new ArrayList<>();
CustomerStatus status = mapStatus(row.status(), issues);
EmailAddress email = mapEmail(row.email(), issues);
boolean vip = mapVip(row.vipFlag(), issues);
Money limit = mapCreditLimit(row.creditLimit(), issues);
if (!issues.isEmpty()) {
return MigrationResult.rejected(row.customerId(), issues);
}
return MigrationResult.accepted(new CanonicalCustomer(
new CustomerId(row.customerId()), email, status, vip,
limit, row.updatedAt()));
}
}Damit lässt sich jede Ablehnung fachlich auswerten: unbekannter Status, ungültige E-Mail, negativer Kreditrahmen oder ungeklärtes VIP-Flag. Es gibt keinen stillen Datenverlust.
Idempotente Upserts, Quarantäne und Checkpoint bilden einen restartfähigen Pfad
Der Batch verarbeitet jede Zeile deterministisch. Gültige Kunden werden idempotent geschrieben; ungeklärte Fälle landen in einer Quarantäne. Der Checkpoint wird erst nach einem dieser beiden erklärbaren Ergebnisse gesetzt.
public MigrationSummary migrate(List<LegacyCustomerRow> sourceRows) {
MigrationSummary summary = new MigrationSummary();
sourceRows.stream()
.sorted(comparing(LegacyCustomerRow::updatedAt))
.map(canonicalizer::canonicalize)
.forEach(result -> {
if (result.accepted()) {
targetStore.upsert(result.customer()); // idempotent
summary.recordAccepted(result.customer().id());
} else {
quarantine.store(result); // erklärbar, nicht still verworfen
summary.recordRejected(result.customerId());
}
checkpoint.save(result.customerId());
});
return summary;
}Wiederholbarkeit
Ein Neustart erzeugt keine Dubletten.
Erklärbarkeit
Jede abgewiesene Zeile besitzt Diagnosecodes und Originaldaten.
Fortsetzbarkeit
Checkpoint erst nach Zielschreibvorgang oder Quarantäne.
Reconciliation vergleicht Bedeutung statt bloßer Zeilenzahl
Gleiche Counts beweisen keine Gleichheit. Der Abgleich prüft pro Kunde Statusbedeutung, Kreditrahmen, VIP-Regel und das Vorhandensein im Zielsystem.
public ReconciliationReport compare(
SourceSnapshot source,
TargetSnapshot target) {
List<ReconciliationDifference> differences = new ArrayList<>();
for (CustomerId id : source.customerIds()) {
SourceCustomer left = source.customer(id);
CanonicalCustomer right = target.customer(id).orElse(null);
if (right == null) {
differences.add(missingTarget(id));
continue;
}
compareStatus(left, right, differences);
compareCreditLimit(left, right, differences);
compareVipMeaning(left, right, differences);
}
return ReconciliationReport.from(differences);
}Abweichungen werden klassifiziert: fehlender Zielsatz, fachlich unterschiedlicher Status, Rundungsdifferenz oder ungeklärte Legacy-Bedeutung. Dadurch kann ein Fachbereich gezielt entscheiden, ob migriert, korrigiert oder gestoppt wird.
Die wichtigsten Risiken werden ausführbar dokumentiert
Die Tests beweisen zwei zentrale Eigenschaften: unbekannte Werte verschwinden nicht und Wiederholungen bleiben idempotent.
@Test
void unknown_status_is_not_silently_mapped() {
LegacyCustomerRow row = fixture.customer()
.withStatus("X")
.build();
MigrationResult result = canonicalizer.canonicalize(row);
assertThat(result.accepted()).isFalse();
assertThat(result.issues())
.extracting(DataQualityIssue::code)
.containsExactly("UNKNOWN_CUSTOMER_STATUS");
}
@Test
void repeated_migration_keeps_one_target_record() {
pipeline.migrate(List.of(fixture.validCustomer()));
pipeline.migrate(List.of(fixture.validCustomer()));
assertThat(targetStore.countById(new CustomerId("C-1001"))).isEqualTo(1);
}Wann ist die Datenmodernisierung ausreichend?
- Jeder Zielwert ist auf eine bestätigte Mapping-Regel zurückführbar.
- Unbekannte Werte werden nicht versteckt, sondern messbar quarantänisiert.
- Die Übertragung kann ohne Dubletten wiederholt und nach Ausfall fortgesetzt werden.
- Reconciliation vergleicht fachliche Bedeutung, nicht nur technische Counts.
- Erst nach stabiler Delta-Quote und geklärter Verantwortung wird das Altsystem schreibgeschützt.
Hier endet das Refactoring bewusst. Eine Event-Plattform, ein Data Lake oder Microservices sind keine automatische Fortsetzung. Sie werden nur eingeführt, wenn ein zusätzlicher fachlicher oder betrieblicher Nutzen belegt ist.
Zwischenstand – Normalisieren und Quarantäne vor Persistenz
Dieser Stand ist groß genug, um die erste echte Verantwortungsgrenze zu zeigen: Rohdaten werden normalisiert, unbekannte Codes werden nicht geraten, und erst danach darf eine Persistenzpipeline folgen.
package com.aydinsude.workbench.intermediate;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/** Zwischenstand: Rohdaten werden bereits fachlich normalisiert, aber noch nicht gespeichert. */
public final class DataModernizationIntermediate {
private static final Map<String, CustomerStatus> STATUS = Map.of(
"A", CustomerStatus.ACTIVE,
"I", CustomerStatus.INACTIVE,
"B", CustomerStatus.BLOCKED);
public CanonicalizationResult canonicalize(LegacyCustomerRow row) {
List<String> reasons = new ArrayList<>();
String normalizedName = row.name() == null ? "" : row.name().trim();
CustomerStatus status = STATUS.get(row.statusCode());
if (normalizedName.isBlank()) {
reasons.add("NAME_MISSING");
}
if (status == null) {
reasons.add("UNKNOWN_STATUS:" + row.statusCode());
}
if (!reasons.isEmpty()) {
return CanonicalizationResult.quarantined(row.id(), reasons);
}
CanonicalCustomer customer = new CanonicalCustomer(
row.id().trim().toUpperCase(Locale.ROOT),
normalizedName,
status);
return CanonicalizationResult.accepted(customer);
}
public enum CustomerStatus { ACTIVE, INACTIVE, BLOCKED }
public record LegacyCustomerRow(String id, String name, String statusCode) {}
public record CanonicalCustomer(String id, String name, CustomerStatus status) {}
public record CanonicalizationResult(CanonicalCustomer customer, String legacyId,
List<String> quarantineReasons) {
static CanonicalizationResult accepted(CanonicalCustomer customer) {
return new CanonicalizationResult(customer, customer.id(), List.of());
}
static CanonicalizationResult quarantined(String legacyId, List<String> reasons) {
return new CanonicalizationResult(null, legacyId, List.copyOf(reasons));
}
public boolean accepted() { return customer != null; }
}
}
Diff: Rohdaten → fachlich normalisierte Daten
- String status = row.status();
- repository.save(row);
+ NormalizedCustomer normalized = normalizer.normalize(row);
+ if (normalized.hasUnknownStatus()) {
+ quarantine.store(row, normalized.diagnostics());
+ return MigrationResult.quarantined(row.id());
+ }
+ repository.save(normalized.customer());Vom Diff zur sicheren nächsten Aktion – Datenmodernisierung
Nächster sicherer Schritt
Zuerst unbekannte Statuswerte und Dubletten quarantänisieren, bevor der erste produktive Zieldatensatz geschrieben wird.
Passender Nachweis
Mapper- und Reconciliation-Test: dieselbe Quelldatei muss reproduzierbar dieselben Fachwerte und dieselben Quarantänegründe erzeugen.
Bewusster Stopppunkt
Stoppen, wenn alle Zielwerte erklärbar, Wiederholung idempotent und Restabweichungen fachlich verantwortet sind.
Praxis-Checkliste – Datenmodernisierung
Vor dem Einsatz im realen Projekt kurz gemeinsam abhaken. Die Liste ergänzt die Fallstudie und ersetzt weder Tests noch fachliche Freigaben.
- Vor dem ersten Eingriff: Repräsentative Rohdatensätze, Nullwerte, unbekannte Codes, Dubletten und Grenzfälle sichern.↗ Testbeleg öffnen
- Sicherheitsnetz: Profiling-Kennzahlen und Golden-Master-Mappings für bekannte sowie unbekannte Werte festhalten.↗ Sicherheitsnetz öffnen
- Kleiner erster Schritt: Nur eine Mehrdeutigkeit gleichzeitig in einen Fachtyp oder eine explizite Mapping-Regel überführen.↗ Zwischenstand öffnen
- Freigabenachweis: Quarantänequote, Reconciliation und wiederholbarer Lauf ohne zusätzliche Zieländerung prüfen.↗ Endzustand öffnen
- Stoppbedingung: Keine unbelegte Geschäftsregel ergänzen; bei unklarer Bedeutung in Quarantäne statt raten.↗ Stopppunkt öffnen
Kapitel 18 Schnittstellenmigration: Vertrag, Adapter und Routing getrennt steuernModernisierung
Die neue Schnittstelle wird nicht gleichzeitig mit allen Aufrufern ausgerollt. Ein Anti-Corruption Layer übersetzt das alte Protokoll in ein stabiles kanonisches Kommando. Eine Routing-Fassade entscheidet pro Consumer, Tenant oder Use Case, ob Alt- oder Neupfad verwendet wird. Consumer-Driven Contract Tests sichern dabei nicht nur JSON- oder XML-Strukturen, sondern auch Fehlercodes, Idempotenz und Zeitverhalten.
- Expand: neue Felder und Endpunkte rückwärtskompatibel bereitstellen.
- Migrate: Consumer kontrolliert umstellen und Telemetrie nach Consumer-ID auswerten.
- Contract: alte Felder erst entfernen, wenn Nutzung und Rückfallweg nachweislich entfallen.
// Design Pattern: Strangler Facade + Strategy
public Result route(Request request, MigrationPolicy policy) {
return policy.useModernPath(request.consumerId())
? modern.execute(mapper.toCanonical(request))
: legacy.execute(request);
}Kapitel 19 Parallelbetrieb: Shadow, Dual Read und kontrollierter Dual WriteModernisierung
Parallelbetrieb dient der Beweisführung und darf kein Dauerzustand werden. Shadow Traffic eignet sich für lesende oder nebenwirkungsfreie Pfade. Dual Read vergleicht Ergebnisse, ohne den führenden Datenbestand zu verändern. Dual Write ist nur mit klarer Führungsrolle, idempotenten Operationen, Outbox oder CDC und einer Reconciliation-Schleife vertretbar.
| Modus | Nutzen | Hauptrisiko |
|---|---|---|
| Shadow | Neupfad unter Realverkehr beobachten | verdeckte Nebenwirkungen |
| Dual Read | fachliche Ergebnisse vergleichen | unterschiedliche Zeitstände |
| Dual Write | beide Datenwelten aktuell halten | partielle Fehler und Reihenfolge |
Jede Abweichung erhält eine fachliche Klasse: tolerierte Rundungsdifferenz, erwartete Zeitverschiebung, bekannter Altfehler oder echter Regressionsverdacht. Nur dadurch wird aus einem bloßen Differenzzähler eine belastbare Freigabeentscheidung.
Kapitel 20 Reconciliation, Cutover und AbschaltkriterienModernisierung
Das Altsystem wird nicht abgeschaltet, weil der neue Dienst produktiv ist, sondern weil definierte Gates über einen vereinbarten Beobachtungszeitraum erfüllt sind. Die Entscheidung ist gemeinsam fachlich, technisch und betrieblich zu verantworten.
| Gate | Beispielkriterium | Nachweis |
|---|---|---|
| Daten | 100 % der erwarteten Schlüssel vorhanden; alle Deltas geklärt | Reconciliation-Bericht und signierte Ausnahme-Liste |
| Fachlichkeit | keine unerklärte Abweichung bei Entscheidungen und Salden | Golden-Master-Vergleich, Fachfreigabe |
| Betrieb | SLO, Fehlerbudget und Kapazität über mindestens zwei Spitzenperioden stabil | Dashboards, Lasttests, Incident-Historie |
| Consumer | keine produktive Nutzung des Altvertrags | Gateway-Telemetrie, Owner-Bestätigung |
| Rollback | Rückfallweg getestet oder bewusst geschlossen | Game Day und Entscheidungsprotokoll |
Betrieb und Verantwortung
Produktionsreife entsteht durch Nachweis, Ownership, Rückfallfähigkeit und explizite Entscheidungsrechte.
Kapitel 21 Betriebsübernahme: Verantwortung beweisbar übergebenBetrieb
Eine Modernisierung ist nicht abgeschlossen, wenn der neue Dienst technisch produktiv ist. Abgeschlossen ist sie erst, wenn Betrieb, Support und Fachverantwortung den Dienst unter realistischen Bedingungen selbstständig führen können. Die Übergabe umfasst deshalb nicht nur Dokumente, sondern nachgewiesene Handlungsfähigkeit: Alarm verstehen, Fall eingrenzen, sicheren Rückfallweg wählen, fachliche Auswirkungen bewerten und den richtigen Entscheider erreichen.
| Abnahmefeld | Verbindlicher Nachweis | Freigabeverantwortung |
|---|---|---|
| Service Ownership | benannter Service Owner, Stellvertretung und Eskalationsweg | Produkt- und Betriebsverantwortung |
| Runbook | Diagnose, Sofortmaßnahme, Rückfallweg und Kommunikationsvorlage | Operations/SRE |
| Supportfähigkeit | repräsentative Störung ohne Projektteam bearbeitet | Support Lead |
| Fachliche Verantwortung | Regel-, Daten- und Ausnahmeentscheidungen eindeutig zugeordnet | Fachbereich/Data Owner |
| Restrisiken | offene Risiken mit Frist, Owner und akzeptierter Auswirkung | gemeinsames Go-live-Gremium |
Kapitel 22 Observability: technische Signale mit fachlichen Folgen verbindenBetrieb
Logs, Metriken und Traces sind nur dann betriebswirksam, wenn sie eine fachliche Frage beantworten. Für den modernisierten Schadenprozess werden deshalb technische Telemetrie, fachliche Ereignisse und Betriebsziele über eine gemeinsame Correlation-ID verbunden. Ein HTTP-Fehler ist lediglich ein Symptom; entscheidend ist, welche Fälle blockiert, doppelt verarbeitet oder fachlich abweichend entschieden wurden.
| Ebene | Beispielsignal | Entscheidungsnutzen |
|---|---|---|
| Technik | Fehlerquote, Latenz, Queue-Alter, Thread- oder Connection-Pool | Engpass und Fehlerdomäne eingrenzen |
| Fachlichkeit | Anteil abweichender Entscheidungen, offene Reconciliation-Deltas, manuelle Nacharbeit | Geschäftsauswirkung bewerten |
| Betrieb | Time to Detect, Time to Mitigate, Alarmqualität, Fehlerbudget | Betriebsreife und Änderungsrisiko steuern |
| Daten | fehlende Schlüssel, verspätete CDC-Ereignisse, Schemaabweichung | Datenintegrität und Cutover-Sicherheit prüfen |
Kapitel 23 Rollback: Code, Daten, Prozess und Fachentscheidung getrennt behandelnBetrieb
Rollback ist kein einzelner Knopf. Deployment, Datenzustand, Prozessrouting und bereits ausgelöste fachliche Wirkungen besitzen unterschiedliche Umkehrbarkeit. Ein altes Container-Image kann schnell aktiviert werden, aber bereits versendete Schreiben, Zahlungen oder Statusmeldungen bleiben externe Tatsachen. Die Rückfallstrategie legt deshalb vor dem Release fest, welche Ebene rücksetzbar, kompensierbar oder nur durch einen Forward Fix korrigierbar ist.
| Ebene | Bevorzugte Strategie | Nicht ausreichend, wenn |
|---|---|---|
| Code | Blue/Green, Canary-Abbruch oder vorheriges Artefakt | Datenformat oder externe Wirkung bereits verändert wurde |
| Konfiguration | versionierte Konfiguration und geprüfter Safe Default | Alt- und Neuversion unterschiedliche Semantik erwarten |
| Daten | Expand/Contract, Restore-Punkt, Reconciliation und gezielte Reparatur | nachfolgende legitime Änderungen überschrieben würden |
| Prozess | Routing auf stabilen Pfad oder Feature Toggle | Vorgänge in beiden Welten teilweise fortgeschritten sind |
| Fachliche Wirkung | Kompensation, Nachbearbeitung und transparente Kommunikation | eine irreversible externe Entscheidung erfolgte |
Kapitel 24 Game Days: Rückfallfähigkeit unter kontrolliertem Stress nachweisenBetrieb
Game Days machen Annahmen über Betrieb und Rückfallfähigkeit überprüfbar. Die Übung simuliert keine abstrakte Katastrophe, sondern einen realen kritischen Geschäftspfad mit definiertem Startzustand, erwarteten Signalen, Entscheidungszeitpunkt und Abbruchgrenze. Beobachtet wird nicht nur, ob Technik reagiert, sondern ob Verantwortlichkeiten, Kommunikation und fachliche Priorisierung funktionieren.
| Szenario | Erwartete Reaktion | Erfolgskriterium |
|---|---|---|
| Neuer Dienst nicht erreichbar | Traffic stoppen, Altpfad bewerten, Warteschlange sichern | keine verlorenen Vorgänge; Entscheidung innerhalb des RTO |
| Reconciliation driftet | betroffene Kohorte isolieren, Ursache klassifizieren, Cutover stoppen | jede Abweichung besitzt Klasse und Owner |
| Events doppelt oder verspätet | Idempotenz prüfen, Replay kontrollieren, Business-KPI beobachten | keine doppelte fachliche Wirkung |
| Rollback scheitert | Change Freeze, Incident Command, Forward Fix oder Kompensation | klare Führungsrolle und dokumentierte Entscheidung |
| Owner nicht erreichbar | Stellvertretung und Eskalationskette aktivieren | Entscheidung ohne Wissensmonopol möglich |
Kapitel 25 Verantwortungsübergabe: Entscheidungen statt nur Aufgaben zuordnenGovernance
Eine RACI-Tabelle ist hilfreich, reicht aber allein nicht aus. Für jede kritische Entscheidung wird festgelegt, wer Evidenz liefert, wer entscheidet, wer ausführt und wer informiert wird. Besonders wichtig sind Grenzfälle: Abschaltung des Altsystems, Akzeptanz einer Datenabweichung, Aktivierung des Break-Glass-Zugangs und Freigabe einer fachlichen Kompensation.
| Entscheidung | Evidenzlieferant | Entscheider | Ausführung |
|---|---|---|---|
| Produktionsfreigabe | Engineering, QA, SRE, Fachbereich | Service Owner | Release-Verantwortung |
| Akzeptanz fachlicher Abweichung | Reconciliation-Team und Data Owner | Fachverantwortung | Daten-/Anwendungsteam |
| Rollback oder Forward Fix | Incident-Analyse und Operations | Incident Lead | SRE/Engineering |
| Abschaltung des Legacy-Pfads | Consumer-, Daten- und SLO-Nachweise | Produkt- und Systemverantwortung | Operations |
| Notfallzugriff | Security- und Betriebsnachweis | benannter Break-Glass-Approver | privilegierter Operator |
Kapitel 26 Governance: Entscheidungsrechte und Leitplanken explizit machenGovernance
Governance schützt eine Modernisierung vor zwei gegensätzlichen Fehlern: ungeprüfter Zentralisierung und unkontrollierter Teamautonomie. Verbindlich geregelt werden daher nicht einzelne Implementierungsdetails, sondern Entscheidungsrechte, nicht verhandelbare Leitplanken und der Nachweis, mit dem eine Ausnahme akzeptiert werden darf. Architektur, Betrieb, Fachlichkeit, Daten und Security erhalten klar abgegrenzte Zuständigkeiten.
| Governance-Ebene | Verbindliche Regel | Nachweis |
|---|---|---|
| Architektur | Domänengrenzen und Integrationsprinzipien sind verbindlich | Architecture Fitness Functions und Review-Protokoll |
| Daten | Owner, Qualitätsziel und Aufbewahrung sind benannt | Data Contract, Qualitätsbericht, Löschkonzept |
| Betrieb | SLO, Alarmreaktion und Rückfallweg sind freigegeben | Dashboard, Runbook und Game-Day-Evidenz |
| Security | Privilegien, Secrets und Auditierung folgen dem Least-Privilege-Prinzip | automatisierte Kontrollen und Freigabe |
| Ausnahmen | Abweichung besitzt Frist, Risiko, Owner und Rückbauplan | zeitlich begrenzte Waiver-Entscheidung |
Kapitel 27 Architekturentscheidungen: ADRs als lebende EntscheidungsakteGovernance
Ein Architecture Decision Record dokumentiert nicht nur das gewählte Ergebnis, sondern den damaligen Kontext, verworfene Alternativen und erwartete Konsequenzen. In einer Legacy-Modernisierung ist besonders wichtig, den Gültigkeitsbereich und einen Revisionsauslöser festzuhalten. Dadurch bleibt nachvollziehbar, warum ein Übergangsadapter, ein Parallelbetrieb oder eine vorübergehende Datenkopie akzeptiert wurde und wann diese Entscheidung neu bewertet werden muss.
| ADR-Bestandteil | Leitfrage | Beispiel |
|---|---|---|
| Kontext | Welche Kräfte und Einschränkungen wirken? | Legacy-Consumer kann sechs Monate nicht migrieren |
| Entscheidung | Was wird verbindlich getan? | Anti-Corruption Layer vor dem neuen Domänenservice |
| Alternativen | Welche realistischen Optionen wurden geprüft? | Direktkopplung, Big-Bang-Ablösung, Event-Adapter |
| Konsequenzen | Welche Vorteile, Kosten und Risiken entstehen? | zusätzlicher Betrieb, aber entkoppelte Domänensprache |
| Revision | Wann verliert die Entscheidung ihre Grundlage? | letzter Legacy-Consumer abgeschaltet |
Kapitel 28 Technische Schulden: Geschäftswirkung, Tilgung und Zins sichtbar machenGovernance
Technische Schulden werden nicht über subjektive Codequalität priorisiert, sondern über ihre wiederkehrende Wirkung auf Lieferfähigkeit, Betriebsrisiko und fachliche Veränderbarkeit. Jede Schuld erhält eine Ursache, einen messbaren Zins, eine betroffene Geschäftsleistung und einen Tilgungsweg. Bewusst eingegangene Übergangslösungen bleiben zulässig, solange Ablaufdatum und Exit-Kriterium kontrolliert werden.
| Schuldenklasse | Messbarer Zins | Tilgungsstrategie |
|---|---|---|
| Kopplung | Änderungen benötigen mehrere Teams und Releases | Schnittstelle stabilisieren und Verantwortungsgrenze schneiden |
| Testdefizit | lange Regression und hohe Nacharbeitsquote | Characterization Tests und risikobasierte Verträge |
| Datenaltlast | manuelle Korrekturen und Reconciliation-Deltas | kanonisches Modell, Qualitätsregeln und schrittweise Bereinigung |
| Betriebsaltlast | lange Diagnose- und Wiederherstellungszeit | Observability, Runbooks und automatisierte Recovery |
| Übergangslösung | doppelte Kosten im Parallelbetrieb | Exit-Kriterium, Owner und verbindlicher Abschalttermin |
Kapitel 29 Risiken, Abhängigkeiten und Modernisierungsmetriken steuernGovernance
Legacy-Programme scheitern selten an einer einzelnen Komponente. Kritisch sind Ketten aus technischen, organisatorischen, fachlichen und externen Abhängigkeiten. Deshalb wird nicht nur eine Risikoliste gepflegt, sondern ein gerichtetes Abhängigkeitsmodell: Welcher Consumer, Dateneigentümer, Anbieter oder Freigabeprozess blockiert welchen Meilenstein und welche Entkopplungsoption reduziert die Wirkung?
| Abhängigkeit | Früher Indikator | Steuerungsmaßnahme |
|---|---|---|
| Consumer | Nutzung unbekannt oder keine Migrationszusage | Telemetry, Consumer-Inventar und Sunset-Vertrag |
| Daten | Owner oder Qualitätsregel fehlt | Data Owner benennen und Reconciliation vor Cutover |
| Organisation | kritisches Wissen bei Einzelperson | Pairing, Entscheidungsprotokoll und Stellvertretung |
| Anbieter | Releasefenster oder proprietäre Schnittstelle | Adapter, Exit-Klausel und getesteter Fallback |
| Regulatorik | Freigabe erst am Projektende vorgesehen | Kontrollnachweise inkrementell erzeugen |
Wirkung statt Aktivität messen
Fortschritt ist nicht die Zahl migrierter Klassen, Services oder Tickets. Entscheidend ist, ob die Organisation Fachänderungen sicherer, schneller und mit weniger Betriebs- und Wissensrisiko umsetzen kann. Die Metriken verbinden deshalb Lieferfähigkeit, Qualität, Betrieb, Daten und Domänenwissen. Aktivitätszahlen dürfen als Kontext dienen, aber niemals allein eine Abschaltung oder Freigabe begründen.
| Perspektive | Wirkungsmetrik | Fehlinterpretation vermeiden |
|---|---|---|
| Lieferfähigkeit | Lead Time für eine fachliche Regeländerung | mehr Deployments bedeuten nicht automatisch mehr Nutzen |
| Qualität | Produktionsfehler und manuelle Nacharbeit pro Vorgang | Testanzahl ersetzt keine Fehlerwirkung |
| Betrieb | MTTD, MTTR und verbrauchtes Fehlerbudget | Uptime ohne kritischen Geschäftspfad ist irreführend |
| Daten | Anteil erklärter Reconciliation-Abweichungen | reine Datensatzanzahl verschleiert fachliche Deltas |
| Wissen | kritische Regeln mit Owner, Beispiel und Testvertrag | Dokumentseiten messen keine Handlungsfähigkeit |
Domänenmodell als gemeinsames Steuerungsobjekt
Das Domänenmodell ist mehr als ein Klassendiagramm. Es verbindet ein gemeinsames Vokabular mit fachlichen Invarianten, Lebenszyklen, Ausnahmefällen und Entscheidungseigentümern. Historische Sonderlogik wird nicht unkritisch übernommen: Sie wird einem aktuellen Geschäftszweck, einer rechtlichen Pflicht oder einem befristeten Übergang zugeordnet. Unbegründete Regeln werden als Klärungsbedarf sichtbar.
| Domänenartefakt | Verbindlicher Inhalt | Qualitätsnachweis |
|---|---|---|
| Begriffsmodell | eindeutige Begriffe und Synonyme | Beispiele aus realen Geschäftsfällen |
| Invarianten | Regeln, die in jedem gültigen Zustand gelten | ausführbare Tests und fachliche Freigabe |
| Lebenszyklus | erlaubte Zustände und Übergänge | vollständige Transition-Matrix |
| Ausnahmen | Grund, Gültigkeit und Verantwortlicher | Entscheidungsakte und Ablaufdatum |
| Ownership | wer eine Regel ändern und freigeben darf | erreichbare Rolle mit Stellvertretung |
Anhang: Orientierung und Nachschlagewerk
Problemorientierte Einstiege, Entscheidungsatlas und Qualitätsnachweise bleiben erhalten, stehen aber nicht mehr vor dem fachlichen Lernweg.
Anhang A.1 Drei Enterprise-Fälle: gemeinsamer Startpunkt, unterschiedliche RisikotreiberEntscheidungsatlas
Der Atlas verbindet Claims-Verarbeitung, Dokumentenbatch und Legacy-SOAP-Integration. Alle drei beginnen mit einem Sicherheitsnetz, unterscheiden sich danach aber deutlich: fachliche Entscheidung und Seiteneffekte, Wiederanlauf und Datenkonsistenz oder externer Vertrag und Fehlerklassifikation bestimmen die Reihenfolge.
1. Gemeinsamer Startpunkt
- Beobachtbares Verhalten sichern: Characterization Tests, Golden Master, Vertragsbeispiele oder Restart-Szenarien.
- Größtes Risiko benennen: fachliche Fehlentscheidung, doppelte Verarbeitung oder Vertragsbruch.
- Kleinste kontrollierbare Grenze schaffen: Abhängigkeit, fachlicher Teilschritt, Port oder Checkpoint sichtbar machen.
- Erst danach abstrahieren: Pattern nur bei stabiler Variante; Architektur nur bei Systemgrenzen.
- Stopppunkt beweisen: Tests, Reconciliation, Metriken und Rückfallfähigkeit müssen den Nutzen belegen.
2. Risikotreiber und erste sinnvolle Eingriffe
| Fallstudie | Primäres Risiko | Sicherheitsnetz | Erster Eingriff |
|---|---|---|---|
| Claims-Verarbeitung | Fachliche Entscheidung ist mit Persistenz, Fremdsystem, Audit und Nachricht vermischt. | Golden Master, Grenzwerte und Alt-/Neuvergleich. | Abhängigkeiten sichtbar machen und Entscheidungsablauf fachlich benennen. |
| Dokumentenbatch | Wiederanlauf kann Datensätze doppelt verarbeiten, überspringen oder Checkpoints falsch setzen. | Restart-, Idempotenz-, Fehler- und Checkpoint-Tests. | Reader, Validator, Store und Checkpoint in klarer Reihenfolge trennen. |
| SOAP-Integration | WSDL-Vertrag, Mapping und Fehlercodes können unbemerkt in die Domäne dringen oder falsch wiederholt werden. | Characterization Tests, gespeicherte XML-Beispiele und Request-Capture. | Fachlichen Port schaffen und SOAP-Vertrag im Adapter/ACL kapseln. |
3. Wann lokal bleiben, wann Pattern, wann Architektur?
| Entscheidungsebene | Claims | Batch | SOAP |
|---|---|---|---|
| Lokal refaktorieren | Long Method zerlegen, Value Objects, Query/Command trennen. | Verarbeitungsreihenfolge, Fehlerpfade und Fachtypen explizit machen. | Mapping und Fehlerübersetzung aus dem Client lösen. |
| Pattern-Schwelle | Strategy/Policy bei echten stabilen Entscheidungsvarianten. | Policy bei wechselnden Validierungsregeln; Ports bei austauschbaren Infrastrukturzielen. | Adapter + Anti-Corruption Layer für den Fremdvertrag; Decorator für eng begrenzten Retry. |
| Architektur-Schwelle | Outbox oder verteilte Transaktion erst bei nachgewiesener Seiteneffekt-Grenze. | Atomare Commit-/Checkpoint-Grenze oder Framework erst bei lokal nicht lösbarer Konsistenz. | Shadow-Betrieb und Reconciliation bei Transport- oder Anbieterwechsel; kein REST-Umbau ohne Nutzen. |
4. Rückfallfähigkeit und Nachweis
Claims
Rückfall: Altpfad bleibt vergleichbar; neue Entscheidung kann deaktiviert werden.
Nachweis: fachliche Ergebnisgleichheit, Grenzwerte, Outbox-/Seiteneffekt-Tests.
Dokumentenbatch
Rückfall: Neustart ab bestätigtem Checkpoint; Idempotenz verhindert Doppelwirkung.
Nachweis: Wiederanlauf nach Fehlern an mehreren Positionen.
SOAP
Rückfall: Primärpfad bleibt führend, Schattenpfad ohne fachliche Nebenwirkung.
Nachweis: Reconciliation von Entscheidung, Diagnosecode und Score.
5. Stopppunkte je Fallstudie
| Fallstudie | Bewusster Stopppunkt | Nicht automatisch ergänzen |
|---|---|---|
| Claims | Entscheidung rein testbar, Seiteneffekte über klare Ports, fachlicher Vergleich stabil. | Microservices, generische Rule Engine oder universeller Event Bus. |
| Batch | Idempotent, restartfähig, Checkpoint begründet und betrieblich messbar. | Workflow-Engine oder generisches Batch-Framework ohne mehrere reale Nutzer. |
| SOAP | WSDL hinter Adapter, Fehler explizit, Retry begrenzt, Umschaltung messbar. | Circuit Breaker, REST-Neubau oder Messaging-Schicht ohne messbaren Bedarf. |
Claims-Fallstudie öffnen · Dokumentenbatch-Fallstudie öffnen · SOAP-Fallstudie öffnen
Anhang A.2 Symptom-zu-Vorgehen-MatrixProblemorientierung
Die Matrix beginnt nicht bei einem beliebten Pattern, sondern bei einem beobachtbaren Problem. Sie zeigt das zuerst abzusichernde Risiko, den kleinsten sinnvollen Eingriff und den Punkt, an dem ein Pattern oder eine Architekturänderung überhaupt gerechtfertigt ist.
| Beobachtbares Symptom | Zuerst absichern | Erster sinnvoller Schritt | Danach prüfen | Passende Fallstudie |
|---|---|---|---|---|
| God Class mit Geschäftslogik, Persistenz und Benachrichtigung | Grenzwerte, Entscheidungen und Reihenfolge der Seiteneffekte | Abhängigkeiten sichtbar machen; fachlichen Ablauf benennen | Value Objects, Query/Command-Trennung, Strategy/Policy nur bei stabilen Varianten | Claims-Verarbeitung |
| Lange Methode mit vielen Bedingungen | Characterization Tests für Haupt-, Grenz- und Fehlerpfade | Compose Method: fachliche Teilschritte extrahieren, ohne Architektur vorwegzunehmen | Primitive Obsession und versteckte Seiteneffekte | Claims-Verarbeitung |
| Unklar, ob Lesen und Ändern gemeinsam erfolgen | beobachtbare Rückgabewerte und alle ausgelösten Änderungen | Query von Modifier trennen | Transaktionsgrenze und Outbox erst bei echter Seiteneffekt-Grenze | Claims-Verarbeitung |
| Batch verarbeitet nach Neustart doppelt oder überspringt Datensätze | Restart-Szenarien an mehreren Fehlerpositionen | Reader, Validierung, Speicherung und Checkpoint-Reihenfolge explizit machen | Idempotenzschlüssel und atomare Commit-/Checkpoint-Grenze | Dokumentenbatch |
| Checkpoint wird gesetzt, obwohl Speicherung fehlschlägt | Fehler zwischen Store und Checkpoint reproduzieren | Bestätigungsreihenfolge korrigieren und Vertrag testen | lokale Transaktion; Framework erst bei mehreren realen Batchvarianten | Dokumentenbatch |
| Versteckte Netzabhängigkeit oder generierte SOAP-Typen in der Domäne | Request-Mapping, Antwortcodes, Timeouts und fachliche Ablehnungen | fachlichen Port einführen; Transport im Adapter kapseln | Anti-Corruption Layer und explizite Fehlerübersetzung | SOAP-Integration |
| Retry wiederholt auch fachliche Fehler | Fehlerklassen und Wiederholbarkeit pro Fehlerart | Retry-Entscheidung aus dem Transport lösen | Decorator nur für explizit retrybare technische Fehler; Backoff und Lastwirkung messen | SOAP-Integration |
| Alt- und Neupfad liefern unerklärte Abweichungen | fachliche Vergleichsmerkmale und repräsentative Sonderfälle | Shadow-Betrieb ohne Nebenwirkung und Reconciliation einführen | Umschalt- und Abbruchkriterien; Primärpfad bis zum Nachweis beibehalten | SOAP-Integration |
| Viele Switches ändern sich gemeinsam | welche Varianten wirklich stabil und fachlich verschieden sind | Bedingungen benennen und lokal zerlegen | Strategy/State erst bei wiederkehrenden stabilen Varianten | Claims-Verarbeitung |
| Unklare Transaktionsgrenze über Datenbank, Nachricht und Fremdsystem | welche Wirkungen atomar sein müssen und welche wiederholbar sind | fachliche Entscheidung von technischen Wirkungen trennen | Outbox, Saga oder Kompensation nur bei nachgewiesener Systemgrenze | Claims-Verarbeitung |
Kompakte Entscheidungsregel
- Symptom konkretisieren: Nicht "Code ist schlecht", sondern beobachtbare Fehlwirkung oder Änderungsrisiko benennen.
- Verhalten sichern: Der Test muss genau das größte Risiko festhalten.
- Klein lokal beginnen: Namen, Grenzen und Reihenfolge sichtbar machen.
- Abstraktion verdienen: Pattern erst bei stabiler Wiederholung, Architektur erst an einer echten Systemgrenze.
- Stoppen können: Wenn Risiko, Änderbarkeit und Betrieb messbar verbessert sind, ist mehr Struktur nicht automatisch besser.
Anhang A.3 Sechs durchgehende Enterprise-FallstudienFallstudien-Kompass
Alle großen Fallstudien folgen demselben Lernvertrag: Ausgangsproblem verstehen, beobachtbares Verhalten sichern, lokal strukturieren, Fachmodell und Grenzen herausarbeiten, technische Seiteneffekte kontrollieren, den Endzustand prüfen und bewusst stoppen.
Datenmodernisierung
Profiling, Fachtypen, Canonical Model, idempotente Übertragung, Quarantäne und Reconciliation.
Fallstudie öffnenLegacy-Modernisierung
Characterization Tests, Modernisierungsentscheidung, Strangler-Schnitt, Parallelbetrieb, Betriebsübergabe und Abschaltung.
Fallstudie öffnenClaims Workbench
God Class, fachliche Typen, Regelisolierung, Transaktionsgrenzen und Alt-/Neuvergleich.
Fallstudie öffnenCustomer Support
Statuswechsel, Rollen, SLA, Audit, Eskalation, Policies und Ports.
Fallstudie öffnenSOAP-Integration
Vertragszwänge, Port, Adapter, Anti-Corruption Layer, Fehlerübersetzung, Retry, Idempotenz und Shadow-Betrieb.
Fallstudie öffnenDokumentenbatch
Restart-Vertrag, Checkpoint-Reihenfolge, Idempotenz, Recovery und Dead Letter.
Fallstudie öffnenGemeinsames Kapitelmuster
| Schritt | Leitfrage | Nachweis |
|---|---|---|
| 1. Ausgangslage | Welches reale Risiko steckt im bestehenden Code? | konkreter Legacy-Code und beobachtbare Symptome |
| 2. Sicherheitsnetz | Welches Verhalten darf sich nicht unbemerkt ändern? | Characterization-, Golden-Master- oder Restart-Tests |
| 3. Lokale Struktur | Welcher kleinste Schritt erhöht Verständlichkeit und Testbarkeit? | benannte Abläufe, sichtbare Abhängigkeiten, Fachtypen |
| 4. Fachliche Grenzen | Welche Regeln und Verantwortlichkeiten sind stabil? | Policies, Ports und gezielte Patterns |
| 5. Betriebsgrenzen | Welche Seiteneffekte, Transaktionen und Rückfallpfade müssen kontrolliert werden? | Idempotenz, Reconciliation, Checkpoints oder Outbox |
| 6. Stopppunkt | Wann ist die Änderung ausreichend und weiteres Design nur noch Spekulation? | messbarer Nutzen, stabile Tests und dokumentierte Restschuld |
Anhang A.4 Was jede durchgehende Codegeschichte vollständig zeigen mussQualitätsnachweis
Die großen Fallstudien wurden nicht nur auf vorhandene Überschriften geprüft, sondern auf einen lückenlosen Lernweg. Jede Geschichte muss Ausgangscode, beobachtbares Risiko, Sicherheitsnetz, kleine Zwischenentscheidungen, ausführbaren Endzustand und einen begründeten Stopppunkt miteinander verbinden.
| Fallstudie | Ausgangscode | Sicherheitsnetz | Code-Evolution | Betriebsnachweis | Stopppunkt |
|---|---|---|---|---|---|
| Datenmodernisierung | inkonsistenter Kundenexport | Profiling und Datenbeispiele | Fachtypen, Canonicalizer, Pipeline | Quarantäne und Reconciliation | nur belegte Regeln migrieren |
| Legacy-Modernisierung | Support-Monolith | Characterization Tests | Port, Strangler, Kohortenrouting | Parallelvergleich und Betriebsübergabe | Abschaltung nur mit Evidenz |
| Claims | God Class mit Seiteneffekten | Golden Master und Grenzwerte | Fachtypen, Policies, Engine, Ports | Transaktionsgrenze und Alt-/Neuvergleich | kein vorschneller Microservice-Schnitt |
| Customer Support | prozeduraler Ticketprozessor | Status-, SLA- und Rollenfälle | Aggregat, Policies, Ports, Eskalation | Audit und wiederholbare Benachrichtigung | kein Workflow-Framework ohne Bedarf |
| SOAP-Integration | vertraglich gekoppelter Legacy-Client | XML-Verträge und Request-Capture | Port, Adapter, ACL, Fehlerübersetzung | Retry, Idempotenz, Shadow und Reconciliation | externen Vertrag nicht unnötig neu schreiben |
| Dokumentenbatch | nebenwirkungsreicher Datei-Batch | Restart- und Checkpoint-Tests | Reader, Policy, Sink und Checkpoint | Idempotenz, Recovery und Dead Letter | kein generisches Batch-Framework ohne Varianten |
Anhang A.5 Didaktische Abgrenzung der großen FallstudienDidaktik
Alle Geschichten verwenden denselben sicheren Grundrhythmus – Verhalten beobachten, kleinen Eingriff wählen, Ergebnis prüfen und bewusst stoppen. Wiederholt wird jedoch nur dieses Vorgehensmodell. Der fachliche Kern jeder Fallstudie bleibt unterschiedlich.
| Fallstudie | Einzigartiges Lernziel | Absichtlich nicht vertieft |
|---|---|---|
| Datenmodernisierung | Mehrdeutige Quelldaten in belegbare Fachtypen, Quarantäne und Reconciliation überführen. | Service-Schnitt oder Prozess-Orchestrierung. |
| Legacy-Modernisierung | Eine Modernisierungsstrategie anhand von Evidenz wählen und einen Strangler kontrolliert betreiben. | Detaillierte Domänenmodellierung einzelner Geschäftsregeln. |
| Claims | Komplexe Geschäftsentscheidung von Transaktion und Seiteneffekten trennen. | Externe Vertragsmigration und Batch-Wiederanlauf. |
| Customer Support | Statusübergänge, Rollen, SLA, Eskalation und Audit als konsistentes Fachmodell koordinieren. | Große Datenmigration oder technische Retry-Mechanik. |
| SOAP | Externen Vertrag über Port, Adapter, ACL, Fehlerklassifikation und Shadow-Betrieb entkoppeln. | Neuentwurf des Partnersystems oder unnötiger REST-Rewrite. |
| Dokumentenbatch | Restart, Checkpoint-Reihenfolge, Idempotenz, Recovery und Dead Letter korrekt beherrschen. | Interaktive Workflow- oder UI-Modellierung. |
Anhang A.6 Code-Evolution: Zwischenstände direkt vergleichenCode-Evolution
Die sechs großen Fallstudien wurden darauf geprüft, ob der Weg vom Ausgangscode zum Endzustand ohne didaktische Sprünge nachvollziehbar bleibt. Entscheidend ist nicht die reine Zeilenzahl, sondern ob jeder Zwischenstand eine klar benannte Verantwortung verändert, durch ein Sicherheitsnetz abgesichert ist und mit dem vorherigen Stand verglichen werden kann.
| Fallstudie | Vergleichbare Entwicklungsachse | Nachweis im Kapitel | Leseregel |
|---|---|---|---|
| Datenmodernisierung | Rohdaten → Fachtypen → Canonicalizer → Migrationspipeline | Profiling, Quarantäne, Checkpoint und Reconciliation | Jeder Schritt reduziert eine konkrete Datenmehrdeutigkeit. |
| Legacy-Modernisierung | Monolith → Port → Kohortenrouting → Strangler-Betrieb | Characterization Tests, Parallelvergleich und Abschaltkriterien | Architekturänderungen folgen erst nach belegtem Verhalten. |
| Claims | God Class → Fachtypen → Policies → transaktionaler Application Service | Tests, getrennte Seiteneffekte und Alt-/Neuvergleich | Die fachliche Entscheidung bleibt in jedem Stand erkennbar. |
| Customer Support | Ticketprozessor → Statusmodell → Policies → Ports und Eskalation | Status-, SLA-, Rollen- und Auditfälle | Jeder Stand macht genau eine bisher implizite Regel sichtbar. |
| SOAP | Legacy-Client → Port → Adapter/ACL → Retry/Idempotenz/Shadow | XML-Verträge, Fehlerklassen und Reconciliation | Transportdetails verschwinden schrittweise aus dem Anwendungscode. |
| Dokumentenbatch | prozeduraler Batch → Fachtypen → Ports → Recovery-Entscheidung | Restart-, Checkpoint-, Idempotenz- und Dead-Letter-Tests | Die Reihenfolge der Nebenwirkungen bleibt explizit prüfbar. |