Home
Enterprise Maven MasterclassMaven Masterclass
Echte Projektartefakte

Code-Lesepfad

Vom Maven-Reaktor über Domain, Use Case, Port und Adapter bis zur ehrlichen Einordnung von Smoke und Produktion.

Dokument: Code-LesepfadZielgruppe: Java-/Maven-LernendeNiveau: PraxisStatus: geprüftStand: 10.07.2026

Dieser Lesepfad verwendet ausschließlich Dateien, die im aktuellen Paket vorhanden sind. Er zeigt nicht nur, wo du suchen sollst, sondern wie du aus konkretem Code Verantwortung, Laufzeit und Produktionsreife ableitest.

1. Starte nicht bei einer zufälligen Java-Klasse

Die zuverlässige Reihenfolge ist:

Workspace-POM
  -> Modul-POM
  -> ausführbarer Einstieg
  -> Application Use Case
  -> Domain-Regeln
  -> Port
  -> Adapter
  -> Runtime-Konfiguration
  -> Tests und Runbook

Damit vermeidest du zwei Fehler: Du hältst eine technische Adapterklasse nicht für Fachlogik, und du hältst eine Demo-Main nicht für die produktive Runtime.

2. Schritt 1: Maven-Reaktor als Architekturkarte

Beispiel Master 1, Version v01-modular-baseline:

Modul Verantwortung Abhängigkeit darf zeigen auf
order-domain Aggregate, Value Objects, Zustände möglichst nur Java-Basis
order-application Use Case und Ports Domain
order-adapter-memory technische Port-Implementierung Application + Domain
order-app Spring-REST-Einstieg Application + Adapter
runnable-smoke separat ausführbarer Lern-/Smoke-Flow eigene Demo-Struktur

Prüfung im Projekt: Öffne die jeweiligen pom.xml und vergleiche <dependencies> mit dieser Richtung. Eine Rückabhängigkeit von Domain auf REST oder Spring wäre ein Warnsignal.

3. Schritt 2: Die fachliche Invariante finden

Im echten Order-Aggregate steht die zentrale Regel:

package com.aydinsude.enterprise.v01.domain;

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

// PATTERN: Aggregate - Order protects consistency of lines and status.
public final class Order {
    private final OrderId id;
    private final List<OrderLine> lines = new ArrayList<>();
    private OrderStatus status = OrderStatus.OPEN;

    private Order(OrderId id) { this.id = id; }

    // PATTERN: Factory Method - valid aggregate root creation.
    public static Order open(OrderId id) { return new Order(id); }

    public OrderId id() { return id; }
    public OrderStatus status() { return status; }
    public List<OrderLine> lines() { return Collections.unmodifiableList(lines); }

    public void addLine(OrderLine line) {
        ensureOpen();
        lines.add(line);
    }

    public Money total() {
        return lines.stream().map(OrderLine::lineTotal).reduce(Money.eur("0.00"), Money::add);
    }

    public void submit() {
        ensureOpen();
        if (lines.isEmpty()) throw new IllegalStateException("Order must contain at least one line");
        status = OrderStatus.SUBMITTED;
    }

    private void ensureOpen() {
        if (status != OrderStatus.OPEN) throw new IllegalStateException("Order is not open");
    }
}

Beim Lesen markierst du:

Das ist wichtiger als die Frage, welches Framework verwendet wird.

4. Schritt 3: Den Use Case als Orchestrierung lesen

package com.aydinsude.enterprise.v01.application;

import com.aydinsude.enterprise.v01.domain.*;

// PATTERN: Application Service / Use Case - coordinates domain object and repository port.
public final class PlaceOrderUseCase {
    private final OrderRepository repository;
    private final PricingPolicy pricingPolicy;

    public PlaceOrderUseCase(OrderRepository repository, PricingPolicy pricingPolicy) {
        this.repository = repository;
        this.pricingPolicy = pricingPolicy;
    }

    public OrderId place(PlaceOrderCommand command) {
        Order order = Order.open(OrderId.newId());
        command.lines().forEach(line -> order.addLine(OrderLine.of(line.sku(), line.quantity(), Money.eur(line.unitPriceEur()))));
        pricingPolicy.calculateTotal(order); // explicit strategy extension point
        order.submit();
        repository.save(order);
        return order.id();
    }
}

Lies jede Zeile mit einer Verantwortung:

  1. Order.open(...) – fachliches Objekt erzeugen.
  2. addLine(...) – Eingabe in Domain-Objekte übersetzen.
  3. pricingPolicy.calculateTotal(order) – austauschbare Regel aufrufen.
  4. order.submit() – Invariante auslösen.
  5. repository.save(order) – über einen Port persistieren.

Der Use Case soll koordinieren, nicht HTTP parsen und nicht SQL formulieren.

