Enterprise-Java-Plattform-Lab

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

Zurück zu Code-Labs

Kapitel 01 · Grundlagen bis Outbox

Was dieses Lab zeigt

Verbindet Java-Grundlagen mit einem fachlichen Order-Modell: Value Objects, Aggregate, Ports und Adapter, Transaktionsgrenze und Outbox werden in einem durchgängigen Lernprojekt sichtbar.

Lernziele

  • Fachliche Modelle stabil aufbauen
  • Use Case und Infrastruktur trennen
  • Order und Outbox konsistent speichern

Technik und Schwerpunkte

Java 2147 Java-Dateien1 Tests/RunnerDDDPorts & AdaptersOutbox
Echter Quellcode aus diesem Lab

Geführter Codepfad

Der Einstieg zeigt den vollständigen Weg eines Bestellauftrags: vom ausführbaren Szenario über den Application Service und das Aggregat bis zur gemeinsamen Ablage von Order und Outbox-Nachricht sowie der späteren Verarbeitung.

DemoAppOrderApplicationServiceOrderSimpleTransactionRunnerInMemoryOutboxRepositoryOutboxPublisherReportingConsumer
Lesereihenfolge der zentralen Klassen. Die Pfeile zeigen den didaktischen Weg durch den realen Quellcode, nicht zwingend jeden Laufzeitaufruf.
1. DemoAppStartet das reale Beispielszenario, verdrahtet Ports mit In-Memory-Adaptern und macht die Reihenfolge des Use Cases sichtbar.
2. OrderApplicationServiceOrchestriert den Place-Order-Use-Case und hält Kundenprüfung, Reservierung, Zahlung, Persistenz und Outbox innerhalb einer Transaktionsgrenze zusammen.
3. OrderKapselt den fachlichen Zustand der Bestellung, erzwingt Statusübergänge und sammelt Domain Events.
4. SimpleTransactionRunnerStellt die im Lab simulierte Transaktionsgrenze bereit, in der Fachzustand und Outbox konsistent verändert werden.
5. InMemoryOutboxRepositorySpeichert Outbox-Nachrichten und dient dem Publisher als Quelle noch nicht veröffentlichter Ereignisse.
6. OutboxPublisherLiest offene Outbox-Einträge, veröffentlicht sie über den EventPublisher und markiert erfolgreiche Zustellungen.
7. ReportingConsumerZeigt die nachgelagerte, idempotente Verarbeitung eines veröffentlichten Order-Events.

1. DemoApp

src/main/java/com/example/deepdive/DemoApp.java
Java-Datei öffnen
Rolle im Ablauf

Startet das reale Beispielszenario, verdrahtet Ports mit In-Memory-Adaptern und macht die Reihenfolge des Use Cases sichtbar.

Im Lesepfad folgt OrderApplicationService: Orchestriert den Place-Order-Use-Case und hält Kundenprüfung, Reservierung, Zahlung, Persistenz und Outbox innerhalb einer Transaktionsgrenze zusammen.

Typ
class DemoApp
Verwendet
OrderApplicationService, SimpleTransactionRunner, InMemoryOutboxRepository, OutboxPublisher
Verwendet von
Einstiege
main(String[] args)
package com.example.deepdive;
import com.example.deepdive.adapter.*;
import com.example.deepdive.application.*;
import com.example.deepdive.domain.*;
import com.example.deepdive.outbox.*;
import com.example.deepdive.shared.*;
import java.time.Clock;
import java.util.*;

