Legacy-Parallelbetriebs-Lab

29 Java-Dateien. 8 zentrale Dateien werden direkt mit echtem Quellcode und ihrem Zusammenspiel erklärt.

Zurück zu Code-Labs

Kapitel 13 · Legacy-Parallelbetrieb

Was dieses Lab zeigt

Betreibt alten und neuen Pfad kontrolliert nebeneinander. Feature Toggles, Shadow Mode, Parity-Vergleich und Fallback reduzieren das Risiko der Migration.

Lernziele

  • Shadow Traffic einsetzen
  • Ergebnisse auf Parität prüfen
  • Fallback kontrolliert auslösen

Technik und Schwerpunkte

JDK-Lab29 Java-Dateien1 Tests/RunnerBranch by AbstractionShadow ModeFeature Toggle
Echter Quellcode aus diesem Lab

Geführter Codepfad

Beim Parallelbetrieb entscheidet ein Router pro Auftrag zwischen Legacy, neuem Pfad und Vergleichsbetrieb. Parity-Events machen Abweichungen sichtbar, bevor Traffic vollständig umgeschaltet wird.

DemoMainRefactoringRouterInMemoryFeatureToggleServiceLegacyOrderProcessorRefactoredOrderProcessorParityReporterOrderProcessingWorkflowRun7CTestRunner
Lesereihenfolge der zentralen Klassen. Die Pfeile zeigen den didaktischen Weg durch den realen Quellcode, nicht zwingend jeden Laufzeitaufruf.
1. DemoMainStartet Legacy-, neuer und Shadow-/Parity-Betrieb mit denselben Requests.
2. RefactoringRouterWählt anhand ProcessingMode und FeatureToggle den auszuführenden Verarbeitungspfad.
3. InMemoryFeatureToggleServiceStellt die Umschaltentscheidung für definierte Kunden oder Szenarien bereit.
4. LegacyOrderProcessorRepräsentiert den weiterhin produktiven Altpfad.
5. RefactoredOrderProcessorFührt den neu strukturierten Bestellpfad aus.
6. ParityReporterVergleicht beide Ergebnisse und schreibt Abweichungen als ParityEvents.
7. OrderProcessingWorkflowBündelt den fachlichen Ablauf, den der neue Prozessor verwendet.
8. Run7CTestRunnerPrüft Routing, Shadow-Vergleich, Feature-Toggles und Parity-Berichte.

1. DemoMain

src/main/java/com/example/enterprise/run7c/DemoMain.java
Java-Datei öffnen
Rolle im Ablauf

Startet Legacy-, neuer und Shadow-/Parity-Betrieb mit denselben Requests.

Im Lesepfad folgt RefactoringRouter: Wählt anhand ProcessingMode und FeatureToggle den auszuführenden Verarbeitungspfad.

Typ
class DemoMain
Verwendet
RefactoringRouter, InMemoryFeatureToggleService, LegacyOrderProcessor, RefactoredOrderProcessor, ParityReporter
Verwendet von
Einstiege
main(String[] args)
package com.example.enterprise.run7c;

public final class DemoMain {
    public static void main(String[] args) {
        InMemoryOutbox outbox = new InMemoryOutbox();
        InMemoryOrderRepository repository = new InMemoryOrderRepository();
        PaymentPort payment = new FakePaymentAdapter();
        LegacyOrderProcessor legacy = new LegacyOrderProcessor(payment, repository, outbox);
        RefactoredOrderProcessor modern = new RefactoredOrderProcessor(new ValidationService(), new PricingService(), new TaxService(), new ShippingService(), payment, repository, outbox);
        InMemoryFeatureToggleService toggles = new InMemoryFeatureToggleService();
        toggles.setDefaultMode(ProcessingMode.SHADOW);
        ParityReporter reporter = new ParityReporter();
        RefactoringRouter router = new RefactoringRouter(legacy, modern, toggles, reporter);
        OrderResult result = router.process(TestData.standardOrder("REQ-7C"));
        System.out.println("RUN7C_DEMO_OK " + result.decision() + " parityEvents=" + reporter.events().size());
    }
}

