Clean & Hexagonal Architecture im Enterprise-Projekt

Saubere Architektur trennt Fachlichkeit von Framework, Datenbank, Messaging, SOAP und Deployment.

Ports & AdapterClean ArchitectureTestbarkeit
Hexagonal Architecture im Enterprise-Projekt
Hexagonal Architecture im Enterprise-Projekt

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

RegelKonsequenz
Domain importiert kein Spring, Jakarta, Hibernate oder KafkaFachmodell bleibt portierbar
Application Core kennt Ports, keine AdapterTechnik kann ausgetauscht werden
Adapter übersetzen technische VerträgeLegacy-Sprache bleibt draußen
Framework startet Anwendung, besitzt sie aber nichtUse 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.
⌂ Cockpit