Clean & Hexagonal Architecture im Enterprise-Projekt
Saubere Architektur trennt Fachlichkeit von Framework, Datenbank, Messaging, SOAP und Deployment.
Ports & AdapterClean ArchitectureTestbarkeit
Problem
In klassischen Enterprise-Systemen landen Controller, EntityManager, SOAP-Client, JMS-Produzent und Geschäftsregeln oft in derselben Klasse. Dadurch ist jeder kleine Fachtest ein halber Integrationstest. Hexagonal Architecture schiebt technische Details an den Rand.
Architekturregeln
| Regel | Konsequenz |
|---|---|
| Domain importiert kein Spring, Jakarta, Hibernate oder Kafka | Fachmodell bleibt portierbar |
| Application Core kennt Ports, keine Adapter | Technik kann ausgetauscht werden |
| Adapter übersetzen technische Verträge | Legacy-Sprache bleibt draußen |
| Framework startet Anwendung, besitzt sie aber nicht | Use Cases bleiben unabhängig |
Ports
Ports ohne Framework-Abhängigkeit
public interface BillingPort { // Pattern: Port
BillingReservation reserveInvoice(OrderId orderId, Money total);
}
public interface OrderRepository { // Pattern: Repository
void save(Order order);
Optional<Order> findById(OrderId id);
}
public interface ClockPort { // Pattern: Dependency Inversion
Instant now();
}
Adapter
SOAP-Adapter schützt die Domain vor Legacy-Sprache
public final class SoapBillingAdapter implements BillingPort { // Pattern: Adapter + ACL
private final LegacyBillingClient client;
private final LegacyBillingMapper mapper;
@Override
public BillingReservation reserveInvoice(OrderId orderId, Money total) {
LegacyInvoiceRequest request = mapper.toLegacyRequest(orderId, total);
LegacyInvoiceResponse response = client.reserve(request);
return mapper.toDomainReservation(response);
}
}
Testbarkeit
Der Use Case kann mit In-Memory-Ports getestet werden. Kein Webserver, keine Datenbank, kein Broker ist nötig.
Schneller Use-Case-Test
@Test
void placeOrderWritesOutboxEvent() {
InMemoryOrderRepository orders = new InMemoryOrderRepository();
InMemoryOutbox outbox = new InMemoryOutbox();
PlaceOrderUseCase useCase = new PlaceOrderUseCase(orders, new AlwaysAvailableInventory(), TransactionRunner.noop(), outbox);
OrderId id = useCase.handle(sampleCommand());
assertThat(orders.findById(id)).isPresent();
assertThat(outbox.events()).hasSize(1);
}
Entscheidungen
V3 dokumentiert diese Architektur zusätzlich in docs/architecture-decisions.md und markiert die verwendeten Pattern in docs/design-patterns.md.