public final class DemoApp {
    public static void main(String[] args) {
        var customers = new InMemoryCustomerRepository();
        var orders = new InMemoryOrderRepository();
        var inventory = new InMemoryInventoryAdapter();
        var payment = new FakePaymentAdapter();
        var outbox = new InMemoryOutboxRepository();
        var audit = new ConsoleAuditAdapter();
        customers.add(new Customer(new CustomerId("C-100"), "Ada Enterprise", false, CustomerSegment.VIP));
        inventory.put(new ProductId("P-1"), 10, Money.eur("19.90"));
        inventory.put(new ProductId("P-2"), 5, Money.eur("49.90"));
        var service = new OrderApplicationService(customers, orders, inventory, payment, outbox, audit, new SimpleTransactionRunner(), Clock.systemUTC());
        var result = service.placeOrder(new PlaceOrderCommand("cmd-1", new OrderId("O-1"), new CustomerId("C-100"), List.of(new PlaceOrderCommand.Line(new ProductId("P-1"), 2), new PlaceOrderCommand.Line(new ProductId("P-2"), 1))));
        System.out.println(result.isOk() ? "DEMO_OK " + result.orElseThrow() : result.errorOrNull().message());
        var publisher = new OutboxPublisher(outbox, msg -> System.out.println("PUBLISH " + msg.type() + " " + msg.id()));
        System.out.println("published=" + publisher.publishBatch());
    }
}

2. OrderApplicationService

src/main/java/com/example/deepdive/application/OrderApplicationService.java
Java-Datei öffnen
Rolle im Ablauf

Orchestriert den Place-Order-Use-Case und hält Kundenprüfung, Reservierung, Zahlung, Persistenz und Outbox innerhalb einer Transaktionsgrenze zusammen.

Im Lesepfad folgt Order: Kapselt den fachlichen Zustand der Bestellung, erzwingt Statusübergänge und sammelt Domain Events.

Typ
class OrderApplicationService
Verwendet
Order
Verwendet von
DemoApp
Einstiege
placeOrder(PlaceOrderCommand command)
Application Service
package com.example.deepdive.application;

import com.example.deepdive.domain.*;
import com.example.deepdive.outbox.*;
import com.example.deepdive.shared.*;
import java.time.Clock;
import java.util.*;

// Pattern: Application Service - orchestriert einen Use Case, hält aber keine Fachlogik versteckt.
public final class OrderApplicationService {
    private final CustomerRepository customers;
    private final OrderRepository orders;
    private final InventoryPort inventory;
    private final PaymentPort payment;
    private final OutboxRepository outbox;
    private final AuditPort audit;
    private final TransactionRunner tx;
    private final Clock clock;

    public OrderApplicationService(CustomerRepository customers, OrderRepository orders, InventoryPort inventory, PaymentPort payment, OutboxRepository outbox, AuditPort audit, TransactionRunner tx, Clock clock) {
        this.customers = customers; this.orders = orders; this.inventory = inventory; this.payment = payment; this.outbox = outbox; this.audit = audit; this.tx = tx; this.clock = clock;
    }

    public Result<PlaceOrderResult, DomainError> placeOrder(PlaceOrderCommand command) {
        return tx.inTransaction(() -> {
            if (outbox.hasCommand(command.idempotencyKey())) return Result.err(new DomainError.DuplicateCommand(command.idempotencyKey()));
            Customer customer = customers.findById(command.customerId()).orElseThrow(() -> new IllegalArgumentException("unknown customer"));
            if (customer.blocked()) return Result.err(new DomainError.CustomerBlocked(customer.id()));
            Order order = Order.draft(command.orderId(), command.customerId());
            for (PlaceOrderCommand.Line line : command.lines()) {
                Result<Money, DomainError> reserved = inventory.reserve(line.productId(), line.quantity());
                if (!reserved.isOk()) return Result.err(reserved.errorOrNull());
                Result<Void, DomainError> added = order.addLine(new OrderLine(line.productId(), line.quantity(), reserved.orElseThrow()));
                if (!added.isOk()) return Result.err(added.errorOrNull());
            }
            Result<Void, DomainError> reserved = order.markReserved();
            if (!reserved.isOk()) return Result.err(reserved.errorOrNull());
            Result<String, DomainError> authorized = payment.authorize(customer.id(), order.total());
            if (!authorized.isOk()) return Result.err(authorized.errorOrNull());
            Result<Void, DomainError> paid = order.authorizePayment(authorized.orElseThrow(), clock.instant());
            if (!paid.isOk()) return Result.err(paid.errorOrNull());
            orders.save(order);
            for (DomainEvent event : order.pullEvents()) outbox.add(OutboxMessage.from(command.idempotencyKey(), event));
            audit.record("ORDER_PLACED", order.id() + " total=" + order.total());
            return Result.ok(new PlaceOrderResult(order.id(), order.status().name(), order.total().toString()));
        });
    }
}

