93 Lernstrecken

Refactoring-Grundlagen & Enterprise-Fallstudien

Alle zugehörigen Inhalte befinden sich auf dieser einen großen Seite. Kapitel und Beispiele sind standardmäßig geschlossen und lassen sich gezielt öffnen.

0 von 125 offen

5 Buchteile · 29 Fachkapitel · 6 Enterprise-Fallstudien · keine Navigation durch Einzeldateien nötig.

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.

Teil IRefactoring verstehen: Smells, Sicherheitsnetz, SOLID und kleine Schritte.
Teil IITechnische Spezialgebiete: Nebenläufigkeit und Performance.
Teil IIIDurchgehende Code-Fallstudien mit Ausgangscode, Tests, Zwischenständen und Stopppunkt.
Teil IVLegacy- und Systemmodernisierung: Daten, Schnittstellen, Parallelbetrieb und Cutover.
Teil VBetrieb und Verantwortung: Observability, Rollback, Governance, ADRs und Schulden.
AnhangEntscheidungsatlas, Symptom-Matrix, Qualitätsnachweise und Praxis-Checklisten.

Direkte Leserwege

Wähle das beobachtbare Problem. Der erste Link führt zur Entscheidungshilfe, der zweite direkt zur passenden großen Fallstudie.

Buchteil 1

Refactoring 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-Katalog

Refactoring Master Workbench

Große, klappbare Seite mit sicheren Transformationen und klaren Einsatzgrenzen.

Bereiche offen: 0 von 0Öffnungszustand wird lokal gespeichert.

01. Refactoring-Katalog

SmellErster SchrittMögliches Ziel
Long MethodExtract Methodklarer Ablauf / Command
Primitive ObsessionValue Objectfachliche Invarianten
Switch StatementsDecompose ConditionalStrategy oder State
Feature EnvyMove Methodkohärentes Fachobjekt

02. Extract Method mit fachlichen Namen

Vorher
if (customer.active && order.total.compareTo(limit) < 0 && !order.blocked) {
    approve(order);
}
Nachher
if (isEligibleForAutomaticApproval(customer, order)) {
    approve(order);
}

Der neue Methodenname erklärt die Geschäftsentscheidung. Er versteckt nicht nur Syntax.

03. Guard Clauses

Nachher
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

OrderId.java
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.

Getrennt
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.

Ziel
DiscountStrategy strategy = strategies.get(customer.tier());
return strategy.apply(subtotal);

07. Extract Interface / Port

NotificationPort.java
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.

Grenzwerttest
@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.

Code Smells Masterclass

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 12 Code-Smell-Labs abgeschlossen

12 von 55 Labs abgeschlossen, 43 offen.

Bereiche offen: 0 von 0Icon-Schalter besitzen Tooltips und ARIA-Beschriftungen.

01. God Class Lab 01/12

Geruch

OrderBackofficeService bündelt Preis, Lager, Persistenz und Benachrichtigung.

Refactoring

Extract Class + Application Service

Pattern-Bezug

Facade

God Class: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
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);
}
Nachher.java
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);
}
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

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

Long Method: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
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
}
Nachher.java
public Decision decide(Claim claim) {
    validateDocuments(claim);
    RiskScore risk = assessRisk(claim);
    Decision decision = policy.decide(claim, risk);
    audit.record(claim.id(), decision);
    return decision;
}
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

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

Primitive Obsession: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
void pay(String iban, BigDecimal amount, String currency, String customerType) {
    if (!iban.startsWith("AT")) throw new IllegalArgumentException();
    // ...
}
Nachher.java
void pay(Iban iban, Money amount, CustomerSegment segment) {
    paymentPort.transfer(new PaymentInstruction(iban, amount, segment));
}
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

04. Switch Explosion Lab 04/12

Geruch

Jedes neue Dokumentformat erweitert mehrere switch-Blöcke.

Refactoring

Replace Conditional with Polymorphism

Pattern-Bezug

Strategy + Factory

Switch Explosion: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
return switch (format) {
    case "JSON" -> parseJson(payload);
    case "CSV" -> parseCsv(payload);
    case "XML" -> parseXml(payload);
    default -> throw new UnsupportedOperationException(format);
};
Nachher.java
DocumentReader reader = readerRegistry.forFormat(envelope.format());
ParsedDocument document = reader.read(envelope);
return pipeline.process(document);
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

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

Feature Envy: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
boolean eligible(Order order) {
    return order.customer().yearsActive() > 3
        && order.customer().complaintCount() == 0
        && order.total().compareTo(new BigDecimal("500")) > 0;
}
Nachher.java
boolean eligible(Order order) {
    return loyaltySpecification.isSatisfiedBy(order);
}
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

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

Data Clumps: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
ShippingQuote quote(String street, String zip, String city, String country,
                    BigDecimal weight, String unit) { ... }
Nachher.java
ShippingQuote quote(PostalAddress destination, Weight weight) {
    return shippingPort.quote(destination, weight);
}
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

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

Shotgun Surgery: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
if (priority.equals("CRITICAL")) team = "MAJOR_INCIDENT";
if (priority.equals("CRITICAL")) due = now.plusMinutes(15);
if (priority.equals("CRITICAL")) notifyOnCall();
Nachher.java
SupportPolicy policy = policies.forPriority(ticket.priority());
Assignment assignment = policy.assign(ticket, clock.instant());
events.publishAll(assignment.events());
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

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

Divergent Change: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
class PaymentOrchestrator {
    // Stripe HTTP, Fraud-Regeln, Retry, SQL und Audit-JSON
}
Nachher.java
FraudDecision decision = fraudChain.evaluate(command);
PaymentProvider provider = providers.resolve(command.provider());
PaymentResult result = provider.charge(command);
auditPort.record(PaymentAudit.from(command, result, decision));
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

09. Hidden Dependencies Lab 09/12

Geruch

Statische Uhr, UUID und SMTP-Aufruf machen Tests instabil.

Refactoring

Introduce Dependency

Pattern-Bezug

Ports and Adapters

Hidden Dependencies: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
String id = UUID.randomUUID().toString();
Instant now = Instant.now();
Mail.sendStatic(customer.email(), "Done");
Nachher.java
String id = idGenerator.nextId();
Instant now = clock.instant();
notificationPort.send(new CompletionNotice(customer.id(), id, now));
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

10. Boolean Blindness Lab 10/12

Geruch

Mehrere boolean-Parameter machen Aufrufe unverständlich.

Refactoring

Replace Flag Argument

Pattern-Bezug

Command

Boolean Blindness: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
process(ticket, true, false, true);
Nachher.java
process(new TicketProcessingCommand(
    ticket.id(), EscalationMode.IMMEDIATE, NotificationMode.CUSTOMER_AND_TEAM));
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

11. Inappropriate Intimacy Lab 11/12

Geruch

Zwei Aggregate ändern gegenseitig interne Collections.

Refactoring

Move Method + Domain Service

Pattern-Bezug

Domain Service

Inappropriate Intimacy: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
order.mutableLines().remove(line);
inventory.internalReservations().put(line.sku(), line.quantity());
Nachher.java
reservationService.replaceLine(order, line, replacement);
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?

12. Exception Control Flow Lab 12/12

Geruch

Fachliche Ablehnungen werden als technische Exceptions transportiert.

Refactoring

Replace Exception with Result

Pattern-Bezug

Result / Policy

Exception Control Flow: vom Code Smell zur klaren Verantwortlichkeit
Vorher.java
try { approve(claim); } catch (LimitExceededException ex) {
    return "MANUAL_REVIEW";
}
Nachher.java
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());
};
Prüffrage: Ist nach dem Umbau klar, welche Klasse aus welchem Änderungsgrund angepasst wird?
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.

Code-Smell-Abschnitt 2

SOLID Masterclass

Zehn umfangreiche Java-Labs zeigen SOLID nicht als Merksatz, sondern als konkrete Refactoring-Entscheidung in Order, Payment, Claims, Support und Dokumentverarbeitung.

Kapitel offen: 0 von 1010/10 Labs abgeschlossenJava 21 · Maven · JUnit
Lernfortschritt: 48 von 55 Labs abgeschlossen

7 Labs offen · anschließend: Pattern Decision Workbench.

Prinzipien: SRP · OCP · LSP · ISP · DIP. Jedes Lab enthält Problem, Refactoring, Pattern-Bezug, Vorher-/Nachher-Code und eine kompakte SVG.

01. SRP: OrderApplicationService entlasten Lab 01/10

Verletzung

Eine Klasse validiert, berechnet, speichert und benachrichtigt.

Refactoring

Extract Class + Ports

Pattern-Bezug

Facade + Domain Service

SRP: OrderApplicationService entlasten
Vorher.java
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);
    }
}
Nachher.java
// 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);
    }
}
Entscheidung: Das SRP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

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

SRP: Audit aus der Fachlogik lösen
Vorher.java
public Decision approve(Claim claim) {
    Decision result = rules.evaluate(claim);
    Files.writeString(Path.of("audit.json"), toJson(claim, result));
    return result;
}
Nachher.java
public Decision approve(Claim claim) {
    Decision result = rules.evaluate(claim);
    auditPort.record(ClaimAudit.from(claim, result));
    return result;
}
Entscheidung: Das SRP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

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

OCP: Rabattarten erweiterbar machen
Vorher.java
return switch (customer.segment()) {
    case STANDARD -> subtotal;
    case BUSINESS -> subtotal.multiply(new BigDecimal("0.95"));
    case VIP -> subtotal.multiply(new BigDecimal("0.90"));
};
Nachher.java
DiscountPolicy policy = policies.resolve(customer.segment());
return policy.apply(subtotal, customer);

// Neue Variante ohne Änderung am Aufrufer:
final class PartnerDiscountPolicy implements DiscountPolicy { ... }
Entscheidung: Das OCP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

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

OCP: Dokumentformate als Plugin
Vorher.java
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);
Nachher.java
DocumentReader reader = registry.readerFor(mediaType);
ParsedDocument document = reader.read(envelope);

registry.register(new PdfDocumentReader());
Entscheidung: Das OCP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

05. LSP: Zahlungsprovider substituierbar halten Lab 05/10

Verletzung

Ein Provider wirft bei capture immer UnsupportedOperationException.

Refactoring

Split Interface

Pattern-Bezug

Capability Interfaces

LSP: Zahlungsprovider substituierbar halten
Vorher.java
interface PaymentProvider {
    Authorization authorize(Payment p);
    Capture capture(Authorization a);
}
final class PrepaidProvider implements PaymentProvider {
    public Capture capture(Authorization a) {
        throw new UnsupportedOperationException();
    }
}
Nachher.java
interface AuthorizingProvider {
    Authorization authorize(Payment payment);
}
interface CapturingProvider {
    Capture capture(Authorization authorization);
}
final class PrepaidProvider implements AuthorizingProvider { ... }
Entscheidung: Das LSP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

06. LSP: Repository-Vertrag präzisieren Lab 06/10

Verletzung

Eine Implementierung liefert null, eine andere Optional.

Refactoring

Strengthen Contract

Pattern-Bezug

Repository

LSP: Repository-Vertrag präzisieren
Vorher.java
interface CustomerRepository {
    Customer find(String id); // null oder Exception?
}
Nachher.java
interface CustomerRepository {
    Optional<Customer> find(CustomerId id);
}