2. RefactoringRouter

src/main/java/com/example/enterprise/run7c/RefactoringRouter.java
Java-Datei öffnen
Rolle im Ablauf

Wählt anhand ProcessingMode und FeatureToggle den auszuführenden Verarbeitungspfad.

Im Lesepfad folgt InMemoryFeatureToggleService: Stellt die Umschaltentscheidung für definierte Kunden oder Szenarien bereit.

Typ
class RefactoringRouter
Verwendet
LegacyOrderProcessor, RefactoredOrderProcessor, ParityReporter
Verwendet von
DemoMain, Run7CTestRunner
Einstiege
process(OrderRequest request)
Branch by Abstraction + Feature Toggle
package com.example.enterprise.run7c;

// Pattern: Branch by Abstraction + Feature Toggle - alter und neuer Pfad sind hinter einem stabilen Einstiegspunkt steuerbar.
public final class RefactoringRouter {
    private final LegacyOrderProcessor legacy;
    private final RefactoredOrderProcessor modern;
    private final FeatureToggleService toggles;
    private final ParityReporter parityReporter;

    public RefactoringRouter(LegacyOrderProcessor legacy, RefactoredOrderProcessor modern,
                             FeatureToggleService toggles, ParityReporter parityReporter) {
        this.legacy = legacy; this.modern = modern; this.toggles = toggles; this.parityReporter = parityReporter;
    }

    public OrderResult process(OrderRequest request) {
        ProcessingMode mode = toggles.modeFor(request.customer());
        return switch (mode) {
            case LEGACY -> legacy.process(request);
            case SHADOW -> shadow(request);
            case NEW_WITH_FALLBACK -> newWithFallback(request);
            case NEW -> modern.process(request);
        };
    }

    private OrderResult shadow(OrderRequest request) {
        OrderResult legacyResult = legacy.process(request);
        try {
            OrderResult modernResult = modern.process(request);
            parityReporter.compare(request, legacyResult, modernResult);
        } catch (RuntimeException ex) {
            parityReporter.compare(request, legacyResult, OrderResult.rejected("modern exception: " + ex.getClass().getSimpleName()));
        }
        return legacyResult;
    }

    private OrderResult newWithFallback(OrderRequest request) {
        try {
            return modern.process(request);
        } catch (RuntimeException ex) {
            return legacy.process(request);
        }
    }
}

3. InMemoryFeatureToggleService

src/main/java/com/example/enterprise/run7c/InMemoryFeatureToggleService.java
Java-Datei öffnen
Rolle im Ablauf

Stellt die Umschaltentscheidung für definierte Kunden oder Szenarien bereit.

Im Lesepfad folgt LegacyOrderProcessor: Repräsentiert den weiterhin produktiven Altpfad.

Typ
class InMemoryFeatureToggleServiceimplements FeatureToggleService
Verwendet
Verwendet von
DemoMain, Run7CTestRunner
Einstiege
setDefaultMode(ProcessingMode defaultMode), setModeForCustomer(String customerId, ProcessingMode mode)
package com.example.enterprise.run7c;

import java.util.HashMap;
import java.util.Map;

public final class InMemoryFeatureToggleService implements FeatureToggleService {
    private ProcessingMode defaultMode = ProcessingMode.LEGACY;
    private final Map<String, ProcessingMode> customerModes = new HashMap<>();
    public void setDefaultMode(ProcessingMode defaultMode) { this.defaultMode = defaultMode; }
    public void setModeForCustomer(String customerId, ProcessingMode mode) { customerModes.put(customerId, mode); }
    @Override public ProcessingMode modeFor(CustomerProfile customer) { return customerModes.getOrDefault(customer.customerId(), defaultMode); }
}

4. LegacyOrderProcessor

src/main/java/com/example/enterprise/run7c/LegacyOrderProcessor.java
Java-Datei öffnen
Rolle im Ablauf