3. Order

src/main/java/com/example/deepdive/domain/Order.java
Java-Datei öffnen
Rolle im Ablauf

Kapselt den fachlichen Zustand der Bestellung, erzwingt Statusübergänge und sammelt Domain Events.

Im Lesepfad folgt SimpleTransactionRunner: Stellt die im Lab simulierte Transaktionsgrenze bereit, in der Fachzustand und Outbox konsistent verändert werden.

Typ
class Order
Verwendet
Verwendet von
OrderApplicationService
Einstiege
draft(OrderId id, CustomerId customerId), rehydrate(OrderId id, CustomerId customerId, List<OrderLine> lines, OrderStatus status, Money total, long version), addLine(OrderLine line), markReserved(), authorizePayment(String transactionId, Instant now)
Aggregate Root
package com.example.deepdive.domain;

import com.example.deepdive.shared.*;
import java.time.Instant;
import java.util.*;

// Pattern: Aggregate Root - Order schützt Statusübergänge, Invarianten und Domain Events.
public final class Order {
    private final OrderId id;
    private final CustomerId customerId;
    private final List<OrderLine> lines = new ArrayList<>();
    private final List<DomainEvent> events = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;
    private Money total = Money.eur("0.00");
    private long version;

    private Order(OrderId id, CustomerId customerId, long version) {
        this.id = Objects.requireNonNull(id);
        this.customerId = Objects.requireNonNull(customerId);
        this.version = version;
    }
    public static Order draft(OrderId id, CustomerId customerId){ return new Order(id, customerId, 0); }
    public static Order rehydrate(OrderId id, CustomerId customerId, List<OrderLine> lines, OrderStatus status, Money total, long version){
        Order order = new Order(id, customerId, version);
        order.lines.addAll(lines);
        order.status = status;
        order.total = total;
        return order;
    }
    public Result<Void, DomainError> addLine(OrderLine line) {
        if (status != OrderStatus.DRAFT) return Result.err(new DomainError.InvalidState(status.name(), "add line"));
        lines.add(line);
        total = total.add(line.lineTotal());
        return Result.ok(null);
    }
    public Result<Void, DomainError> markReserved(){
        if (status != OrderStatus.DRAFT) return Result.err(new DomainError.InvalidState(status.name(), "reserve"));
        status = OrderStatus.RESERVED;
        return Result.ok(null);
    }
    public Result<Void, DomainError> authorizePayment(String transactionId, Instant now){
        if (status != OrderStatus.RESERVED) return Result.err(new DomainError.InvalidState(status.name(), "authorize payment"));
        status = OrderStatus.PAYMENT_AUTHORIZED;
        events.add(new OrderPlaced(id, total.toString(), now));
        return Result.ok(null);
    }
    public Result<Void, DomainError> invoice(){
        if (status != OrderStatus.PAYMENT_AUTHORIZED) return Result.err(new DomainError.InvalidState(status.name(), "invoice"));
        status = OrderStatus.INVOICED;
        return Result.ok(null);
    }
    public void incrementVersion(){ version++; }
    public List<DomainEvent> pullEvents(){ var copy = List.copyOf(events); events.clear(); return copy; }
    public OrderId id(){ return id; }
    public CustomerId customerId(){ return customerId; }
    public List<OrderLine> lines(){ return List.copyOf(lines); }
    public OrderStatus status(){ return status; }
    public Money total(){ return total; }
    public long version(){ return version; }
}

4. SimpleTransactionRunner

src/main/java/com/example/deepdive/adapter/SimpleTransactionRunner.java
Java-Datei öffnen
Rolle im Ablauf

Stellt die im Lab simulierte Transaktionsgrenze bereit, in der Fachzustand und Outbox konsistent verändert werden.

Im Lesepfad folgt InMemoryOutboxRepository: Speichert Outbox-Nachrichten und dient dem Publisher als Quelle noch nicht veröffentlichter Ereignisse.