Customer customer = customers.find(id)
    .orElseThrow(() -> new CustomerNotFound(id));
Entscheidung: Das LSP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

07. ISP: Support-Ports aufteilen Lab 07/10

Verletzung

Ein breites Gateway zwingt Clients zu unbenutzten Methoden.

Refactoring

Extract Interface

Pattern-Bezug

Role Interfaces

ISP: Support-Ports aufteilen
Vorher.java
interface SupportGateway {
    Ticket load(TicketId id);
    void save(Ticket ticket);
    void sendSms(String phone, String text);
    void exportCsv(LocalDate day);
}
Nachher.java
interface TicketRepository {
    Ticket load(TicketId id);
    void save(Ticket ticket);
}
interface NotificationPort { void notify(Notice notice); }
interface SupportExportPort { Export export(LocalDate day); }
Entscheidung: Das ISP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

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

ISP: Query und Command trennen
Vorher.java
interface ClaimService {
    ClaimDetails load(ClaimId id);
    Decision decide(ClaimId id);
    void reopen(ClaimId id);
    void delete(ClaimId id);
}
Nachher.java
interface ClaimQuery {
    ClaimDetails load(ClaimId id);
}
interface DecideClaimUseCase {
    Decision decide(ClaimId id);
}
Entscheidung: Das ISP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

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

DIP: SMTP und Datenbank hinter Ports
Vorher.java
public final class RegisterCustomerService {
    private final JdbcCustomerDao dao = new JdbcCustomerDao();
    private final SmtpClient mail = new SmtpClient();
}
Nachher.java
public final class RegisterCustomerUseCase {
    private final CustomerRepository customers;
    private final NotificationPort notifications;

    public RegisterCustomerUseCase(
        CustomerRepository customers,
        NotificationPort notifications) { ... }
}
Entscheidung: Das DIP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?

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

DIP: Zeit und IDs kontrollierbar machen
Vorher.java
Ticket ticket = new Ticket(
    UUID.randomUUID().toString(),
    Instant.now(),
    request.subject());
Nachher.java
Ticket ticket = new Ticket(
    ticketIds.nextId(),
    clock.instant(),
    request.subject());
Entscheidung: Das DIP-Prinzip ist kein Selbstzweck. Der Umbau lohnt sich, wenn unabhängige Änderungsgründe, Varianten oder Infrastrukturabhängigkeiten tatsächlich vorhanden sind.
Prüffrage: Kann eine neue Variante oder Infrastrukturimplementierung ergänzt werden, ohne den stabilen Use Case zu verändern?
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.

Code-Smell-Abschnitt 3

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.

Kapitel offen: 0 von 1010/10 Labs abgeschlossenJava 21 · Maven · JUnit
Lernfortschritt: 48 von 55 Labs abgeschlossen

7 Labs offen · anschließend: Pattern Decision Workbench.

Themen: Fabriken · Builder · Dependency Injection · Ressourcen · Wertsemantik · Immutability · Enum · Lambdas · Optional · Exceptions.

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

Statische Fabrikmethoden statt unklarer Konstruktoren
Vorher.java
new CustomerId(UUID.fromString(raw))
Nachher.java
CustomerId id = CustomerId.parse(raw);
Entscheidung: Benannte Fabriken erklären Absicht, können cachen und Untertypen zurückgeben.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Builder für viele optionale Parameter
Vorher.java
new PaymentRequest(id, amount, EUR, "PAY-42")
Nachher.java
PaymentRequest.builder()
    .customer(id)
    .amount(amount)
    .reference("PAY-42")
    .build();
Entscheidung: Builder sind sinnvoll bei vielen Parametern und Invarianten, nicht für jedes kleine Record.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Dependency Injection statt versteckter Singletons
Vorher.java
Repository repo = GlobalServices.repository();
Nachher.java
var service = new InvoiceService(repository, clock);
Entscheidung: Abhängigkeiten werden am Konstruktionspunkt sichtbar und unveränderlich.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

try-with-resources statt manuellem close
Vorher.java
BufferedReader r = Files.newBufferedReader(path);
try { return r.lines().toList(); } finally { r.close(); }
Nachher.java
try (BufferedReader r = Files.newBufferedReader(path)) {
    return r.lines().toList();
}
Entscheidung: Die Sprache garantiert das Schließen auch bei Exceptions und unterdrückten Folgefehlern.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

equals und hashCode als fachliche Einheit
Vorher.java
class OrderKey { String tenant; String number; /* equals fehlt */ }
Nachher.java
record OrderKey(String tenant, String number) {
    OrderKey { /* Invarianten */ }
}
Entscheidung: Records erzeugen konsistente Wertsemantik; Identität muss fachlich bewusst gewählt werden.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Unveränderlichkeit und defensive Kopien
Vorher.java
this.items = items;
Nachher.java
this.items = List.copyOf(items);
Entscheidung: Unveränderliche Objekte reduzieren Seiteneffekte und vereinfachen Nebenläufigkeit.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Enum statt int-Konstanten
Vorher.java
static final int HIGH = 10;
Nachher.java
enum Priority { LOW(1), NORMAL(5), HIGH(10); }
Entscheidung: Ein Enum bündelt gültige Werte, Verhalten und Compiler-Sicherheit.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Lambdas gezielt für kleine Strategien
Vorher.java
rules.add(new Predicate<>() { public boolean test(PaymentRequest p) { return p.amount().signum() > 0; }});
Nachher.java
rules.add(PaymentRules::hasPositiveAmount);
Entscheidung: Methodenreferenzen halten die Pipeline lesbar; komplexe Regeln bleiben benannte Methoden oder Klassen.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Optional nur als Rückgabewert
Vorher.java
String name = names.get(id);
Nachher.java
Optional<String> name = directory.findName(id);
Entscheidung: Optional macht Abwesenheit am API-Rand sichtbar, gehört aber nicht in Felder, Parameter oder Collections.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?

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

Exceptions übersetzen und Kontext bewahren
Vorher.java
catch (Exception ex) { throw new SchritttimeException(ex); }
Nachher.java
catch (Exception ex) {
    throw new PaymentException(reference, "Provider failed", ex);
}
Entscheidung: Die Fachschicht erhält stabile Begriffe; Ursache und Referenz bleiben für Betrieb und Diagnose erhalten.
Prüffrage: Verbessert die Änderung API-Verständlichkeit, Korrektheit oder Wartbarkeit messbar - oder erzeugt sie nur zusätzliche Abstraktion?
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.

Code-Smell-Abschnitt 4

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.

Kapitel offen: 0 von 88/8 Labs abgeschlossenJava 21 · Maven · JUnit
Lernfortschritt: 48 von 55 Labs abgeschlossen

7 Labs offen · anschließend: Pattern Decision Workbench.

Themen: Namen · kleine Methoden · Guard Clauses · Value Objects · Kommentare · Side Effects · Result Types · Decision Objects.

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

Sprechende Namen statt Abkürzungen
Vorher.java
int d; boolean f;
Nachher.java
int overdueDays; boolean fraudSuspected;
Warum: Namen tragen Fachbedeutung und vermeiden mentale Übersetzung.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Kleine Methoden mit einer Aufgabe
Vorher.java
validate(); calculate(); persist(); notify();
Nachher.java
validateOrder(); Money total = calculateTotal(); save(order);
Warum: Jede Methode beantwortet eine klare Frage und bleibt auf einer Abstraktionsebene.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Guard Clauses statt tiefer Verschachtelung
Vorher.java
if (customer != null) { if (customer.isActive()) { ... } }
Nachher.java
if (customer == null) return;
if (!customer.isActive()) return;
Warum: Frühe Ausstiege machen den Hauptpfad sichtbar.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Fachobjekte statt Primitive Obsession
Vorher.java
String customerName; String status;
Nachher.java
CustomerName name; CustomerStatus status;
Warum: Gültigkeit und Bedeutung liegen im Typ statt in Kommentaren.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Kommentare durch klare Struktur ersetzen
Vorher.java
// calculate 20 percent tax
var t = n.multiply(new BigDecimal("0.20"));
Nachher.java
BigDecimal tax = calculateVat(netAmount);
Warum: Kommentare erklären Entscheidungen; der Code selbst erklärt den Ablauf.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Seiteneffekte explizit an Ports geben
Vorher.java
repository.save(order); mail.send(...);
Nachher.java
save(order); notifier.orderAccepted(order);
Warum: Geschäftslogik und technische Effekte werden separat testbar.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Fehler als fachliches Ergebnis modellieren
Vorher.java
try { validate(amount); } catch (Exception e) { return false; }
Nachher.java
Result<BigDecimal> result = validator.validate(amount);
Warum: Erwartbare Ablehnungen sind Daten; unerwartete Fehler bleiben Exceptions.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.

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

Boolean Blindness durch Entscheidungen ersetzen
Vorher.java
return amount.compareTo(limit) <= 0;
Nachher.java
return OrderDecision.reject("Express limit exceeded");
Warum: Ein Entscheidungsobjekt transportiert Ergebnis und fachliche Begründung.
Grenze: Kleine Methoden und zusätzliche Typen sind nur dann besser, wenn sie echte Fachbegriffe oder stabile Verantwortlichkeiten ausdrücken.
Buchteil 2

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.

Code-Smell-Abschnitt 5

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.

Kapitel offen: 0 von 88/8 Labs abgeschlossenJava 21 · Maven · JUnit
Lernfortschritt: 48 von 55 Labs abgeschlossen

7 Labs offen · anschließend: Pattern Decision Workbench.

Themen: Shared State · Atomicity · LongAdder · Immutable Snapshots · Lock Scope · Executor Ownership · CompletableFuture · Virtual Threads.

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

Shared Mutable State durch ConcurrentHashMap kapseln
Unsicherer Ausgangscode.java
HashMap<String, Ticket> tickets = new HashMap<>();
Refaktorierter Java-21-Code.java
return tickets.computeIfAbsent(id, key -> new SupportTicket(key, subject));
Warum: Gemeinsamer Zustand wird hinter einer kleinen API gekapselt; atomare Map-Operationen ersetzen externe Synchronisation.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Check-then-act atomar ausführen
Unsicherer Ausgangscode.java
if (stock.get(sku) >= qty) stock.put(sku, stock.get(sku) - qty);
Refaktorierter Java-21-Code.java
if (available.compareAndSet(current, current - quantity)) return true;
Warum: Prüfen und Ändern bilden eine atomare fachliche Operation; Überreservierungen werden verhindert.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Hot Counters mit LongAdder entlasten
Unsicherer Ausgangscode.java
synchronized void accepted() { accepted++; }
Refaktorierter Java-21-Code.java
private final LongAdder accepted = new LongAdder();
Warum: Viele schreibende Threads konkurrieren nicht mehr um denselben Monitor; gelesen wird über einen Snapshot.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Konfiguration als immutable Snapshot veröffentlichen
Unsicherer Ausgangscode.java
routes.clear(); routes.putAll(next);
Refaktorierter Java-21-Code.java
routes.set(Map.copyOf(next));
Warum: Leser sehen immer eine vollständige Version der Routing-Tabelle und niemals einen halbfertigen Umbau.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Lock-Bereich auf Zustandsänderung begrenzen
Unsicherer Ausgangscode.java
synchronized long allocate() { audit.write(next); return next++; }
Refaktorierter Java-21-Code.java
lock.lock(); try { number = next++; } finally { lock.unlock(); }
Warum: Langsame Audit-I/O blockiert keine weiteren Nummernvergaben; der kritische Abschnitt bleibt minimal.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Executor-Lebenszyklus explizit besitzen
Unsicherer Ausgangscode.java
var pool = Executors.newFixedThreadPool(20);
Refaktorierter Java-21-Code.java
new NotificationDispatcher(applicationExecutor);
Warum: Der Use Case erzeugt keine versteckten Threads; Start, Shutdown und Ressourcenlimit bleiben im Composition Root.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Asynchrone Fehler als fachliche Entscheidung behandeln
Unsicherer Ausgangscode.java
future.join(); // CompletionException entweicht ungeplant
Refaktorierter Java-21-Code.java
future.exceptionally(error -> 100).thenApply(this::toDecision);
Warum: Fehlerpolitik, Aggregation und Ergebnisbildung sind sichtbar und testbar statt in Callback-Ketten verteilt.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.

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