Repräsentiert den weiterhin produktiven Altpfad.

Im Lesepfad folgt RefactoredOrderProcessor: Führt den neu strukturierten Bestellpfad aus.

Typ
class LegacyOrderProcessor
Verwendet
Verwendet von
DemoMain, RefactoringRouter, Run7CTestRunner
Einstiege
process(OrderRequest request)
package com.example.enterprise.run7c;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

// Legacy-Beispiel: bewusst gemischte Verantwortungen. Nicht als Zielarchitektur verwenden.
public final class LegacyOrderProcessor {
    private final PaymentPort payment;
    private final OrderRepository repository;
    private final OutboxPort outbox;

    public LegacyOrderProcessor(PaymentPort payment, OrderRepository repository, OutboxPort outbox) {
        this.payment = payment; this.repository = repository; this.outbox = outbox;
    }

    public OrderResult process(OrderRequest request) {
        List<String> messages = new ArrayList<>();
        if (request.customer().blocked()) return OrderResult.rejected("customer blocked");
        if (request.lines().isEmpty()) return OrderResult.rejected("empty order");

        Money net = Money.eur("0");
        Money tax = Money.eur("0");
        for (OrderLine line : request.lines()) {
            if (line.sku().startsWith("DISCONTINUED")) return OrderResult.rejected("discontinued sku " + line.sku());
            Money lineNet = line.netTotal();
            net = net.add(lineNet);
            tax = tax.add(lineNet.multiply(line.taxRate()));
        }

        if (request.customer().premium()) {
            net = net.subtract(net.multiply(new BigDecimal("0.05")));
            messages.add("premium discount applied");
        }
        Money shipping = switch (request.shippingMethod()) {
            case "EXPRESS" -> Money.eur("19.90");
            case "PICKUP" -> Money.eur("0.00");
            default -> request.customer().premium() ? Money.eur("0.00") : Money.eur("5.90");
        };
        Money grand = net.add(tax).add(shipping);
        PaymentReceipt receipt = payment.authorize(request.customer().customerId(), grand);
        if (!receipt.approved()) return OrderResult.rejected("payment declined: " + receipt.reason());
        String orderId = "ORD-" + request.requestId();
        OrderResult result = new OrderResult(OrderDecision.ACCEPTED, orderId, net, tax, shipping, grand, messages);
        repository.save(orderId, request, result);
        outbox.append(orderId, "OrderAccepted", "grandTotal=" + grand.amount());
        return result;
    }
}

5. RefactoredOrderProcessor

src/main/java/com/example/enterprise/run7c/RefactoredOrderProcessor.java
Java-Datei öffnen
Rolle im Ablauf

Führt den neu strukturierten Bestellpfad aus.

Im Lesepfad folgt ParityReporter: Vergleicht beide Ergebnisse und schreibt Abweichungen als ParityEvents.

Typ
class RefactoredOrderProcessor
Verwendet
OrderProcessingWorkflow
Verwendet von
DemoMain, RefactoringRouter, Run7CTestRunner
Einstiege
process(OrderRequest request)
Application Service
package com.example.enterprise.run7c;

// Pattern: Application Service - orchestriert den Use Case, ohne alle Fachregeln selbst zu besitzen.
public final class RefactoredOrderProcessor {
    private final ValidationService validation;
    private final PricingService pricing;
    private final TaxService taxService;
    private final ShippingService shippingService;
    private final PaymentPort payment;
    private final OrderRepository repository;
    private final OutboxPort outbox;

    public RefactoredOrderProcessor(ValidationService validation, PricingService pricing, TaxService taxService,
                                    ShippingService shippingService, PaymentPort payment, OrderRepository repository,
                                    OutboxPort outbox) {
        this.validation = validation; this.pricing = pricing; this.taxService = taxService; this.shippingService = shippingService;
        this.payment = payment; this.repository = repository; this.outbox = outbox;
    }