5. Schritt 4: Port und Adapter als Paar lesen

package com.aydinsude.enterprise.v01.application;

import com.aydinsude.enterprise.v01.domain.Order;
import com.aydinsude.enterprise.v01.domain.OrderId;
import java.util.Optional;

// PATTERN: Repository Port - application defines persistence contract; adapter implements it.
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}
package com.aydinsude.enterprise.v01.adapter.memory;

import com.aydinsude.enterprise.v01.application.OrderRepository;
import com.aydinsude.enterprise.v01.domain.Order;
import com.aydinsude.enterprise.v01.domain.OrderId;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

// PATTERN: Adapter + Repository implementation - technical storage behind application port.
public final class InMemoryOrderRepository implements OrderRepository {
    private final Map<OrderId, Order> store = new ConcurrentHashMap<>();
    @Override public void save(Order order) { store.put(order.id(), order); }
    @Override public Optional<Order> findById(OrderId id) { return Optional.ofNullable(store.get(id)); }
}

Die Schnittstelle steht innen, die Implementierung außen. Die ConcurrentHashMap macht den Adapter lokal ausführbar und thread-sicher für einfache Zugriffe, ersetzt aber keine Transaktion, keine dauerhafte Speicherung und kein Datenmodell.

6. Schritt 5: Den Eingang zuletzt lesen

package com.aydinsude.enterprise.v01.app;

import com.aydinsude.enterprise.v01.application.PlaceOrderCommand;
import com.aydinsude.enterprise.v01.application.PlaceOrderUseCase;
import org.springframework.web.bind.annotation.*;
import java.util.Map;

// PATTERN: Adapter - REST DTO boundary maps HTTP to application command.
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
    private final PlaceOrderUseCase useCase;
    public OrderController(PlaceOrderUseCase useCase) { this.useCase = useCase; }
    @PostMapping
    public Map<String, String> place(@RequestBody PlaceOrderCommand command) {
        return Map.of("orderId", useCase.place(command).value().toString());
    }
}

Der REST-Controller ist dünn. Er nimmt ein Command entgegen, ruft den Use Case auf und formt die Antwort. Sobald Geschäftsregeln wie „Bestellung darf nicht leer sein“ im Controller auftauchen, wird die Regel schwerer wiederzuverwenden und zu testen.

7. Schritt 6: Smoke-Flow und Fachruntime unterscheiden

Im selben Master existiert ein runnable-smoke. Dessen RunnableSmokeApp modelliert eine eigene Pipeline mit ValidateOrderStep, ReserveInventoryStep, AuthorizePaymentStep, PublishOutboxStep und DeploymentReadinessStep.

Das ist wertvoll, weil der Ablauf direkt startbar und gut lesbar ist. Er verwendet aber Demo-Fakten in einer Map und führt weder den oben gezeigten OrderController noch eine echte Datenbank oder ein echtes Outbox-System aus. Lies Smoke und Fachruntime daher als zwei verschiedene Lernartefakte.

8. Fachprozess lesen: Master 8

package com.aydin.enterprise.master8.runnable;

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

public final class InsuranceProcessFacade {
    public InsuranceResult runDemo(Customer customer) {
        List<String> steps = new ArrayList<>();
        // PATTERN: Pipeline - Quote -> Underwriting -> Policy -> Billing -> Claim -> Payout.
        Quote quote = new QuoteService().createQuote(customer, new BigDecimal("125000.00"));
        steps.add("Quote erstellt: " + quote.quoteNumber());
        UnderwritingDecision decision = new UnderwritingService().decide(quote);
        steps.add("Underwriting: " + decision.status() + " / " + decision.reason());
        if (!decision.accepted()) return InsuranceResult.rejected("Underwriting abgelehnt", steps);
        Policy policy = new PolicyService().issuePolicy(quote, LocalDate.now());
        steps.add("Police erstellt: " + policy.policyNumber());
        Invoice invoice = new BillingService().createInvoice(policy);
        steps.add("Rechnung erstellt: " + invoice.invoiceNumber() + " Betrag=" + invoice.amount());
        PaymentReceipt payment = new PaymentService().settle(invoice);
        steps.add("Zahlung verbucht: " + payment.receiptNumber());
        Claim claim = new ClaimsService().registerClaim(policy, new BigDecimal("8500.00"), "Wasserschaden");
        steps.add("Schaden erfasst: " + claim.claimNumber());
        ClaimDecision claimDecision = new ClaimsService().assess(claim, policy);
        steps.add("Schadenentscheidung: " + claimDecision.status());
        Payout payout = new ClaimsService().payout(claimDecision);
        steps.add("Auszahlung: " + payout.payoutNumber() + " Betrag=" + payout.amount());
        ReinsuranceNote note = new ReinsuranceService().evaluate(policy, claimDecision);
        steps.add("Rueckversicherung: " + note.message());
        AuditTrail audit = new AuditTrail("TRACE-INS-8", steps);
        return InsuranceResult.accepted("Versicherungsablauf abgeschlossen", audit.events());
    }
}