Blockierende I/O mit Virtual Threads skalieren
Unsicherer Ausgangscode.java
Executors.newFixedThreadPool(200)
Refaktorierter Java-21-Code.java
Executors.newVirtualThreadPerTaskExecutor()
Warum: Java 21 Virtual Threads vereinfachen blockierende Workloads; Interrupts und Fehler werden trotzdem bewusst übersetzt.
Grenze: Thread-Sicherheit entsteht nicht durch wahllosen Einsatz synchroner Collections. Die fachliche Atomarität, Ownership und Fehlerpolitik müssen explizit bleiben.
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.

7/7 Labs

Performance Refactorings

Messbare Optimierungen an Datenzugriffen, Traversierungen, Speicher, CPU und Regression-Schutz. Jeder Schritt zeigt Problem, Entscheidung, Java-Code und eine kompakte themenspezifische SVG.

Vollständig: 55/55 Labs

Offen: 0 Labs · anschließend: Pattern Decision Workbench

Bereiche offen: 0 von 0Performance-Labs: 7/7Grundregel: Measure → Change → Verify
Filter:

Lab 01 · Mehrfache Traversierungen bündeln Single-Pass Aggregator

Mehrfache Traversierungen bündeln

Ausgangspunkt

Mehrere Streams berechnen Summe, Stückzahl und Zeilenanzahl getrennt. Das wirkt elegant, liest dieselbe Liste aber mehrfach.

Vorher.java
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.

Nachher.java
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());
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.

Lab 02 · Remote Reads mit Cache-Aside begrenzen Cache-Aside Decorator

Remote Reads mit Cache-Aside begrenzen

Ausgangspunkt

Ein Kundenprofil wird innerhalb mehrerer Use Cases immer wieder vom entfernten System geladen.

Vorher.java
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.

Nachher.java
return delegate.findById(id).map(value -> {
    cache.put(id, new Entry(value, now.plus(ttl)));
    return value;
});
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.

Lab 03 · Große Ergebnismengen seitenweise verarbeiten Cursor Pagination

Große Ergebnismengen seitenweise verarbeiten

Ausgangspunkt

Ein Export lädt alle Rechnungen in eine Liste und erzeugt dadurch lange Pausen und hohen Heap-Verbrauch.

Vorher.java
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.

Nachher.java
String cursor = null;
do {
    InvoicePage page = query.fetchAfter(cursor, pageSize);
    page.rows().forEach(sink);
    cursor = page.nextCursor();
} while (cursor != null);
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.

Lab 04 · N+1-Zugriffe durch Batch Fetch ersetzen DataLoader / Batch Fetch

N+1-Zugriffe durch Batch Fetch ersetzen

Ausgangspunkt

Für jede Bestellposition wird einzeln ein Preis geladen. Bei 500 Positionen entstehen bis zu 500 Remote- oder Datenbankaufrufe.

Vorher.java
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.

Nachher.java
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);
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.

Lab 05 · String-Allokationen im Report reduzieren Pre-sized Buffer

String-Allokationen im Report reduzieren

Ausgangspunkt

CSV-Ausgabe entsteht mit wiederholter String-Verkettung in einer Schleife.

Vorher.java
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.

Nachher.java
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();
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.

Lab 06 · Teure reine Berechnung memoizen Memoization Decorator

Teure reine Berechnung memoizen

Ausgangspunkt

Eine deterministische Risikobewertung wird für identische Requests mehrfach ausgeführt.

Vorher.java
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.

Nachher.java
private final ConcurrentHashMap<RiskRequest, RiskDecision> memo = new ConcurrentHashMap<>();
public RiskDecision evaluate(RiskRequest request) {
    return memo.computeIfAbsent(request, delegate::evaluate);
}
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.

Lab 07 · Performance als Budget und Testgrenze behandeln Executable Performance Budget

Performance als Budget und Testgrenze behandeln

Ausgangspunkt

Optimierungen werden ohne Zielwert umgesetzt und spätere Regressionen bleiben lange unbemerkt.

Vorher.java
// "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.

Nachher.java
var budget = new PerformanceBudget(Duration.ofMillis(100));
Duration actual = measuredSearchDuration();
budget.verify(actual, "customer-search");
Messregel: Erst Profiler, Tracing oder Benchmark als Evidenz verwenden. Die Optimierung muss fachlich gleichwertig bleiben und durch Tests abgesichert sein.
Buchteil 3

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.

Große Fallstudie

Order Pricing – 7 Refactoring-Schritte

Eine zusammenhängende Seite vom komplexen Legacy-Code bis zur begründeten Strategy-Lösung.

Bereiche offen: 0 von 0Öffnungszustand wird lokal gespeichert.

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.

Refactoring-Ablauf
Refactoring-Schritte: 7 insgesamt. Schritt 1 bis 3 sichern und ordnen den Code; Schritt 4 bis 6 formen Fachmodell und Pattern; Schritt 7 bewertet den Zielzustand.

02. Schritt 0 – Komplexer Ausgangscode

LegacyPricingService.java
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;
}
ProblemKonsequenz
Berechnung, Persistenz und E-Mail in einer MethodeJede Änderung berührt mehrere Verantwortlichkeiten.
Strings als TypcodesTippfehler werden erst zur Laufzeit sichtbar.
BigDecimal ohne WährungFachlich ungültige Operationen bleiben möglich.
if-/else-KetteNeue 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.

LegacyPricingCharacterizationTest.java
@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.

OrderApplicationService.java
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.

Money.java
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.

DiscountStrategy.java / PricingService.java
// 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

PricingService.java
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());
}
Trade-off: Eine Registry ist übersichtlich, solange die Auswahl nur von einem stabilen Schlüssel abhängt. Bei kontextabhängiger Auswahl wäre ein Resolver oder eine Specification geeigneter.

08. Schritt 6 – Testbarer Zielcode

PricingServiceTest.java
@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

VerbessertNoch offen
Fachtypen, reine Preisberechnung, austauschbare Regeln, isolierte TestsSteuerlogik, 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.

Kommentierte Lösung öffnen

Übungslösung: Staffelrabatt sicher ergänzen

Decorator-Lösung mit Begründung und Grenzwerttests.

Kommentierte Musterlösung

Lösung – Staffelrabatt

Decorator-Lösung mit Begründung und Grenzwerttests.

Bereiche offen: 0 von 0Öffnungszustand wird lokal gespeichert.

01. Lösung: Staffelrabatt

Die Lösung nutzt einen Decorator, weil der Staffelrabatt eine bestehende Kundensegment-Strategy ergänzt und nicht ersetzt.

TieredDiscount.java
// 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;
    }
}
TieredDiscountTest.java
@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.

Große Fallstudie · 8 Schritten

Payment Orchestration Refactoring Workbench

Vom rekursiven Legacy-Payment-Service zur expliziten Orchestrierung mit Adapter, Chain of Responsibility, Strategy Registry, Facade und atomarer Idempotenz-Reservierung.

Bereiche offen: 0 von 0
Ablaufdiagramm

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.

Ziel: Der Ablauf bleibt synchron und verständlich; Retry und Persistenz werden nicht magisch versteckt.

01 - Legacy-Ausgangscode auf-/zuklappen

LegacyPaymentService.java
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

PaymentRequest.java
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

PaymentProvider.java / CardProviderAdapter.java
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

FraudChain.java
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

PaymentOrchestrator.java
var cached = store.find(request.idempotencyKey());
if (cached.isPresent()) return cached.get();
var result = provider.charge(request);
store.save(request.idempotencyKey(), result);
Produktionshinweis: Ein echtes System benötigt atomare Reservierung beziehungsweise Unique Constraint. Das In-Memory-Beispiel erklärt nur den fachlichen Ablauf.

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.

PaymentExecutionStore.java
public interface PaymentExecutionStore {
  Reservation reserve(String idempotencyKey);
  void complete(String idempotencyKey, PaymentResult result);
  void release(String idempotencyKey);

  enum Reservation { ACQUIRED, IN_PROGRESS, COMPLETED }
}
ResilientPaymentOrchestrator.java
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.

Abgeschlossen: Payment besitzt jetzt alle 8 geplanten Refactoring-Schritte. Der Test ResilientPaymentOrchestratorTest beweist, dass ein wiederholter Schlüssel nur einen Provideraufruf auslöst.

09 - Tests und Lernaufgabe auf-/zuklappen

PaymentOrchestratorTest.java
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.

Große Fallstudie · 7 Schritten

Claims Management Refactoring Workbench

Von einer Map-basierten Prozedur zu Fachobjekt, Specification, expliziter Zustandsmaschine, testbarem Domain Service und austauschbarer Entscheidungs-Policy.

Bereiche offen: 0 von 0
Ablaufdiagramm

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

LegacyClaimProcessor.java
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

Claim.java
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

ClaimSpecification.java
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

ClaimStateMachine.java
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

ClaimReviewService.java
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.

ClaimDecisionPolicy.java
@FunctionalInterface
public interface ClaimDecisionPolicy {
  ClaimDecision decide(Claim claim);
}
ClaimDecisionService.java
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.

Abgeschlossen: Claims besitzt jetzt alle 7 geplanten Refactoring-Schritte. Kleine Fälle werden automatisch genehmigt; große bleiben bewusst in manueller Prüfung.

09 - Tests, Grenzen und nächste Vertiefung auf-/zuklappen

ClaimReviewServiceTest.java
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.

Durchgehender Refactoring-Pfad der Claims-Verarbeitung

01. Das echte Problem lesen, bevor Code verschoben wird

Code-Evolution: God Class → Fachtypen → Policies → Application Service
9 sichtbare Codeblöcke und 10 Lernschritte; Entscheidung, Seiteneffekte und Transaktion werden nacheinander getrennt.
Einzigartiger Fokus: Geschäftsentscheidung versus Seiteneffekt

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.