    public OrderResult process(OrderRequest request) {
        return new OrderProcessingWorkflow(validation, pricing, taxService, shippingService, payment, repository, outbox).execute(request);
    }
}

6. ParityReporter

src/main/java/com/example/enterprise/run7c/ParityReporter.java
Java-Datei öffnen
Rolle im Ablauf

Vergleicht beide Ergebnisse und schreibt Abweichungen als ParityEvents.

Im Lesepfad folgt OrderProcessingWorkflow: Bündelt den fachlichen Ablauf, den der neue Prozessor verwendet.

Typ
class ParityReporter
Verwendet
Verwendet von
DemoMain, RefactoringRouter, Run7CTestRunner
Einstiege
compare(OrderRequest request, OrderResult legacy, OrderResult modern), events()
Observer/Reporter
package com.example.enterprise.run7c;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

// Pattern: Observer/Reporter - Shadow-Ergebnisse werden gemeldet, ohne den Use Case zu blockieren.
public final class ParityReporter {
    private final List<ParityEvent> events = new ArrayList<>();
    public void compare(OrderRequest request, OrderResult legacy, OrderResult modern) {
        boolean same = legacy.decision() == modern.decision()
                && Objects.equals(legacy.grandTotal(), modern.grandTotal())
                && Objects.equals(legacy.messages(), modern.messages());
        events.add(new ParityEvent(request.requestId(), same, same ? "EXACT_MATCH" : "BUSINESS_DIFFERENCE",
                "legacy=" + legacy + " modern=" + modern));
    }
    public List<ParityEvent> events() { return List.copyOf(events); }
}

7. OrderProcessingWorkflow

src/main/java/com/example/enterprise/run7c/OrderProcessingWorkflow.java
Java-Datei öffnen
Rolle im Ablauf

Bündelt den fachlichen Ablauf, den der neue Prozessor verwendet.

Im Lesepfad folgt Run7CTestRunner: Prüft Routing, Shadow-Vergleich, Feature-Toggles und Parity-Berichte.

Typ
class OrderProcessingWorkflow
Verwendet
Verwendet von
RefactoredOrderProcessor
Einstiege
execute(OrderRequest request)
Method Object
package com.example.enterprise.run7c;

import java.util.ArrayList;
import java.util.List;

// Pattern: Method Object - zerlegt eine lange Methode in einen zustandsbehafteten, benannten Workflow.
public final class OrderProcessingWorkflow {
    private final ValidationService validation;
    private final PricingService pricing;
    private final TaxService taxService;
    private final ShippingService shippingService;
    private final PaymentPort payment;
    private final OrderRepository repository;
    private final OutboxPort outbox;

    private OrderRequest request;
    private Money net;
    private Money tax;
    private Money shipping;
    private Money grand;
    private final List<String> messages = new ArrayList<>();

    public OrderProcessingWorkflow(ValidationService validation, PricingService pricing, TaxService taxService,
                                   ShippingService shippingService, PaymentPort payment, OrderRepository repository,
                                   OutboxPort outbox) {
        this.validation = validation; this.pricing = pricing; this.taxService = taxService; this.shippingService = shippingService;
        this.payment = payment; this.repository = repository; this.outbox = outbox;
    }

    public OrderResult execute(OrderRequest request) {
        this.request = request;
        var error = validation.validate(request);
        if (error.isPresent()) return OrderResult.rejected(error.get());
        calculateTotals();
        PaymentReceipt receipt = payment.authorize(request.customer().customerId(), grand);
        if (!receipt.approved()) return OrderResult.rejected("payment declined: " + receipt.reason());
        return persistAndPublish();
    }

    private void calculateTotals() {
        net = pricing.netTotal(request);
        tax = taxService.taxTotal(request);
        shipping = shippingService.shipping(request);
        if (request.customer().premium()) messages.add("premium discount applied");
        grand = net.add(tax).add(shipping);
    }