Nutze beim Lesen vier Spalten:

Code-Schritt Fachliche Bedeutung Ergebnis Produktionsfrage
createQuote Angebot kalkulieren Quote + Prämie Tarifversion? Währung? Rundung?
decide Risikoentscheidung Annahme/Ablehnung Regelwerk? Erklärbarkeit?
issuePolicy Vertrag erzeugen Police Nummernkreis? Persistenz?
createInvoice / settle Forderung und Zahlung Rechnung/Beleg Zahlungsprovider? Idempotenz?
registerClaim / assess Schaden bearbeiten Entscheidung Dokumente? Betrugsprüfung?
payout Leistung auszahlen Auszahlung Freigaben? Bankintegration?

Die Tabelle entsteht aus dem realen Methodenfluss. Die Produktionsfragen sind Prüffragen, keine Behauptung, dass diese Funktionen bereits implementiert sind.

9. Legacy-Code lesen: Master 14

package com.aydin.enterprise.master14.demo;
import java.util.ArrayList;
import java.util.List;
public final class DemoProcessFacade {
    public DemoResult run() {
        // PATTERN: Pipeline - die fachlichen Schritte laufen kontrolliert nacheinander.
        List<String> steps = new ArrayList<>();
        for (String step : List.of("Legacy Read", "Characterization Test", "Strangler Facade", "Extract Domain", "Outbox Bridge", "Data Migration", "Parallel Run", "Cutover")) {
            steps.add("OK - " + step);
        }
        // PATTERN: Result Object - ein konsistentes Ergebnis fuer Smoke Test und Dokumentation.
        return DemoResult.accepted("Master 14 Legacy Refactoring and Migration System Demo erfolgreich", steps);
    }
}

Die Reihenfolge ist fachlich sinnvoll, der Code erzeugt aber nur Statuszeilen. Suche bei Legacy-Themen zusätzlich nach:

Fehlen diese Belege, ist das Modul eine Lern- oder Strukturvorlage.

10. End-to-End-Code lesen: Master 25

package com.aydin.master25.e2e;

import com.aydin.master25.contracts.CorrelationId;
import com.aydin.master25.contracts.E2eStep;
import java.util.ArrayList;
import java.util.List;

// PATTERN: Facade - offers one simple method for a complex cross-system flow.
public class E2eOrchestrator {
    public List<E2eStep> run(CorrelationId correlationId) {
        List<E2eStep> steps = new ArrayList<>();
        // PATTERN: Pipeline - each step enriches the same end-to-end business story.
        steps.add(new E2eStep("Portal", "create scenario", "scenario accepted " + correlationId.value()));
        steps.add(new E2eStep("Gateway", "route request", "correlation and idempotency checked"));
        steps.add(new E2eStep("Insurance", "issue policy", "policy demo created"));
        steps.add(new E2eStep("Banking", "capture payment", "ledger entry simulated"));
        steps.add(new E2eStep("ERP", "create invoice", "invoice demo created"));
        steps.add(new E2eStep("Logistics", "send document", "document shipment simulated"));
        steps.add(new E2eStep("Legacy", "compare old data", "anti-corruption mapping simulated"));
        steps.add(new E2eStep("Observability", "collect telemetry", "audit trail complete"));
        return steps;
    }
}

Die Klasse ist eine Facade und Pipeline. Sie erzeugt E2eStep-Objekte, ruft jedoch keine Clients oder Repositories auf. Deshalb lautet die korrekte Aussage: Der End-to-End-Ablauf wird lokal modelliert und ausgegeben. Nicht korrekt wäre: Die acht Systeme sind integriert.

11. Pattern-Kommentare richtig verwenden

Viele Klassen enthalten // PATTERN:. Nutze den Kommentar als Einstieg, prüfe danach aber die Struktur:

Ein Kommentar allein macht noch kein Pattern. Die Beziehungen im Code müssen dazu passen.

12. Praktische Suchbefehle

PowerShell

Get-ChildItem -Recurse -Filter pom.xml
Get-ChildItem -Recurse -Filter *.java | Select-String "PATTERN:"
Get-ChildItem -Recurse -Filter *.java | Select-String "class .*UseCase|interface .*Repository|class .*Controller"

Bash

find . -name pom.xml
find . -name '*.java' -print0 | xargs -0 grep -n 'PATTERN:'
find . -name '*.java' -print0 | xargs -0 grep -En 'class .*UseCase|interface .*Repository|class .*Controller'

13. Abschluss-Checkliste

⌂ Cockpit