BeobachtungRisikoErster Nachweis
Datum wird direkt gelesenTests sind nicht reproduzierbarAudit-Ausgabe charakterisieren
Fraud-Aufruf mitten in der MethodeNetzfehler verändert FachentscheidungTimeout-Fall sichern
Persistenz und Nachricht unmittelbarpartielle SeiteneffekteReihenfolge beobachten
Strings und Booleansungültige Zustände bleiben darstellbarGrenzfälle sammeln
LegacyClaimProcessor.java - vollständiger Ausgangspunkt
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.

Regel: Erst wenn Status, Reserve, Audit und Nachricht reproduzierbar sind, darf der Code zerlegt werden.
ComplexClaimRefactoringTest.java - Characterization und Vergleich
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.

Ports - technische Wirkungen werden benannt
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.

ClaimCommand.java - Fachtypen an der Grenze
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 }
}
Zwischenstand: Der Ablauf ist noch derselbe, aber ungültige Währung, leere ID und negative Beträge können nicht mehr unbemerkt weitergereicht werden.

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.

Compose Method - der Geschäftsablauf wird sichtbar
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.

ClaimDecisionEngine.java - reine fachliche Entscheidung
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.

Gezielte Policy-Kombination
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()));
Stop-Regel: Kein generisches Regelwerk einführen, solange wenige stabile Policies ausreichen.

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.

ProcessClaimUseCase.java - koordinierter Endablauf
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();
        }
    }
}
WirkungGarantieFehlerstrategie
Entscheidungdeterministischerneut berechenbar
Claim speichernatomar mit OutboxRollback
Ereignis publizierenmindestens einmalidempotenter Consumer
Auditnachvollziehbartechnisch 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.

ReconciliationService.java - fachlicher Vergleich
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.

ClaimsIntermediate.java
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

Änderungsdiff
- Decision decision = calculateAndPersistAndNotify(command);
- return decision;
+ ClaimDecision decision = decisionService.decide(command);
+ repository.save(decision);
+ outbox.append(ClaimDecided.from(decision));
+ audit.record(decision);
+ return decision;
Was sich fachlich ändert: Verantwortung verschoben: Die Fachentscheidung ist rein. Persistenz, Ereignis und Audit bleiben sichtbar koordiniert, aber nicht mehr Teil der Regelberechnung.

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
Vergleichsregel: Dieser Zwischenstand muss separat kompilierbar und gegenüber Ausgangs- sowie Endzustand fachlich erklärbar bleiben.
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.

Durchgehende Legacy-Modernisierungsfallstudie

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.

Lernpfad: Ausgangscode → Evidenz → sichere Grenze → Parallelbetrieb → Abschaltung

Die vorhandenen Einzeltechniken bleiben erhalten und werden durch einen roten Faden verbunden.

Bereiche offen: 0 von 0

Legacy-Modernisierung als vollständige Geschichte

Code-Evolution: Monolith → Port → Routing → Strangler
Die Modernisierungsentscheidung bleibt vom technischen Umbau getrennt; Parallelbetrieb und Abschaltung besitzen eigene Nachweise.
Einzigartiger Fokus: Modernisierungsstrategie und Cutover

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?

Legacy-Modernisierung vom Ausgangssystem bis zur kontrollierten Abschaltung

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.

LegacyModernizationIntermediate.java
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

Änderungsdiff
- 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ührend
Was sich fachlich ändert: Verantwortung verschoben: Der Altpfad bleibt fachlich führend; Kohortenauswahl und Vergleich werden zu eigenen, testbaren Entscheidungen.

Vom 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
Vergleichsregel: Dieser Zwischenstand muss separat kompilierbar und gegenüber Ausgangs- sowie Endzustand fachlich erklärbar bleiben.

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.

LegacySupportMonolith.java - Ausgangszustand
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;
}
BeobachtungRisiko bei ÄnderungErster Nachweis
String-Array als DatenmodellSpaltenpositionen und Null-Konventionen werden verwechselt.repräsentative Legacy-Zeilen und Mapping-Tests
Regel und Seiteneffekt in einer Methodefachlich richtige Entscheidung kann technisch doppelt wirken.Snapshot von Team, Status, Mail und Audit
SQL und SMTP direkt im AblaufUnit-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.

Golden-Master-Vertrag
BehaviorSnapshot snapshot = new BehaviorSnapshot(
        returnedTeam,
        persistedStatus,
        sentMails.size(),
        auditEntries.size());

assertEquals(new BehaviorSnapshot(
        "MAJOR_INCIDENT", "ASSIGNED", 1, 1), snapshot);
Abbruchregel: Solange Grenzfälle, Fehlerpfade und Seiteneffekte nicht reproduzierbar sind, wird keine Zielarchitektur umgesetzt.

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.

ModernizationAssessment.java
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;
}
EntscheidungWannNächster kleiner Schritt
Stabilisierenkritisch, wenig EvidenzCharacterization Tests, Observability, Fehlerpfade
Kapselnhohe Kopplung, aber kein sicherer ErsatzschnittFacade/Port um Legacy-Grenze
Stranglerstabile fachliche Grenze und hoher ÄnderungsdruckKohorte, 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.

SupportProcessingPort.java
@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.

StranglerSupportGateway.java
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;
}
Wichtige Grenze: Der Schattenpfad darf keine zweite Mail, kein zweites Audit und keine zweite Statusänderung auslösen. Für produktiven Parallelbetrieb wird er daher mit side-effect-freien Adaptern oder aufgezeichneten Inputs ausgeführt.

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.

VergleichBewertungAktion
Status und Team gleichfachlich äquivalentKohorte vorsichtig erhöhen
Diagnosecode anders, Entscheidung gleicherklärbare technische DifferenzMapping dokumentieren
Team oder Status andersfachliche AbweichungKohorte stoppen, Fall analysieren
Shadow technisch nicht verfügbarkeine VergleichsevidenzPrimärpfad nicht automatisch zurückrollen; Evidenzlücke markieren
Kein Prozentwert ohne Bedeutung: Eine Gleichheitsquote ist nur belastbar, wenn Sonderfälle repräsentiert und alle Abweichungsklassen fachlich erklärt sind.

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.

OperationsHandover.java
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.

CutoverEvidence.java
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

Schritt 01 Characterization Tests

Das beobachtbare Verhalten des alten Ticket-Services wird vor jeder Strukturänderung mit Tests eingefroren.

Warum dieser Schritt: Noch kein Pattern: Safety Net. Der Schritt verhindert unbeabsichtigte fachliche Änderungen.

Schritt 01/09Legacy Support

02 - Schritt 02: Use Case aus Controller extrahieren auf-/zuklappen

Schritt 02 Use Case aus Controller extrahieren

HTTP, Session, SQL und Geschäftsablauf werden getrennt. Der neue Anwendungsdienst erhält nur fachliche Eingaben.

Warum dieser Schritt: Extract Method und Extract Class reduzieren die erste große Verantwortungsvermischung.

Schritt 02/09Legacy Support

03 - Schritt 03: String-Arrays durch SupportTicket ersetzen auf-/zuklappen

Schritt 03 String-Arrays durch SupportTicket ersetzen

Das untypisierte Legacy-Datenformat wird an der Grenze in ein valides Fachobjekt übersetzt.

Warum dieser Schritt: Value Object und Rich Domain Model machen Status, Priorität und Zeit explizit.

Schritt 03/09Legacy Support

04 - Schritt 04: Repository Adapter einführen auf-/zuklappen

Schritt 04 Repository Adapter einführen

Der Use Case kennt keine SQL-Spalten und keine Legacy-DAO-Methoden mehr.

Warum dieser Schritt: Adapter + Repository kapseln Datenformat und Persistenzzugriff.

Schritt 04/09Legacy Support

05 - Schritt 05: Fachregeln als Specifications auf-/zuklappen

Schritt 05 Fachregeln als Specifications

Sofortbearbeitung und Schließbarkeit werden als benannte, kombinierbare Regeln formuliert.

Warum dieser Schritt: Specification ersetzt verstreute if-Bedingungen und SQL-Fragmente.

Schritt 05/09Legacy Support

06 - Schritt 06: Teamzuordnung als Strategy auf-/zuklappen

Schritt 06 Teamzuordnung als Strategy

Die Auswahl von Service Desk, Senior Support oder Major Incident ist austauschbar.

Warum dieser Schritt: Strategy ersetzt die wachsende Prioritäts- und Produkttyp-Verzweigung.

Schritt 06/09Legacy Support

07 - Schritt 07: Ports and Adapters für Benachrichtigungen auf-/zuklappen

Schritt 07 Ports and Adapters für Benachrichtigungen

SMTP, Chat und Ticketing-Webhooks liegen hinter einem fachlichen Ausgangsport.

Warum dieser Schritt: Ports and Adapters hält Infrastruktur außerhalb des Anwendungsfalls.

Schritt 07/09Legacy Support

08 - Schritt 08: Domain Events für Folgeaktionen auf-/zuklappen

Schritt 08 Domain Events für Folgeaktionen

Zuweisung und Lösung veröffentlichen Ereignisse; Audit und Benachrichtigung reagieren getrennt.

Warum dieser Schritt: Observer / Domain Events entkoppelt Nebenwirkungen.

Schritt 08/09Legacy Support

09 - Schritt 09: Strangler Facade für schrittweise Migration auf-/zuklappen

Schritt 09 Strangler Facade für schrittweise Migration

Alte Aufrufer bleiben stabil, während einzelne Aktionen kontrolliert an den neuen Kern delegiert werden.

Warum dieser Schritt: Strangler Pattern + Facade ermöglichen Migration ohne Big Bang.

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.

SupportFacade.java
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

Legacy-Modernisierungsinventar

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üffeldLeitfrageNachweis
Fachliche KritikalitätWelche Geschäftsentscheidung darf nicht ausfallen oder unbemerkt abweichen?Prozesskennzahl, SLO, Compliance-Vorgabe
ÄnderungsdruckWo häufen sich fachliche Änderungen, Hotfixes oder Sonderfälle?Change-Historie, Incident- und Ticketdaten
KopplungWelche Aufrufer, Tabellen, Batchläufe und Fremdsysteme hängen tatsächlich am Pfad?Schritttime-Traces, SQL- und Schnittstelleninventar
BeobachtbarkeitKann aktuelles Verhalten reproduzierbar verglichen werden?Characterization Tests, Golden Master, Telemetrie
EigentumWer entscheidet über Regeln, Daten und Abschaltkriterien?benannte fachliche und technische Verantwortliche
Qualitätsregel: Eine Modernisierungsmaßnahme wird erst priorisiert, wenn Nutzen, Risiko, Beweisführung und Rückfallweg gemeinsam dokumentiert sind.

12 - Entscheidungsbaum: stabilisieren, kapseln, Strangler oder Rewrite

Entscheidungsbaum Legacy-Modernisierung

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.

ModernizationDecision.java
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-Landkarte
Anti-PatternWarum es scheitertGegenmaßnahme
Big-Bang RewriteFeedback kommt spät; Alt- und Neulogik driften lange unbemerkt auseinander.Vertikale Migrationsscheiben, Kohortenrouting, Differential Tests und klare Abschaltkriterien.
Shared Database ForeverNeue Module bleiben über Tabellen, Trigger und implizite Schreibrechte gekoppelt.Datenbesitz benennen, Schnittstellenvertrag einführen, Expand/Contract und Reconciliation verwenden.
Distributed MonolithServices 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 EndeAlt und Neu werden dauerhaft parallel betrieben; Kosten und Abweichungen steigen.Owner, Enddatum, Qualitätsmetriken und automatisierbare Abschaltbedingungen festlegen.
Framework-first ModernisierungTechnologie ä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