Typ
class SimpleTransactionRunnerimplements TransactionRunner
Verwendet
Verwendet von
DemoApp
Einstiege
inTransaction(Supplier<T> work)
package com.example.deepdive.adapter;
import com.example.deepdive.application.TransactionRunner;
import java.util.function.Supplier;
public final class SimpleTransactionRunner implements TransactionRunner {
    public <T> T inTransaction(Supplier<T> work) {
        try { return work.get(); } catch (RuntimeException ex) { throw ex; }
    }
}

5. InMemoryOutboxRepository

src/main/java/com/example/deepdive/adapter/InMemoryOutboxRepository.java
Java-Datei öffnen
Rolle im Ablauf

Speichert Outbox-Nachrichten und dient dem Publisher als Quelle noch nicht veröffentlichter Ereignisse.

Im Lesepfad folgt OutboxPublisher: Liest offene Outbox-Einträge, veröffentlicht sie über den EventPublisher und markiert erfolgreiche Zustellungen.

Typ
class InMemoryOutboxRepositoryimplements OutboxRepository
Verwendet
Verwendet von
DemoApp
Einstiege
add(OutboxMessage message), pending(), update(OutboxMessage message), hasCommand(String commandKey), all()
package com.example.deepdive.adapter;
import com.example.deepdive.outbox.*;
import java.util.*;
public final class InMemoryOutboxRepository implements OutboxRepository {
    private final Map<String, OutboxMessage> messages = new LinkedHashMap<>();
    public void add(OutboxMessage message){ messages.put(message.id(), message); }
    public List<OutboxMessage> pending(){ return messages.values().stream().filter(m -> !m.published()).toList(); }
    public void update(OutboxMessage message){ messages.put(message.id(), message); }
    public boolean hasCommand(String commandKey){ return messages.values().stream().anyMatch(m -> m.commandKey().equals(commandKey)); }
    public List<OutboxMessage> all(){ return List.copyOf(messages.values()); }
}

6. OutboxPublisher

src/main/java/com/example/deepdive/outbox/OutboxPublisher.java
Java-Datei öffnen
Rolle im Ablauf

Liest offene Outbox-Einträge, veröffentlicht sie über den EventPublisher und markiert erfolgreiche Zustellungen.

Im Lesepfad folgt ReportingConsumer: Zeigt die nachgelagerte, idempotente Verarbeitung eines veröffentlichten Order-Events.

Typ
class OutboxPublisher
Verwendet
Verwendet von
DemoApp
Einstiege
publishBatch()
Publisher
package com.example.deepdive.outbox;

// Pattern: Publisher - getrennte technische Veröffentlichung nach dem Commit.
public final class OutboxPublisher {
    private final OutboxRepository outbox;
    private final EventPublisher publisher;
    public OutboxPublisher(OutboxRepository outbox, EventPublisher publisher) { this.outbox = outbox; this.publisher = publisher; }
    public int publishBatch() {
        int count = 0;
        for (OutboxMessage message : outbox.pending()) {
            OutboxMessage attempted = message.markAttempt();
            try {
                publisher.publish(attempted);
                outbox.update(attempted.markPublished());
                count++;
            } catch (RuntimeException ex) {
                outbox.update(attempted);
            }
        }
        return count;
    }
}

7. ReportingConsumer

src/main/java/com/example/deepdive/outbox/ReportingConsumer.java
Java-Datei öffnen
Rolle im Ablauf

Zeigt die nachgelagerte, idempotente Verarbeitung eines veröffentlichten Order-Events.

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

Typ
class ReportingConsumer
Verwendet
Verwendet von
Einstiege
consume(OutboxMessage message), projections()
package com.example.deepdive.outbox;
import java.util.*;
public final class ReportingConsumer {
    private final IdempotencyStore idempotency;
    private final List<String> projections = new ArrayList<>();
    public ReportingConsumer(IdempotencyStore idempotency){ this.idempotency = idempotency; }
    public void consume(OutboxMessage message){
        if (!idempotency.markIfNew(message.id())) return;
        projections.add(message.type() + " -> " + message.payload());
    }
    public List<String> projections(){ return List.copyOf(projections); }
}
Alle Projektdateien öffnen (52 Einträge)
⌂ Cockpit