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:
- Zustand:
OPENoderSUBMITTED. - erlaubte Aktion:
addLine()nur beiOPEN. - Invariante:
submit()verbietet eine leere Bestellung. - Kapselung: Die Liste wird nur unveränderlich herausgegeben.
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:
Order.open(...)– fachliches Objekt erzeugen.addLine(...)– Eingabe in Domain-Objekte übersetzen.pricingPolicy.calculateTotal(order)– austauschbare Regel aufrufen.order.submit()– Invariante auslösen.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:
- tatsächlichem Zugriff auf Alt-Daten oder Alt-APIs,
- Mapping zwischen Legacy- und Zielmodell,
- Characterization Tests mit konkreten Eingaben und Ausgaben,
- Vergleichslogik für Parallelbetrieb,
- Cutover- und Rollback-Zuständen,
- Audit- und Reconciliation-Daten.
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:
- Aggregate: Schützt die Klasse tatsächlich Zustände und Invarianten?
- Repository: Gibt es einen Port und mindestens eine Implementierung?
- Strategy: Kann eine Regel ausgetauscht werden?
- Facade: Verbirgt die Klasse mehrere Schritte hinter einer einfachen Operation?
- Pipeline: Werden Schritte in einer definierten Reihenfolge abgearbeitet?
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
- Ich habe zuerst das Workspace-POM gelesen.
- Ich kann die Modulabhängigkeiten erklären.
- Ich habe eine fachliche Invariante im Code gefunden.
- Ich kann Use Case, Port und Adapter unterscheiden.
- Ich kenne den ausführbaren Einstieg.
- Ich weiß, ob der Einstieg die Fachruntime oder nur einen Smoke-Flow startet.
- Ich habe konkrete Produktionslücken notiert.
- Ich habe Tests, Plattformdateien und Runbooks geprüft.