Domänenwissen 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.

  1. Repräsentative Geschäftsfälle und Ausnahmen aus Produktion, Support und Fachbereich sammeln.
  2. Beobachtetes Verhalten mit Characterization Tests und kanonischen Beispieldaten sichern.
  3. Regeln nach fachlicher Absicht, historischer Kompatibilität und rein technischem Workaround klassifizieren.
  4. Entscheidungstabellen und Zustandsmodelle gemeinsam mit benannten Verantwortlichen freigeben.
  5. Nur bestätigte Regeln in den neuen Kern übernehmen; Altkompatibilität bewusst am Adapterrand halten.
Domänenregel: Ein Codepfad gilt erst als verstanden, wenn erklärt werden kann, welche fachliche Entscheidung er trifft, für wen sie gilt, auf welchen Daten sie beruht und wie eine Abweichung erkannt wird.
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.

Customer-Support-Refactoring-Pfad

01. Ausgangscode: Ein Ticketprozessor trägt zu viele Entscheidungen

Code-Evolution: Ticketprozessor → Statusmodell → Policies → Ports
8 sichtbare Codeblöcke und 9 Lernschritte; Rollen, SLA, Audit und Eskalation werden einzeln sichtbar.
Einzigartiger Fokus: Status, Rollen und SLA

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.

LegacyTicketProcessor.java - vollständiger Ausgangspunkt
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;
    }
}
BeobachtungRisikoNachweis
Status als Stringunzulässige ÜbergängeÜbergangstests
Rolle in if-Kaskadeverteilte BerechtigungRollenfälle charakterisieren
SLA und Team gekoppeltRegeländerung verändert AblaufFristen pro Kategorie sichern
Audit direkt geschriebenpartielle SeiteneffekteReihenfolge 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.

CustomerSupportStoryTest.java - Characterization und Zieltests
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.

SupportTicket.java - Aggregate Root und State-Regeln
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; }
}
Pattern: State wird hier bewusst als explizite Zustandsmaschine im Aggregat umgesetzt, nicht als Klasse pro Zustand. Die Anzahl der Übergänge ist überschaubar.

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.

AssignmentPolicy.java
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) {}
}
SlaPolicy.java
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.

Ports für Repository, Audit und Benachrichtigung
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.

SupportTicketService.java
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) {}
}
SchrittOwnerFehlerstrategie
Team/SLA entscheidenPolicydeterministisch erneut berechenbar
Status ändernAggregateungültigen Übergang ablehnen
SpeichernRepository-PortTransaktion/Rollback
Audit/NotificationOutput-Portsidempotente 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.

EscalationService.java
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.

Betriebsregel: Eine Benachrichtigung darf wiederholt werden können, ohne einen zweiten Statusübergang auszulösen. Audit und Ticketzustand bleiben die führende Evidenz.

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.

CustomerSupportIntermediate.java
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

Änderungsdiff
- 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);
Was sich fachlich ändert: Verantwortung verschoben: Status, Team und SLA werden nicht mehr nebenbei gesetzt, sondern durch benannte Fachtypen und Policies entschieden.

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
Vergleichsregel: Dieser Zwischenstand muss separat kompilierbar und gegenüber Ausgangs- sowie Endzustand fachlich erklärbar bleiben.
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.

Große Fallstudie · 7 Schritte

Document Processing Refactoring Workbench

Von Typcodes, binären Payloads und direkter Archivierung zu einer formatneutralen, testbaren Verarbeitungspipeline. Jeder Schritt besitzt eine kompakte themenspezifische SVG.

Fallstudie: 7 von 7 Schritte abgeschlossen

Value Object, Strategy, Factory Registry, Specification, Pipeline, Ports and Adapters und Domain Events.

Bereiche offen: 0 von 0

01 - Schritt 01: Characterization Tests auf-/zuklappen

Document Schritt 1: Characterization Tests

Das Verhalten des alten Prozessors wird mit repräsentativen JSON-, CSV- und Fehlerfällen eingefroren.

Warum dieser Schritt: Safety Net vor Strukturänderungen; noch kein Pattern.

Schritt 01/07Document Processing

02 - Schritt 02: DocumentEnvelope statt String und byte[] auf-/zuklappen

Document Schritt 2: DocumentEnvelope statt String und byte[]

Format, Status, Empfangszeit und Payload werden in einem validierten Fachobjekt gebündelt.

Warum dieser Schritt: Replace Data Value with Object / Value Object.

Schritt 02/07Document Processing

03 - Schritt 03: DocumentReader als Strategy auf-/zuklappen

Document Schritt 3: DocumentReader als Strategy

Formatspezifisches Lesen wandert aus der switch-Kaskade in austauschbare Reader.

Warum dieser Schritt: Strategy trennt Varianten vom Ablauf.

Schritt 03/07Document Processing

04 - Schritt 04: Reader Registry als Factory auf-/zuklappen

Document Schritt 4: Reader Registry als Factory

Die Auswahl des passenden Readers erfolgt zentral anhand des Dokumentformats.

Warum dieser Schritt: Factory/Registry verhindert verstreute Konstruktion und Typprüfung.

Schritt 04/07Document Processing

05 - Schritt 05: Validierung als Specification auf-/zuklappen

Document Schritt 5: Validierung als Specification

Inhalts- und Größenregeln werden benannt, kombiniert und separat getestet.

Warum dieser Schritt: Specification ersetzt schwer lesbare boolesche Bedingungen.

Schritt 05/07Document Processing

06 - Schritt 06: Verarbeitung als Pipeline auf-/zuklappen

Document Schritt 6: Verarbeitung als Pipeline

Lesen, Validieren, Transformieren und Archivieren werden als klarer Ablauf sichtbar.

Warum dieser Schritt: Pipeline / Chain of Responsibility reduziert den Monolithen.

Schritt 06/07Document Processing

07 - Schritt 07: ArchivePort und Domain Events auf-/zuklappen

Document Schritt 7: ArchivePort und Domain Events

Das Archiv wird über einen Port angesprochen; Erfolg und Ablehnung werden als Ereignisse veröffentlicht.

Warum dieser Schritt: Ports and Adapters + Observer/Domain Events entkoppeln Infrastruktur und Nebenwirkungen.

Schritt 07/07Document Processing

DocumentProcessingFacade.java
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

Code-Evolution: Batch-Monolith → Fachtypen → Ports → Recovery
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.

Durchgehender Lehrbuchpfad des Dokumentenbatch-Refactorings

So liest du diese Fallstudie

Einzigartiger Fokus: Wiederanlauf und Verarbeitungsgarantie

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

Offenes Risiko: Ein Neustart kann Datensätze doppelt verarbeiten, überspringen oder einen zu frühen Checkpoint übernehmen.

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?“

LegacyBatchProcessor.java
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;
    }
}
Refactoring-Pfad vom Legacy-Batch zum restartfähigen Ablauf

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

Gewonnene Zusage: Jeder Datensatz besitzt eine stabile Identität und jedes Ergebnis ist verarbeitet, übersprungen oder fehlgeschlagen.

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.

BatchDocument.java
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(); }
}
BatchItemResult.java
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

Gewonnene Zusage: Checkpoint, Zielsystem und Fehlerjournal können unabhängig simuliert und betrieblich ausgetauscht werden.

Checkpoint-Speicher, Dokumentziel und Fehlerjournal werden als schmale Ports extrahiert. Jeder Port entspricht einem konkreten Test- oder Betriebsbedarf.

BatchPorts.java
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

Gewonnene Zusage: Fachlich unzulässige Dokumente werden begründet übersprungen, technische Fehler bleiben Wiederanlaufprobleme.

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.

DocumentValidationPolicy.java
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

Zentrale Invariante: Ein Checkpoint darf niemals einen Datensatz bestätigen, dessen fachliche Wirkung nicht bestätigt wurde.

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.

RefactoredDocumentBatchProcessor.java
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(); }
    }
}
Warum kein „exactly once“? Ohne gemeinsame atomare Infrastruktur wäre die Aussage irreführend. Der gezeigte Vertrag ist At-least-once plus idempotenter Sink.

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. Endzustand
var 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

Gewonnene Zusage: Temporäre technische Fehler werden durch Restart behandelt; dauerhaft nicht verarbeitbare Dokumente erhalten einen eigenen nachvollziehbaren Nachbearbeitungspfad.

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.

BatchRecoveryDecision.java
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");
        };
    }
}
FehlerartBehandlungCheckpoint
temporärer Sink-Ausfallnächster Lauf startet am letzten bestätigten Indexunverändert
fachlich korrigierbarer Inhaltmanuelle Korrektur und gezielte Wiedereinspeisungbewusst nicht automatisch vorziehen
dauerhaft unbekanntes FormatDead Letter mit Ursache und Originalpayloadnach 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.

VorherNachher
Typcodes, Map-Speicher, versteckte FortschrittslogikFachtypen, explizite Ports, begründeter Checkpoint
Fehler werden geloggt und weitergelaufenSkip und technischer Fehler getrennt
Neustartverhalten unklarAt-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.

DocumentBatchIntermediate.java
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

Änderungsdiff
- checkpointStore.save(document.offset());
- documentRepository.insert(document);
+ if (!documentRepository.exists(document.id())) {
+     documentRepository.save(document);
+ }
+ checkpointStore.advanceTo(document.offset());
Was sich fachlich ändert: Verantwortung verschoben: Der Checkpoint bestätigt nur noch bereits dauerhaft verarbeitete Daten. Wiederholungen bleiben durch Idempotenz ungefährlich.

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
Vergleichsregel: Dieser Zwischenstand muss separat kompilierbar und gegenüber Ausgangs- sowie Endzustand fachlich erklärbar bleiben.
Kapitel 16 Enterprise-Fallstudie: Legacy-SOAP-Integration13 Lernschritte

Ausgangssystem

Code-Evolution: Legacy-Client → Port → Adapter/ACL → Shadow
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.

Durchgehender Lehrbuchpfad der SOAP-Modernisierung

So liest du diese Fallstudie

Einzigartiger Fokus: externer Vertrag und Fehlersemantik

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.

SOAP-Legacy-Sicherheitsnetz

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.

VertragszwangRisikoIm Sicherheitsnetz fixierter Nachweis
Statuscode 00Grenzwert 75 könnte unbemerkt verändert werdenScore 23 bleibt APPROVED, Score 75 bleibt MANUAL_REVIEW
Statuscode B12fachliche Ablehnung könnte als technischer Fehler behandelt werdenREJECTED und nicht wiederholbar
TimeoutRetry-Fähigkeit und Status könnten verloren gehenTECHNICAL_REVIEW, Code T01, retryable=true
Request-MappingTrimmen, Länderformat, Betrag und Zeit könnten driftenvollstä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.

LegacyCustomerRiskSoapClient.java
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

Checkpoint: Noch keine Zielarchitektur.Der Legacy-Client bleibt produktiv. Der einzige Fortschritt ist, dass Ergebnis, Audit, Status und Request-Mapping reproduzierbar geprüft werden können.

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.

