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.
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.
Rolle im AblaufStartet 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.
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());
}
}
Rolle im AblaufOrchestriert 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()));
});
}
}
Rolle im AblaufKapselt 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; }
}
Rolle im AblaufSpeichert 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()); }
}