    private OrderResult persistAndPublish() {
        String orderId = "ORD-" + request.requestId();
        OrderResult result = new OrderResult(OrderDecision.ACCEPTED, orderId, net, tax, shipping, grand, List.copyOf(messages));
        repository.save(orderId, request, result);
        outbox.append(orderId, "OrderAccepted", "grandTotal=" + grand.amount());
        return result;
    }
}

8. Run7CTestRunner

src/main/java/com/example/enterprise/run7c/Run7CTestRunner.java
Java-Datei öffnen
Rolle im Ablauf

Prüft Routing, Shadow-Vergleich, Feature-Toggles und Parity-Berichte.

Damit ist der zentrale Pfad abgeschlossen; der Test-/Runner-Code und die vollständige Dateiliste darunter zeigen die übrigen Varianten.

Typ
class Run7CTestRunner
Verwendet
RefactoringRouter, InMemoryFeatureToggleService, LegacyOrderProcessor, RefactoredOrderProcessor, ParityReporter
Verwendet von
Einstiege
main(String[] args)
package com.example.enterprise.run7c;

public final class Run7CTestRunner {
    public static void main(String[] args) {
        shouldKeepParityInShadowMode();
        shouldRejectBlockedCustomerInBothPaths();
        shouldRouteToNewPathWhenEnabled();
        System.out.println("RUN7C_TESTS_OK");
    }
    private static void shouldKeepParityInShadowMode() {
        TestFixture fixture = TestFixture.create();
        fixture.toggles.setDefaultMode(ProcessingMode.SHADOW);
        OrderResult result = fixture.router.process(TestData.standardOrder("REQ-SHADOW"));
        assertTrue(result.accepted(), "legacy result should be accepted");
        assertEquals(1, fixture.reporter.events().size(), "shadow parity event expected");
        assertTrue(fixture.reporter.events().get(0).match(), "legacy and modern should match");
    }
    private static void shouldRejectBlockedCustomerInBothPaths() {
        TestFixture fixture = TestFixture.create();
        fixture.toggles.setDefaultMode(ProcessingMode.SHADOW);
        OrderResult result = fixture.router.process(TestData.blockedOrder());
        assertEquals(OrderDecision.REJECTED, result.decision(), "blocked customer rejected");
        assertTrue(fixture.reporter.events().get(0).match(), "blocked customer parity");
    }
    private static void shouldRouteToNewPathWhenEnabled() {
        TestFixture fixture = TestFixture.create();
        fixture.toggles.setDefaultMode(ProcessingMode.NEW);
        OrderResult result = fixture.router.process(TestData.standardOrder("REQ-NEW"));
        assertTrue(result.accepted(), "new path accepted");
        assertEquals(0, fixture.reporter.events().size(), "new mode does not shadow compare");
    }
    private static void assertTrue(boolean condition, String message) { if (!condition) throw new AssertionError(message); }
    private static void assertEquals(Object expected, Object actual, String message) { if (!java.util.Objects.equals(expected, actual)) throw new AssertionError(message + " expected=" + expected + " actual=" + actual); }
    private record TestFixture(RefactoringRouter router, InMemoryFeatureToggleService toggles, ParityReporter reporter) {
        static TestFixture create() {
            InMemoryOutbox outbox = new InMemoryOutbox();
            InMemoryOrderRepository repository = new InMemoryOrderRepository();
            PaymentPort payment = new FakePaymentAdapter();
            LegacyOrderProcessor legacy = new LegacyOrderProcessor(payment, repository, outbox);
            RefactoredOrderProcessor modern = new RefactoredOrderProcessor(new ValidationService(), new PricingService(), new TaxService(), new ShippingService(), payment, repository, outbox);
            InMemoryFeatureToggleService toggles = new InMemoryFeatureToggleService();
            ParityReporter reporter = new ParityReporter();
            return new TestFixture(new RefactoringRouter(legacy, modern, toggles, reporter), toggles, reporter);
        }
    }
}
Alle Projektdateien öffnen (31 Einträge)
⌂ Cockpit