LegacyCustomerRiskSoapClientCharacterizationTest.java
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.

CustomerRiskPort.java
@FunctionalInterface
public interface CustomerRiskPort {
    RiskAssessment assess(CustomerRiskQuery query);
}

Schritt 5 – SOAP-Vertrag mit Adapter und ACL einkapseln

Checkpoint: Die Domäne kennt keine generierten Klassen mehr.Der Anwendungscode spricht nur noch den fachlichen Port; WSDL-Typen, Partnercodes und Null-Konventionen bleiben im Adapter.

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.

SOAP-Port, Adapter und Anti-Corruption Layer
SoapCustomerRiskAdapter.java
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 SituationInternes ErgebnisRetry
00 mit ScoreAPPROVED oder MANUAL_REVIEWnein
B12REJECTEDnein
TimeoutTECHNICAL_REVIEW / T01ja
TransportfehlerTECHNICAL_REVIEW / T99noch nein
fehlender StatusTECHNICAL_REVIEW / T98nein
unbekannter CodeTECHNICAL_REVIEW / Originalcodenein

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

Checkpoint: Lokale Entkopplung ist abgeschlossen.Erst jetzt ist der Code stabil genug, um Retry, Idempotenz und Parallelbetrieb hinzuzufügen, ohne Transport und Fachlogik erneut zu vermischen.

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. Migration
var 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.

RetryingCustomerRiskPort.java
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.

IdempotentCustomerRiskPort.java
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.

SOAP-Migration mit Retry, Idempotenz und Shadow-Vergleich
ShadowComparingCustomerRiskPort.java
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

Checkpoint: Strukturqualität allein reicht nicht.Der neue Pfad wird erst führend, wenn fachliche Gleichheit, technische Stabilität und betriebliche Verantwortlichkeit nachgewiesen sind.

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.

KriteriumBeispiel für FreigabeAbbruchsignal
fachliche Gleichheiterklärte Abweichungsquote unter vereinbartem Grenzwertunbekannte Abweichungen bei Ablehnung oder Freigabe
technische StabilitätTimeout- und Fehlerquote innerhalb SLORetry-Sturm oder erhöhte Partnerlast
Idempotenzkeine doppelten wirksamen Aufrufemehrfache Statusänderung oder Audit-Duplikate
BetriebDashboard, Alarmierung und Runbook verfügbarAbweichungen ohne verantwortlichen Owner

Schritt 13 – Kontrolliert umschalten und bewusst stoppen

Stopppunkt:Der externe SOAP-Vertrag muss nicht neu geschrieben werden. Das Refactoring endet, sobald die eigene Codebasis entkoppelt, testbar, fehlertolerant und sicher umschaltbar ist.
  1. Legacy-Pfad bleibt führend, neuer Adapter läuft als Shadow.
  2. Abweichungen werden klassifiziert und fachlich entschieden.
  3. Der neue Port wird für einen begrenzten Anteil führend; Rückschaltung bleibt möglich.
  4. Nach stabiler Beobachtung wird der alte SOAP-Client aus dem Anwendungscode entfernt.
  5. 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.

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.

SoapIntermediate.java
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

Änderungsdiff
- 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);
+ };
Was sich fachlich ändert: Verantwortung verschoben: WSDL-Typen und Partnercodes bleiben im Adapter. Der Anwendungscode arbeitet nur noch mit einem stabilen fachlichen Ergebnis.

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
Vergleichsregel: Dieser Zwischenstand muss separat kompilierbar und gegenüber Ausgangs- sowie Endzustand fachlich erklärbar bleiben.
Buchteil 4

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
Code-Evolution: Rohdaten → Fachtypen → Canonicalizer → Pipeline
9 sichtbare Codeblöcke; Profiling, Mapping, Quarantäne und Reconciliation sind getrennt nachvollziehbar.
Einzigartiger Fokus: Datenbedeutung und Nachweis

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.

Datenmodernisierung als schrittweiser Lernpfad

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.

Schritt 0 · Reales Problem

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.

Legacy-Export – dieselben Daten begleiten uns durch das Kapitel
-- 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.1999

Mehrdeutiger 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.

Erster Stopppunkt: Solange Codes und Dubletten nicht erklärt sind, wird kein produktiver Zielbestand geschrieben.
Schritt 1 · Verhalten beobachten

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.

LegacyCustomerProfile.java
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.

Schritt 2 · Bedeutung explizit machen

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.

CustomerStatus.java
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);
        };
    }
}
EmailAddress.java
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;
    }
}
Warum kein Default? Ein unbekanntes X automatisch als INACTIVE zu speichern wäre technisch bequem, aber fachlich unbelegt. Deshalb wird die Zeile abgewiesen und mit einem Diagnosecode dokumentiert.
Schritt 3 · Kanonisches Modell

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.

CanonicalCustomer.java
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);
    }
}
Vorher

Lose Strings, implizite Codes, negative Beträge und Nullwerte.

Nachher

CustomerId, EmailAddress, CustomerStatus und Money schützen die Domäne.

Schritt 4 · Übersetzung mit Beweis

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.

CustomerCanonicalizer.java – zentraler Ablauf
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.

Schritt 5 · Kontrollierte Übertragung

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.

CustomerMigrationPipeline.java
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.

Schritt 6 · Fachlicher Abgleich

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.

CustomerReconciliation.java
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.

Schritt 7 · Tests als Migrationsvertrag

Die wichtigsten Risiken werden ausführbar dokumentiert

Die Tests beweisen zwei zentrale Eigenschaften: unbekannte Werte verschwinden nicht und Wiederholungen bleiben idempotent.

CustomerMigrationPipelineTest.java – Vertragsausschnitt
@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);
}
Abbruchregel: Der Cutover wird gestoppt, sobald ungeklärte Statuswerte, nicht erklärbare Saldenabweichungen oder nicht-idempotente Wiederholungen auftreten.
Endzustand · Bewusster Stopppunkt

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.

DataModernizationIntermediate.java
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

Änderungsdiff
- 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());
Was sich fachlich ändert: Verantwortung verschoben: Die Persistenz entscheidet nicht mehr über Datenbedeutung. Normalisierung und Quarantäne liegen vor dem Schreibzugriff.

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
Vergleichsregel: Dieser Zwischenstand muss separat kompilierbar und gegenüber Ausgangs- sowie Endzustand fachlich erklärbar bleiben.
Kapitel 18 Schnittstellenmigration: Vertrag, Adapter und Routing getrennt steuernModernisierung
Schnittstellenmigration über Adapter und Routing-Fassade

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.
MigrationRouter.java
// 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 mit Shadow Traffic und fachlichem Vergleich

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.

ModusNutzenHauptrisiko
ShadowNeupfad unter Realverkehr beobachtenverdeckte Nebenwirkungen
Dual Readfachliche Ergebnisse vergleichenunterschiedliche Zeitstände
Dual Writebeide Datenwelten aktuell haltenpartielle 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
Abschaltkriterien für Daten, Betrieb und Fachlichkeit

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.

GateBeispielkriteriumNachweis
Daten100 % der erwarteten Schlüssel vorhanden; alle Deltas geklärtReconciliation-Bericht und signierte Ausnahme-Liste
Fachlichkeitkeine unerklärte Abweichung bei Entscheidungen und SaldenGolden-Master-Vergleich, Fachfreigabe
BetriebSLO, Fehlerbudget und Kapazität über mindestens zwei Spitzenperioden stabilDashboards, Lasttests, Incident-Historie
Consumerkeine produktive Nutzung des AltvertragsGateway-Telemetrie, Owner-Bestätigung
RollbackRückfallweg getestet oder bewusst geschlossenGame Day und Entscheidungsprotokoll
Definition of Done: Abschaltung umfasst Traffic-Stopp, Schreibschutz, Aufbewahrung, Audit-Export, Secret-Entzug, Job-Deaktivierung, Monitoring-Anpassung und dokumentierte Wiederanlaufgrenze.
Buchteil 5

Betrieb und Verantwortung

Produktionsreife entsteht durch Nachweis, Ownership, Rückfallfähigkeit und explizite Entscheidungsrechte.

Kapitel 21 Betriebsübernahme: Verantwortung beweisbar übergebenBetrieb
Verantwortung wird beweisbar übergeben

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.

AbnahmefeldVerbindlicher NachweisFreigabeverantwortung
Service Ownershipbenannter Service Owner, Stellvertretung und EskalationswegProdukt- und Betriebsverantwortung
RunbookDiagnose, Sofortmaßnahme, Rückfallweg und KommunikationsvorlageOperations/SRE
Supportfähigkeitrepräsentative Störung ohne Projektteam bearbeitetSupport Lead
Fachliche VerantwortungRegel-, Daten- und Ausnahmeentscheidungen eindeutig zugeordnetFachbereich/Data Owner
Restrisikenoffene Risiken mit Frist, Owner und akzeptierter Auswirkunggemeinsames Go-live-Gremium
Übergaberegel: Eine Dateiablage ist keine Betriebsübernahme. Freigegeben wird erst nach einer beobachteten Übung, in der das übernehmende Team Diagnose, Entscheidung und Kommunikation selbst ausführt.
Kapitel 22 Observability: technische Signale mit fachlichen Folgen verbindenBetrieb
technische Signale mit fachlichen Folgen verbinden

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.

EbeneBeispielsignalEntscheidungsnutzen
TechnikFehlerquote, Latenz, Queue-Alter, Thread- oder Connection-PoolEngpass und Fehlerdomäne eingrenzen
FachlichkeitAnteil abweichender Entscheidungen, offene Reconciliation-Deltas, manuelle NacharbeitGeschäftsauswirkung bewerten
BetriebTime to Detect, Time to Mitigate, Alarmqualität, FehlerbudgetBetriebsreife und Änderungsrisiko steuern
Datenfehlende Schlüssel, verspätete CDC-Ereignisse, SchemaabweichungDatenintegrität und Cutover-Sicherheit prüfen
SLO-Regel: Ein Dashboard gilt erst als vollständig, wenn jedes rote Signal eine benannte Handlung, einen Owner und einen fachlichen Auswirkungsindikator besitzt.
Kapitel 23 Rollback: Code, Daten, Prozess und Fachentscheidung getrennt behandelnBetrieb
Code, Daten, Prozess und Fachentscheidung getrennt behandeln

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.

EbeneBevorzugte StrategieNicht ausreichend, wenn
CodeBlue/Green, Canary-Abbruch oder vorheriges ArtefaktDatenformat oder externe Wirkung bereits verändert wurde
Konfigurationversionierte Konfiguration und geprüfter Safe DefaultAlt- und Neuversion unterschiedliche Semantik erwarten
DatenExpand/Contract, Restore-Punkt, Reconciliation und gezielte Reparaturnachfolgende legitime Änderungen überschrieben würden
ProzessRouting auf stabilen Pfad oder Feature ToggleVorgänge in beiden Welten teilweise fortgeschritten sind
Fachliche WirkungKompensation, Nachbearbeitung und transparente Kommunikationeine irreversible externe Entscheidung erfolgte
Entscheidungsregel: Bei unklarer Daten- oder Fachwirkung wird nicht blind zurückgerollt. Der Incident Lead friert weitere Änderungen ein, bewertet die Wirkungsfläche und entscheidet zwischen Rückschaltung, Kompensation und Forward Fix.
Kapitel 24 Game Days: Rückfallfähigkeit unter kontrolliertem Stress nachweisenBetrieb
Rückfallfähigkeit unter kontrolliertem Stress nachweisen

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.

SzenarioErwartete ReaktionErfolgskriterium
Neuer Dienst nicht erreichbarTraffic stoppen, Altpfad bewerten, Warteschlange sichernkeine verlorenen Vorgänge; Entscheidung innerhalb des RTO
Reconciliation driftetbetroffene Kohorte isolieren, Ursache klassifizieren, Cutover stoppenjede Abweichung besitzt Klasse und Owner
Events doppelt oder verspätetIdempotenz prüfen, Replay kontrollieren, Business-KPI beobachtenkeine doppelte fachliche Wirkung
Rollback scheitertChange Freeze, Incident Command, Forward Fix oder Kompensationklare Führungsrolle und dokumentierte Entscheidung
Owner nicht erreichbarStellvertretung und Eskalationskette aktivierenEntscheidung ohne Wissensmonopol möglich
Nachbereitungsregel: Jede Übung endet mit messbaren Findings, verantwortlichen Personen und Fristen. Ein wiederkehrender Befund ohne Maßnahme ist ein akzeptiertes Produktionsrisiko und muss entsprechend freigegeben werden.
Kapitel 25 Verantwortungsübergabe: Entscheidungen statt nur Aufgaben zuordnenGovernance
Entscheidungen, nicht nur Aufgaben zuordnen

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.

EntscheidungEvidenzlieferantEntscheiderAusführung
ProduktionsfreigabeEngineering, QA, SRE, FachbereichService OwnerRelease-Verantwortung
Akzeptanz fachlicher AbweichungReconciliation-Team und Data OwnerFachverantwortungDaten-/Anwendungsteam
Rollback oder Forward FixIncident-Analyse und OperationsIncident LeadSRE/Engineering
Abschaltung des Legacy-PfadsConsumer-, Daten- und SLO-NachweiseProdukt- und SystemverantwortungOperations
NotfallzugriffSecurity- und Betriebsnachweisbenannter Break-Glass-Approverprivilegierter Operator
Governance-Regel: Kritische Entscheidungen dürfen nicht an unbenannte Gruppen delegiert werden. Jede Rolle braucht eine erreichbare Person, Stellvertretung, zeitliche Gültigkeit und dokumentierte Entscheidungsgrenze.
Kapitel 26 Governance: Entscheidungsrechte und Leitplanken explizit machenGovernance
Entscheidungsrechte und Leitplanken explizit machen

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-EbeneVerbindliche RegelNachweis
ArchitekturDomänengrenzen und Integrationsprinzipien sind verbindlichArchitecture Fitness Functions und Review-Protokoll
DatenOwner, Qualitätsziel und Aufbewahrung sind benanntData Contract, Qualitätsbericht, Löschkonzept
BetriebSLO, Alarmreaktion und Rückfallweg sind freigegebenDashboard, Runbook und Game-Day-Evidenz
SecurityPrivilegien, Secrets und Auditierung folgen dem Least-Privilege-Prinzipautomatisierte Kontrollen und Freigabe
AusnahmenAbweichung besitzt Frist, Risiko, Owner und Rückbauplanzeitlich begrenzte Waiver-Entscheidung
Governance-Regel: Ein Gremium ersetzt keine Entscheidung. Für jede Leitplanke müssen Entscheider, prüfbare Evidenz, Eskalationsweg und Ablaufdatum einer Ausnahme sichtbar sein.
Kapitel 27 Architekturentscheidungen: ADRs als lebende EntscheidungsakteGovernance
ADRs als lebende Entscheidungsakte führen

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-BestandteilLeitfrageBeispiel
KontextWelche Kräfte und Einschränkungen wirken?Legacy-Consumer kann sechs Monate nicht migrieren
EntscheidungWas wird verbindlich getan?Anti-Corruption Layer vor dem neuen Domänenservice
AlternativenWelche realistischen Optionen wurden geprüft?Direktkopplung, Big-Bang-Ablösung, Event-Adapter
KonsequenzenWelche Vorteile, Kosten und Risiken entstehen?zusätzlicher Betrieb, aber entkoppelte Domänensprache
RevisionWann verliert die Entscheidung ihre Grundlage?letzter Legacy-Consumer abgeschaltet
ADR-Regel: Ein ADR ohne verworfene Alternativen und Revisionsauslöser ist nur eine Beschreibung. Eine belastbare Entscheidungsakte erklärt auch, wann sie ersetzt oder aufgehoben werden muss.
Kapitel 28 Technische Schulden: Geschäftswirkung, Tilgung und Zins sichtbar machenGovernance
Geschäftswirkung, Tilgung und Zins sichtbar machen

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.

SchuldenklasseMessbarer ZinsTilgungsstrategie
KopplungÄnderungen benötigen mehrere Teams und ReleasesSchnittstelle stabilisieren und Verantwortungsgrenze schneiden
Testdefizitlange Regression und hohe NacharbeitsquoteCharacterization Tests und risikobasierte Verträge
Datenaltlastmanuelle Korrekturen und Reconciliation-Deltaskanonisches Modell, Qualitätsregeln und schrittweise Bereinigung
Betriebsaltlastlange Diagnose- und WiederherstellungszeitObservability, Runbooks und automatisierte Recovery
Übergangslösungdoppelte Kosten im ParallelbetriebExit-Kriterium, Owner und verbindlicher Abschalttermin
Priorisierungsregel: Schulden werden zuerst dort getilgt, wo ihr Zins eine strategische Änderung, ein SLO oder eine regulatorische Verpflichtung gefährdet - nicht dort, wo der Code lediglich unästhetisch wirkt.
Kapitel 29 Risiken, Abhängigkeiten und Modernisierungsmetriken steuernGovernance
Modernisierung als gerichtetes System steuern

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ängigkeitFrüher IndikatorSteuerungsmaßnahme
ConsumerNutzung unbekannt oder keine MigrationszusageTelemetry, Consumer-Inventar und Sunset-Vertrag
DatenOwner oder Qualitätsregel fehltData Owner benennen und Reconciliation vor Cutover
Organisationkritisches Wissen bei EinzelpersonPairing, Entscheidungsprotokoll und Stellvertretung
AnbieterReleasefenster oder proprietäre SchnittstelleAdapter, Exit-Klausel und getesteter Fallback
RegulatorikFreigabe erst am Projektende vorgesehenKontrollnachweise inkrementell erzeugen
Risikoregel: Ein Risiko ohne Frühindikator ist zu spät steuerbar. Jede kritische Abhängigkeit braucht ein beobachtbares Signal, einen Owner und eine konkrete Entkopplungs- oder Notfalloption.

Wirkung statt Aktivität messen

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.

PerspektiveWirkungsmetrikFehlinterpretation vermeiden
LieferfähigkeitLead Time für eine fachliche Regeländerungmehr Deployments bedeuten nicht automatisch mehr Nutzen
QualitätProduktionsfehler und manuelle Nacharbeit pro VorgangTestanzahl ersetzt keine Fehlerwirkung
BetriebMTTD, MTTR und verbrauchtes FehlerbudgetUptime ohne kritischen Geschäftspfad ist irreführend
DatenAnteil erklärter Reconciliation-Abweichungenreine Datensatzanzahl verschleiert fachliche Deltas
Wissenkritische Regeln mit Owner, Beispiel und TestvertragDokumentseiten messen keine Handlungsfähigkeit
Metrik-Regel: Jede Kennzahl braucht Ziel, Betrachtungszeitraum, Datenquelle und eine benannte Entscheidung. Eine Zahl, die keine Handlung verändert, ist Reporting - keine Steuerung.

Domänenmodell als gemeinsames Steuerungsobjekt

Begriffe, Regeln und Ausnahmefälle 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änenartefaktVerbindlicher InhaltQualitätsnachweis
Begriffsmodelleindeutige Begriffe und SynonymeBeispiele aus realen Geschäftsfällen
InvariantenRegeln, die in jedem gültigen Zustand geltenausführbare Tests und fachliche Freigabe
Lebenszykluserlaubte Zustände und Übergängevollständige Transition-Matrix
AusnahmenGrund, Gültigkeit und VerantwortlicherEntscheidungsakte und Ablaufdatum
Ownershipwer eine Regel ändern und freigeben darferreichbare Rolle mit Stellvertretung
Domänenregel: Eine Regel gilt erst als gesichert, wenn Bedeutung, Beispiel, Gegenbeispiel, Owner und ausführbarer Vertrag zusammen vorliegen. Code allein ist kein ausreichendes Wissensarchiv.
Buchteil A

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
Refactoring-Entscheidungsatlas

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.

Konsolidierter Refactoring-Entscheidungsatlas für Claims, Batch und SOAP

1. Gemeinsamer Startpunkt

  1. Beobachtbares Verhalten sichern: Characterization Tests, Golden Master, Vertragsbeispiele oder Restart-Szenarien.
  2. Größtes Risiko benennen: fachliche Fehlentscheidung, doppelte Verarbeitung oder Vertragsbruch.
  3. Kleinste kontrollierbare Grenze schaffen: Abhängigkeit, fachlicher Teilschritt, Port oder Checkpoint sichtbar machen.
  4. Erst danach abstrahieren: Pattern nur bei stabiler Variante; Architektur nur bei Systemgrenzen.
  5. Stopppunkt beweisen: Tests, Reconciliation, Metriken und Rückfallfähigkeit müssen den Nutzen belegen.

2. Risikotreiber und erste sinnvolle Eingriffe

FallstudiePrimäres RisikoSicherheitsnetzErster Eingriff
Claims-VerarbeitungFachliche Entscheidung ist mit Persistenz, Fremdsystem, Audit und Nachricht vermischt.Golden Master, Grenzwerte und Alt-/Neuvergleich.Abhängigkeiten sichtbar machen und Entscheidungsablauf fachlich benennen.
DokumentenbatchWiederanlauf 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-IntegrationWSDL-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?

EntscheidungsebeneClaimsBatchSOAP
Lokal refaktorierenLong Method zerlegen, Value Objects, Query/Command trennen.Verarbeitungsreihenfolge, Fehlerpfade und Fachtypen explizit machen.Mapping und Fehlerübersetzung aus dem Client lösen.
Pattern-SchwelleStrategy/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-SchwelleOutbox 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

FallstudieBewusster StopppunktNicht automatisch ergänzen
ClaimsEntscheidung rein testbar, Seiteneffekte über klare Ports, fachlicher Vergleich stabil.Microservices, generische Rule Engine oder universeller Event Bus.
BatchIdempotent, restartfähig, Checkpoint begründet und betrieblich messbar.Workflow-Engine oder generisches Batch-Framework ohne mehrere reale Nutzer.
SOAPWSDL hinter Adapter, Fehler explizit, Retry begrenzt, Umschaltung messbar.Circuit Breaker, REST-Neubau oder Messaging-Schicht ohne messbaren Bedarf.
Leitregel: Sicherheitsnetz vor Struktur, Struktur vor Pattern, Pattern vor Architektur. Der Risikotreiber entscheidet über die Reihenfolge - nicht die Beliebtheit einer Technik.

Claims-Fallstudie öffnen · Dokumentenbatch-Fallstudie öffnen · SOAP-Fallstudie öffnen

Anhang A.2 Symptom-zu-Vorgehen-MatrixProblemorientierung
Direkte Problemorientierung

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.

Vom beobachtbaren Symptom zum sicheren Refactoring-Vorgehen
Beobachtbares SymptomZuerst absichernErster sinnvoller SchrittDanach prüfenPassende Fallstudie
God Class mit Geschäftslogik, Persistenz und BenachrichtigungGrenzwerte, Entscheidungen und Reihenfolge der SeiteneffekteAbhängigkeiten sichtbar machen; fachlichen Ablauf benennenValue Objects, Query/Command-Trennung, Strategy/Policy nur bei stabilen VariantenClaims-Verarbeitung
Lange Methode mit vielen BedingungenCharacterization Tests für Haupt-, Grenz- und FehlerpfadeCompose Method: fachliche Teilschritte extrahieren, ohne Architektur vorwegzunehmenPrimitive Obsession und versteckte SeiteneffekteClaims-Verarbeitung
Unklar, ob Lesen und Ändern gemeinsam erfolgenbeobachtbare Rückgabewerte und alle ausgelösten ÄnderungenQuery von Modifier trennenTransaktionsgrenze und Outbox erst bei echter Seiteneffekt-GrenzeClaims-Verarbeitung
Batch verarbeitet nach Neustart doppelt oder überspringt DatensätzeRestart-Szenarien an mehreren FehlerpositionenReader, Validierung, Speicherung und Checkpoint-Reihenfolge explizit machenIdempotenzschlüssel und atomare Commit-/Checkpoint-GrenzeDokumentenbatch
Checkpoint wird gesetzt, obwohl Speicherung fehlschlägtFehler zwischen Store und Checkpoint reproduzierenBestätigungsreihenfolge korrigieren und Vertrag testenlokale Transaktion; Framework erst bei mehreren realen BatchvariantenDokumentenbatch
Versteckte Netzabhängigkeit oder generierte SOAP-Typen in der DomäneRequest-Mapping, Antwortcodes, Timeouts und fachliche Ablehnungenfachlichen Port einführen; Transport im Adapter kapselnAnti-Corruption Layer und explizite FehlerübersetzungSOAP-Integration
Retry wiederholt auch fachliche FehlerFehlerklassen und Wiederholbarkeit pro FehlerartRetry-Entscheidung aus dem Transport lösenDecorator nur für explizit retrybare technische Fehler; Backoff und Lastwirkung messenSOAP-Integration
Alt- und Neupfad liefern unerklärte Abweichungenfachliche Vergleichsmerkmale und repräsentative SonderfälleShadow-Betrieb ohne Nebenwirkung und Reconciliation einführenUmschalt- und Abbruchkriterien; Primärpfad bis zum Nachweis beibehaltenSOAP-Integration
Viele Switches ändern sich gemeinsamwelche Varianten wirklich stabil und fachlich verschieden sindBedingungen benennen und lokal zerlegenStrategy/State erst bei wiederkehrenden stabilen VariantenClaims-Verarbeitung
Unklare Transaktionsgrenze über Datenbank, Nachricht und Fremdsystemwelche Wirkungen atomar sein müssen und welche wiederholbar sindfachliche Entscheidung von technischen Wirkungen trennenOutbox, Saga oder Kompensation nur bei nachgewiesener SystemgrenzeClaims-Verarbeitung

Kompakte Entscheidungsregel

  1. Symptom konkretisieren: Nicht "Code ist schlecht", sondern beobachtbare Fehlwirkung oder Änderungsrisiko benennen.
  2. Verhalten sichern: Der Test muss genau das größte Risiko festhalten.
  3. Klein lokal beginnen: Namen, Grenzen und Reihenfolge sichtbar machen.
  4. Abstraktion verdienen: Pattern erst bei stabiler Wiederholung, Architektur erst an einer echten Systemgrenze.
  5. Stoppen können: Wenn Risiko, Änderbarkeit und Betrieb messbar verbessert sind, ist mehr Struktur nicht automatisch besser.
Warnsignal: Kann ein Team den Nutzen eines Patterns nur mit zukünftigen Möglichkeiten begründen, ist die Pattern-Schwelle wahrscheinlich noch nicht erreicht.
Anhang A.3 Sechs durchgehende Enterprise-FallstudienFallstudien-Kompass
Einheitliche Leserführung

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 öffnen

Legacy-Modernisierung

Characterization Tests, Modernisierungsentscheidung, Strangler-Schnitt, Parallelbetrieb, Betriebsübergabe und Abschaltung.

Fallstudie öffnen

Claims Workbench

God Class, fachliche Typen, Regelisolierung, Transaktionsgrenzen und Alt-/Neuvergleich.

Fallstudie öffnen

Customer Support

Statuswechsel, Rollen, SLA, Audit, Eskalation, Policies und Ports.

Fallstudie öffnen

SOAP-Integration

Vertragszwänge, Port, Adapter, Anti-Corruption Layer, Fehlerübersetzung, Retry, Idempotenz und Shadow-Betrieb.

Fallstudie öffnen

Dokumentenbatch

Restart-Vertrag, Checkpoint-Reihenfolge, Idempotenz, Recovery und Dead Letter.

Fallstudie öffnen

Gemeinsames Kapitelmuster

SchrittLeitfrageNachweis
1. AusgangslageWelches reale Risiko steckt im bestehenden Code?konkreter Legacy-Code und beobachtbare Symptome
2. SicherheitsnetzWelches Verhalten darf sich nicht unbemerkt ändern?Characterization-, Golden-Master- oder Restart-Tests
3. Lokale StrukturWelcher kleinste Schritt erhöht Verständlichkeit und Testbarkeit?benannte Abläufe, sichtbare Abhängigkeiten, Fachtypen
4. Fachliche GrenzenWelche Regeln und Verantwortlichkeiten sind stabil?Policies, Ports und gezielte Patterns
5. BetriebsgrenzenWelche Seiteneffekte, Transaktionen und Rückfallpfade müssen kontrolliert werden?Idempotenz, Reconciliation, Checkpoints oder Outbox
6. StopppunktWann 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
Qualitätskompass für große Fallstudien

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.

FallstudieAusgangscodeSicherheitsnetzCode-EvolutionBetriebsnachweisStopppunkt
Datenmodernisierunginkonsistenter KundenexportProfiling und DatenbeispieleFachtypen, Canonicalizer, PipelineQuarantäne und Reconciliationnur belegte Regeln migrieren
Legacy-ModernisierungSupport-MonolithCharacterization TestsPort, Strangler, KohortenroutingParallelvergleich und BetriebsübergabeAbschaltung nur mit Evidenz
ClaimsGod Class mit SeiteneffektenGolden Master und GrenzwerteFachtypen, Policies, Engine, PortsTransaktionsgrenze und Alt-/Neuvergleichkein vorschneller Microservice-Schnitt
Customer Supportprozeduraler TicketprozessorStatus-, SLA- und RollenfälleAggregat, Policies, Ports, EskalationAudit und wiederholbare Benachrichtigungkein Workflow-Framework ohne Bedarf
SOAP-Integrationvertraglich gekoppelter Legacy-ClientXML-Verträge und Request-CapturePort, Adapter, ACL, FehlerübersetzungRetry, Idempotenz, Shadow und Reconciliationexternen Vertrag nicht unnötig neu schreiben
Dokumentenbatchnebenwirkungsreicher Datei-BatchRestart- und Checkpoint-TestsReader, Policy, Sink und CheckpointIdempotenz, Recovery und Dead Letterkein generisches Batch-Framework ohne Varianten
Leseregel: Ein Kapitel gilt erst als vollständig, wenn der Leser erklären kann, welches Risiko zuerst abgesichert wird, warum der nächste Refactoring-Schritt klein genug ist und woran der sinnvolle Endpunkt erkannt wird.
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.

FallstudieEinzigartiges LernzielAbsichtlich nicht vertieft
DatenmodernisierungMehrdeutige Quelldaten in belegbare Fachtypen, Quarantäne und Reconciliation überführen.Service-Schnitt oder Prozess-Orchestrierung.
Legacy-ModernisierungEine Modernisierungsstrategie anhand von Evidenz wählen und einen Strangler kontrolliert betreiben.Detaillierte Domänenmodellierung einzelner Geschäftsregeln.
ClaimsKomplexe Geschäftsentscheidung von Transaktion und Seiteneffekten trennen.Externe Vertragsmigration und Batch-Wiederanlauf.
Customer SupportStatusübergänge, Rollen, SLA, Eskalation und Audit als konsistentes Fachmodell koordinieren.Große Datenmigration oder technische Retry-Mechanik.
SOAPExternen Vertrag über Port, Adapter, ACL, Fehlerklassifikation und Shadow-Betrieb entkoppeln.Neuentwurf des Partnersystems oder unnötiger REST-Rewrite.
DokumentenbatchRestart, Checkpoint-Reihenfolge, Idempotenz, Recovery und Dead Letter korrekt beherrschen.Interaktive Workflow- oder UI-Modellierung.
Leseregel: Wiederholt sich eine Erklärung, verweist das Kapitel künftig auf den gemeinsamen Entscheidungsatlas. Im Kapitel selbst bleibt nur die fallspezifische Begründung.
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.

FallstudieVergleichbare EntwicklungsachseNachweis im KapitelLeseregel
DatenmodernisierungRohdaten → Fachtypen → Canonicalizer → MigrationspipelineProfiling, Quarantäne, Checkpoint und ReconciliationJeder Schritt reduziert eine konkrete Datenmehrdeutigkeit.
Legacy-ModernisierungMonolith → Port → Kohortenrouting → Strangler-BetriebCharacterization Tests, Parallelvergleich und AbschaltkriterienArchitekturänderungen folgen erst nach belegtem Verhalten.
ClaimsGod Class → Fachtypen → Policies → transaktionaler Application ServiceTests, getrennte Seiteneffekte und Alt-/NeuvergleichDie fachliche Entscheidung bleibt in jedem Stand erkennbar.
Customer SupportTicketprozessor → Statusmodell → Policies → Ports und EskalationStatus-, SLA-, Rollen- und AuditfälleJeder Stand macht genau eine bisher implizite Regel sichtbar.
SOAPLegacy-Client → Port → Adapter/ACL → Retry/Idempotenz/ShadowXML-Verträge, Fehlerklassen und ReconciliationTransportdetails verschwinden schrittweise aus dem Anwendungscode.
Dokumentenbatchprozeduraler Batch → Fachtypen → Ports → Recovery-EntscheidungRestart-, Checkpoint-, Idempotenz- und Dead-Letter-TestsDie Reihenfolge der Nebenwirkungen bleibt explizit prüfbar.
Qualitätsmaßstab: Ein Zwischenstand ist nur dann sinnvoll, wenn der Leser ihn mit dem vorherigen Stand vergleichen, die beabsichtigte Änderung benennen und den zugehörigen Test oder Betriebsnachweis finden kann. Reine Umbenennungen oder große Sprünge ohne Nachweis zählen nicht als eigener Lernschritt.
⌂ Cockpit