Enterprise Academy Final Plus - echte Beispiele und Projektlogik

Diese Fassung ersetzt die generischen Platzhaltertexte durch konkrete Inhalte aus dem Projekt. Jedes Kapitel nennt echte Dateien, echte Befehle und echte Codeausschnitte. Die Beispiele sind nicht mehr als abstrakte Schablone gedacht, sondern als Wegweiser durch das vorhandene Maven-/Container-Lab.

← Zurueck zum Index

01. Ausgangslage: welche Plattform wird gebaut?

Die Plattform bildet einen Order-Acceptance-Prozess nach. Ein Client ruft die Runtime auf, die Runtime prueft fachliche Regeln, schreibt in PostgreSQL, legt ein Outbox-Event an, veroeffentlicht ueber RabbitMQ und macht Zustand und Fehler ueber Observability sichtbar.

Wichtige Dateien:

  • 04_PROJEKT/docker-compose.yml
  • 04_PROJEKT/maven-project/runtime/src/main/java/com/example/order/runtime/OrderRuntimeApplication.java
  • 04_PROJEKT/maven-project/runtime/src/main/resources/application.yml
  • 04_PROJEKT/containers/keycloak/realm-order-platform.json
name: codex-enterprise-maven-v2

services:
  postgres:
    image: postgres:16-alpine
    container_name: cemv2-postgres
    environment:
      POSTGRES_DB: orderdb
      POSTGRES_USER: order_user
      POSTGRES_PASSWORD: order_pass
    ports:
      - "5432:5432"
    volumes:
      - cemv2-postgres-data:/var/lib/postgresql/data
      - ./containers/postgres/init.sql:/docker-entrypoint-initdb.d/00-init.sql:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U order_user -d orderdb"]
      interval: 5s
      timeout: 3s
      retries: 20

  adminer:
    image: adminer:4
    container_name: cemv2-adminer
    ports:
      - "8081:8080"
    depends_on:
      postgres:
        condition: service_healthy

  rabbitmq:
    image: rabbitmq:3-management-alpine
    container_name: cemv2-rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: order_user
      RABBITMQ_DEFAULT_PASS: order_pass
    ports:
      - "5672:5672"
      - "15672:15672"
    volumes:
      - ./containers/rabbitmq/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro
      - ./containers/rabbitmq/definitions.json:/etc/rabbitmq/definitions.json:ro
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 5s
      timeout: 3s
      retries: 20

  keycloak:
    image: quay.io/keycloak/keycloak:25.0
    container_name: cemv2-keycloak
    command: ["start-dev", "--import-realm"]
    environment:
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: admin
      KC_HEALTH_ENABLED: "true"
    ports:
      - "8082:8080"
    volumes:
      - ./containers/keycloak/realm-order-platform.json:/opt/keycloak/data/import/realm-order-platform.json:ro

  wiremock:
    image: wiremock/wiremock:3.9.1
    container_name: cemv2-wiremock
    ports:
      - "8089:8080"
    volumes:
      - ./containers/wiremock/mappings:/home/wiremock/mappings:ro
      - ./containers/wiremock/__files:/home/wiremock/__files:ro

  mailhog:
    image: mailhog/mailhog:v1.0.1
    container_name: cemv2-mailhog
    ports:
      - "1025:1025"
      - "8025:8025"

  prometheus:
    image: prom/prometheus:v2.54.1
    container_name: cemv2-prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./containers/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - ./containers/prometheus/order-platform-alerts.yml:/etc/prometheus/order-platform-alerts.yml:ro

  grafana:
    image: grafana/grafana:11.1.4
    container_name: cemv2-grafana
    environment:
      GF_SECURITY_ADMIN_USER: admin
      GF_SECURITY_ADMIN_PASSWORD: admin
    ports:
      - "3000:3000"
    volumes:
      - ./containers/grafana/provisioning:/etc/grafana/provisioning:ro
    depends_on:
      - prometheus

volumes:
  cemv2-postgres-data:

Praktische Pruefung:

docker compose up -d
docker compose ps
curl -i http://localhost:8080/actuator/health

Enterprise-Lektion: Eine echte Lernplattform braucht sichtbare Systemgrenzen. Ohne Datenbank, Broker, Identity Provider und Monitoring bleibt Architektur nur Theorie.

02. Maven Multi-Module als Architekturgrenze

Maven wird hier nicht nur als Build-Werkzeug benutzt. Die Module bilden Verantwortungen ab: Domain, Ports, Application, Adapter, Runtime und Integrationstests. Dadurch werden falsche Abhaengigkeiten frueh sichtbar.

<modules>
    <module>domain</module>
    <module>ports</module>
    <module>application</module>
    <module>adapters</module>
    <module>runtime</module>
    <module>integration-tests</module>

Richtiges Prinzip:

runtime -> adapters -> application -> ports -> domain

Falsches Prinzip:

domain -> spring-jdbc
domain -> rabbitmq
domain -> spring-security

Praktische Pruefung:

cd 04_PROJEKT/maven-project
mvn -q -DskipTests verify
mvn -q -pl domain dependency:tree

Enterprise-Lektion: Gute Modulstruktur verhindert nicht jeden Fehler, aber sie macht Architekturverletzungen pruefbar.

03. Domain-Modell und fachliche Invarianten

Die Domain schuetzt Mindestpositionen, Statusuebergaenge und fachliche Werte. Sie kennt keine Datenbank, keinen Broker, kein Webframework und keine Security-Technik.

package com.example.order.domain;

import java.math.BigDecimal;
import java.util.List;

// Pattern: Aggregate Root - Order schützt fachliche Invarianten zentral.
public final class Order {
    private final OrderId id;
    private final List<OrderLine> lines;
    private OrderStatus status;

    private Order(OrderId id, List<OrderLine> lines) {
        if (lines == null || lines.isEmpty()) {
            throw new DomainRuleViolation("Eine Bestellung braucht mindestens eine Position.");
        }

        this.id = id;
        this.lines = List.copyOf(lines);
        this.status = OrderStatus.DRAFT;
    }

    public static Order draft(OrderId id, List<OrderLine> lines) {
        return new Order(id, lines);
    }

    public BigDecimal totalAmount() {
        return lines.stream()
                .map(OrderLine::lineAmount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }

    public void markAccepted() {
        if (status == OrderStatus.REJECTED) {
            throw new DomainRuleViolation("Abgelehnte Bestellung kann nicht angenommen werden.");
        }

        this.status = OrderStatus.ACCEPTED;
    }

    public OrderId id() {
        return id;
    }

    public OrderStatus status() {
        return status;
    }
}

Warum ist das wichtig? Wenn fachliche Regeln nur im Controller oder in SQL stehen, werden sie bei neuen Schnittstellen leicht umgangen. Invarianten gehoeren in den fachlichen Kern.

Praktische Pruefung:

grep -R "org.springframework\|jakarta.persistence\|java.sql" 04_PROJEKT/maven-project/domain/src/main/java || true

Erwartung: Keine Treffer.

04. Ports: fachliche Verträge ohne Infrastruktur

Ports beschreiben, was die Anwendung braucht, nicht wie es technisch umgesetzt wird. OrderRepository sagt nicht PostgreSQL, JDBC oder JPA. PaymentGateway sagt nicht WireMock, REST oder Timeout.

package com.example.order.ports;

import com.example.order.domain.Order;
import com.example.order.domain.OrderId;

import java.util.Optional;

// Pattern: Port - fachlicher Vertrag ohne Infrastrukturabhängigkeit.
public interface OrderRepository {
    void save(Order order);
    Optional<Order> findById(OrderId id);
}
package com.example.order.ports;

import com.example.order.domain.Order;

// Pattern: Gateway Port - externe Payment-Kommunikation bleibt austauschbar.
public interface PaymentGateway {
    PaymentAuthorization authorize(Order order);
}

Enterprise-Lektion: Ports sind die Sprache zwischen Fachlogik und Infrastruktur. Sie halten fachliche Tests klein und Adapter austauschbar.

05. Application Service: Ablauf koordinieren

Der Use Case ist der fachliche Ablauf: Bestellung erzeugen, Payment autorisieren, Status setzen, speichern, Outbox Event schreiben.

package com.example.order.application;

import com.example.order.domain.Order;
import com.example.order.domain.OrderId;
import com.example.order.domain.OrderLine;
import com.example.order.ports.OrderRepository;
import com.example.order.ports.OutboxPublisher;
import com.example.order.ports.PaymentGateway;

import java.util.List;

// Pattern: Application Service - koordiniert Ports, enthält keine Infrastrukturdetails.
public final class AcceptOrderUseCase {
    private final OrderRepository orders;
    private final PaymentGateway payments;
    private final OutboxPublisher outbox;

    public AcceptOrderUseCase(OrderRepository orders, PaymentGateway payments, OutboxPublisher outbox) {
        this.orders = orders;
        this.payments = payments;
        this.outbox = outbox;
    }

    public OrderId accept(OrderId id, List<OrderLine> lines) {
        Order order = Order.draft(id, lines);
        payments.authorize(order);
        order.markAccepted();

        orders.save(order);
        outbox.append(id.value(), "OrderAccepted", "{\"orderId\":\"" + id.value() + "\"}");

        return id;
    }
}

Praktische Pruefung:

grep -R "payments.authorize\|outbox.append\|orders.save" 04_PROJEKT/maven-project/application/src/main/java

Enterprise-Lektion: Der Application Service darf orchestrieren, aber keine HTTP-, JDBC- oder RabbitMQ-Details enthalten.

06. PostgreSQL Adapter und Flyway Schema

PostgreSQL ist im Adapter gekapselt. Die Tabellen werden ueber Flyway erzeugt. Das Schema trennt order_platform fuer fachliche Daten und outbox fuer Integrationsereignisse.

package com.example.order.adapters.postgres;

import com.example.order.domain.Order;
import com.example.order.domain.OrderId;
import com.example.order.ports.OrderRepository;
import org.springframework.jdbc.core.JdbcTemplate;

import java.util.Optional;

// Pattern: Adapter - PostgreSQL-Details bleiben außerhalb der Fachlogik.
public final class JdbcOrderRepository implements OrderRepository {
    private final JdbcTemplate jdbc;

    public JdbcOrderRepository(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Override
    public void save(Order order) {
        jdbc.update("""
                insert into order_platform.orders(order_id, status, total_amount)
                values (?, ?, ?)
                on conflict(order_id)
                do update set status = excluded.status, total_amount = excluded.total_amount
                """, order.id().value(), order.status().name(), order.totalAmount());
    }

    @Override
    public Optional<Order> findById(OrderId id) {
        return Optional.empty();
    }
}
create table if not exists order_platform.orders (
    order_id varchar(80) primary key,
    status varchar(40) not null,
    total_amount numeric(19, 2) not null,
    created_at timestamptz not null default now()
);

create table if not exists outbox.events (
    event_id bigserial primary key,
    aggregate_id varchar(80) not null,
    event_type varchar(120) not null,
    payload_json jsonb not null,
    status varchar(30) not null,
    created_at timestamptz not null default now(),
    published_at timestamptz
);

Praktische Pruefung:

docker compose exec postgres psql -U order_user -d orderdb -c "select table_schema, table_name from information_schema.tables where table_schema in ('order_platform','outbox');"

Enterprise-Lektion: Migrationen sind Teil der Anwendung. Ohne versioniertes Schema ist ein reproduzierbarer Betrieb kaum moeglich.

07. Outbox Relay statt Dual Write

Das Outbox Pattern verhindert, dass Datenbank und Broker auseinanderlaufen. Der Use Case schreibt Bestellung und Event in dieselbe Datenbanktransaktion. Das Relay publiziert spaeter nach RabbitMQ.

package com.example.order.runtime;

import com.example.order.adapters.events.JdbcOutboxRelayRepository;
import com.example.order.adapters.events.OutboxEventRecord;
import com.example.order.ports.OrderEventPublisher;
import org.springframework.scheduling.annotation.Scheduled;

import java.util.Map;

// Pattern: Outbox Relay - trennt DB-Transaktion von Message-Broker-Veröffentlichung.
public final class OutboxRelayJob {
    private final JdbcOutboxRelayRepository outbox;
    private final OrderEventPublisher publisher;

    private final Map<String, String> routingKeys = Map.of(
            "OrderAccepted", "order.accepted"
    );

    public OutboxRelayJob(JdbcOutboxRelayRepository outbox, OrderEventPublisher publisher) {
        this.outbox = outbox;
        this.publisher = publisher;
    }

    @Scheduled(fixedDelayString = "${order.outbox.relay-delay-ms:5000}")
    public void publishNewEvents() {
        for (OutboxEventRecord event : outbox.findNewEvents(25)) {
            try {
                String routingKey = routingKeys.getOrDefault(event.eventType(), "order.unknown");
                publisher.publish(routingKey, event.payloadJson());
                outbox.markPublished(event.eventId());
            } catch (RuntimeException ex) {
                outbox.markFailed(event.eventId(), ex.getMessage());
            }
        }
    }
}

Diagnose:

select event_id, aggregate_id, event_type, status, created_at, published_at
from outbox.events
order by event_id desc;

Enterprise-Lektion: Das System wird robuster, weil ein Broker-Ausfall nicht automatisch zum Datenverlust fuehrt. Gleichzeitig braucht man Monitoring fuer Outbox-Backlog.

08. RabbitMQ: Exchange, Queue, Binding und DLQ

RabbitMQ entkoppelt Order und Billing. Die Exchange order.events verteilt fachliche Ereignisse. Die Queue billing.order-accepted ist ein konkreter Consumer-Eingang. DLQ-Strukturen isolieren fehlerhafte Nachrichten.

{
  "vhosts": [
    {"name": "/"}
  ],
  "permissions": [
    {
      "user": "order_user",
      "vhost": "/",
      "configure": ".*",
      "write": ".*",
      "read": ".*"
    }
  ],
  "exchanges": [
    {
      "name": "order.events",
      "vhost": "/",
      "type": "topic",
      "durable": true,
      "auto_delete": false,
      "internal": false,
      "arguments": {}
    },
    {
      "name": "order.dlx",
      "vhost": "/",
      "type": "topic",
      "durable": true,
      "auto_delete": false,
      "internal": false,
      "arguments": {}
    }
  ],
  "queues": [
    {
      "name": "billing.order-accepted",
      "vhost": "/",
      "durable": true,
      "auto_delete": false,
      "arguments": {
        "x-dead-letter-exchange": "order.dlx",
        "x-dead-letter-routing-key": "billing.order-accepted.dead"
      }
    },
    {
      "name": "billing.order-accepted.dlq",
      "vhost": "/",
      "durable": true,
      "auto_delete": false,
      "arguments": {}
    }
  ],
  "bindings": [
    {
      "source": "order.events",
      "vhost": "/",
      "destination": "billing.order-accepted",
      "destination_type": "queue",
      "routing_key": "order.accepted",
      "arguments": {}
    },
    {
      "source": "order.dlx",
      "vhost": "/",
      "destination": "billing.order-accepted.dlq",
      "destination_type": "queue",
      "routing_key": "billing.order-accepted.dead",
      "arguments": {}
    }
  ]
// ... gekuerzt: siehe Projektdatei 04_PROJEKT/containers/rabbitmq/definitions.json

Praktische Pruefung:

curl -u order_user:order_pass http://localhost:15672/api/queues
curl -u order_user:order_pass http://localhost:15672/api/exchanges/%2F/order.events

Enterprise-Lektion: Messaging entkoppelt Systeme, erzeugt aber neue Betriebspflichten: Idempotenz, DLQ, Replay, Monitoring und Backpressure.

09. Keycloak und rollenbasierte REST-Security

Keycloak liefert JWTs mit Realm-Rollen. Spring Security schuetzt Endpunkte am Runtime-Rand. Domain und Application Layer bleiben frei von OAuth2-Details.

package com.example.order.runtime.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.web.SecurityFilterChain;

// Pattern: Security Boundary - Zugriffsregeln werden zentral am Runtime-Rand definiert.
@Configuration
@EnableMethodSecurity
public class SecurityConfiguration {
    @Bean
    SecurityFilterChain apiSecurity(HttpSecurity http,
                                    Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter) throws Exception {
        return http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/actuator/health", "/actuator/info", "/actuator/prometheus").permitAll()
                        .requestMatchers("/api/orders/*/accept").hasRole("ORDER_ADMIN")
                        .requestMatchers("/api/orders/**").hasAnyRole("ORDER_READER", "ORDER_ADMIN")
                        .requestMatchers("/api/billing/**").hasRole("BILLING_USER")
                        .anyRequest().authenticated()
                )
                .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter)))
                .httpBasic(Customizer.withDefaults())
                .build();
    }
}
package com.example.order.runtime.security;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.util.Collection;
import java.util.List;
import java.util.Map;

// Pattern: Anti-Corruption Layer - Keycloak-Rollen werden in Spring Authorities übersetzt.
public final class RealmRoleJwtAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
    @Override
    public AbstractAuthenticationToken convert(Jwt jwt) {
        Collection<GrantedAuthority> authorities = extractRealmRoles(jwt).stream()
                .map(role -> "ROLE_" + role.toUpperCase().replace('-', '_'))
                .map(SimpleAuthority::new)
                .map(GrantedAuthority.class::cast)
                .toList();

        return new JwtAuthenticationToken(jwt, authorities, jwt.getSubject());
    }

    @SuppressWarnings("unchecked")
    private List<String> extractRealmRoles(Jwt jwt) {
        Map<String, Object> realmAccess = jwt.getClaim("realm_access");
        if (realmAccess == null) {
            return List.of();
        }

        Object roles = realmAccess.get("roles");
        if (roles instanceof List<?> list) {
            return list.stream()
                    .filter(String.class::isInstance)
                    .map(String.class::cast)
                    .toList();
        }

        return List.of();
    }

    private record SimpleAuthority(String authority) implements GrantedAuthority {
        @Override
        public String getAuthority() {
            return authority;
        }
    }
}

Praktische Pruefung:

TOKEN=$(curl -s -X POST "http://localhost:8082/realms/order-platform/protocol/openid-connect/token"   -H "Content-Type: application/x-www-form-urlencoded"   -d "client_id=order-runtime"   -d "grant_type=password"   -d "username=order-admin"   -d "password=admin" | python -c "import sys,json; print(json.load(sys.stdin)['access_token'])")

curl -i -X POST http://localhost:8080/api/orders/ORD-3001/accept   -H "Authorization: Bearer $TOKEN"

Enterprise-Lektion: 401 bedeutet meist: kein oder ungueltiges Token. 403 bedeutet meist: Token gueltig, aber Rolle reicht nicht.

10. Observability: fachliche und technische Sichtbarkeit

Metriken zeigen nicht nur HTTP-Latenz. Wichtig sind auch fachliche Signale: angenommene Bestellungen, abgelehnte Zahlungen, Outbox-Backlog und Health-Status.

package com.example.order.runtime.observability;

import io.micrometer.core.instrument.Counter;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;

import java.time.Duration;

// Pattern: Observability Facade - Fachmetriken werden zentral und testbar erfasst.
public final class BusinessMetrics {
    private final Counter acceptedOrders;
    private final Counter failedPayments;
    private final Timer orderAcceptanceTimer;

    public BusinessMetrics(MeterRegistry registry) {
        this.acceptedOrders = Counter.builder("order_accepted_total")
                .description("Total accepted orders")
                .tag("bounded_context", "order")
                .register(registry);

        this.failedPayments = Counter.builder("payment_failed_total")
                .description("Total failed payment authorizations")
                .tag("provider", "wiremock-payment")
                .register(registry);

        this.orderAcceptanceTimer = Timer.builder("order_acceptance_duration")
                .description("Duration of order acceptance use case")
                .publishPercentileHistogram()
                .minimumExpectedValue(Duration.ofMillis(10))
                .maximumExpectedValue(Duration.ofSeconds(5))
                .register(registry);
    }

    public void orderAccepted() {
        acceptedOrders.increment();
    }

    public void paymentFailed() {
        failedPayments.increment();
    }

    public Timer.Sample startOrderAcceptance(MeterRegistry registry) {
        return Timer.start(registry);
    }

    public void stopOrderAcceptance(Timer.Sample sample) {
        sample.stop(orderAcceptanceTimer);
    }
}
package com.example.order.runtime.observability;

import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.jdbc.core.JdbcTemplate;

// Pattern: Health Check - betriebliche Risiken werden explizit prüfbar.
public final class OutboxHealthIndicator implements HealthIndicator {
    private final JdbcTemplate jdbc;

    public OutboxHealthIndicator(JdbcTemplate jdbc) {
        this.jdbc = jdbc;
    }

    @Override
    public Health health() {
        Long backlog = jdbc.queryForObject(
                "select count(*) from outbox.events where status = 'NEW'",
                Long.class
        );

        if (backlog != null && backlog > 100) {
            return Health.down()
                    .withDetail("outboxBacklog", backlog)
                    .withDetail("reason", "Outbox backlog is above threshold")
                    .build();
        }

        return Health.up()
                .withDetail("outboxBacklog", backlog == null ? 0 : backlog)
                .build();
    }
}

Praktische Pruefung:

curl http://localhost:8080/actuator/prometheus | grep -E "order_|outbox_|http_server"
curl http://localhost:9090/api/v1/targets

Enterprise-Lektion: Ein System ist erst betreibbar, wenn man Fehler sehen kann, bevor Kunden sie melden.

11. Resilience: Timeout, Retry, Circuit Breaker und Rate Limit

Resilience schuetzt das System vor langsamen oder fehlerhaften Abhaengigkeiten. Payment ist dafuer ein gutes Beispiel: Provider koennen langsam sein, HTTP 500 liefern oder zeitweise ausfallen.

package com.example.order.adapters.resilience;

import com.example.order.domain.Order;
import com.example.order.ports.PaymentAuthorization;
import com.example.order.ports.PaymentGateway;
import io.github.resilience4j.bulkhead.Bulkhead;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.decorators.Decorators;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.timelimiter.TimeLimiter;

import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.function.Supplier;

// Pattern: Resilience Decorator - schützt den externen Payment Gateway ohne Fachlogik zu verändern.
public final class ResilientPaymentGateway implements PaymentGateway {
    private final PaymentGateway delegate;
    private final CircuitBreaker circuitBreaker;
    private final Retry retry;
    private final Bulkhead bulkhead;
    private final TimeLimiter timeLimiter;

    public ResilientPaymentGateway(PaymentGateway delegate,
                                   CircuitBreaker circuitBreaker,
                                   Retry retry,
                                   Bulkhead bulkhead,
                                   TimeLimiter timeLimiter) {
        this.delegate = delegate;
        this.circuitBreaker = circuitBreaker;
        this.retry = retry;
        this.bulkhead = bulkhead;
        this.timeLimiter = timeLimiter;
    }

    @Override
    public PaymentAuthorization authorize(Order order) {
        Supplier<PaymentAuthorization> supplier = () -> delegate.authorize(order);

        Supplier<PaymentAuthorization> protectedSupplier = Decorators.ofSupplier(supplier)
                .withBulkhead(bulkhead)
                .withCircuitBreaker(circuitBreaker)
                .withRetry(retry)
                .decorate();

        try {
            return timeLimiter.executeFutureSupplier(() -> CompletableFuture.supplyAsync(
                    protectedSupplier,
                    Executors.newVirtualThreadPerTaskExecutor()
            ));
        } catch (Exception ex) {
            throw new PaymentProviderUnavailableException("Payment Provider ist aktuell nicht zuverlässig erreichbar.", ex);
        }
    }

    public static ResilientPaymentGateway standard(PaymentGateway delegate) {
        CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("paymentProvider");
        Retry retry = Retry.ofDefaults("paymentProvider");
        Bulkhead bulkhead = Bulkhead.ofDefaults("paymentProvider");
        TimeLimiter timeLimiter = TimeLimiter.of("paymentProvider", Duration.ofSeconds(2));
        return new ResilientPaymentGateway(delegate, circuitBreaker, retry, bulkhead, timeLimiter);
    }
}

Praktische Pruefung:

curl -i -X POST http://localhost:8089/payment/authorize
curl http://localhost:8080/actuator/metrics | grep resilience

Enterprise-Lektion: Retry ohne Idempotenz kann Schaden erzeugen. Circuit Breaker ohne Monitoring ist nur versteckte Fehlerunterdrueckung.

12. Testcontainers und echte Integrationstests

Testcontainers startet echte Infrastruktur fuer Tests. Dadurch werden PostgreSQL, RabbitMQ und WireMock nicht nur im Kopf, sondern in einer reproduzierbaren Umgebung geprueft.

package com.example.order.it.support;

import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.utility.DockerImageName;

// Pattern: Test Fixture - echte Infrastruktur wird für E2E Tests reproduzierbar gestartet.
public final class RealSystemTestEnvironment implements AutoCloseable {
    private final PostgreSQLContainer<?> postgres;
    private final RabbitMQContainer rabbitMq;
    private final GenericContainer<?> wireMock;

    public RealSystemTestEnvironment() {
        this.postgres = new PostgreSQLContainer<>("postgres:16-alpine")
                .withDatabaseName("orderdb")
                .withUsername("order_user")
                .withPassword("order_pass");

        this.rabbitMq = new RabbitMQContainer(DockerImageName.parse("rabbitmq:3-management-alpine"))
                .withUser("order_user", "order_pass");

        this.wireMock = new GenericContainer<>(DockerImageName.parse("wiremock/wiremock:3.9.1"))
                .withExposedPorts(8080);
    }

    public void start() {
        postgres.start();
        rabbitMq.start();
        wireMock.start();
    }

    public String jdbcUrl() {
        return postgres.getJdbcUrl();
    }

    public String jdbcUsername() {
        return postgres.getUsername();
    }

    public String jdbcPassword() {
        return postgres.getPassword();
    }

    public String rabbitHost() {
        return rabbitMq.getHost();
    }

    public int rabbitPort() {
        return rabbitMq.getAmqpPort();
    }

    public String wireMockBaseUrl() {
        return "http://" + wireMock.getHost() + ":" + wireMock.getMappedPort(8080);
    }

    @Override
    public void close() {
        wireMock.stop();
        rabbitMq.stop();
        postgres.stop();
    }
}

Praktische Pruefung:

cd 04_PROJEKT/maven-project
mvn -pl integration-tests -am verify

Enterprise-Lektion: Unit Tests pruefen Logik. E2E-/Integrationstests pruefen Systemgrenzen. Man braucht beides.

13. CI/CD und Quality Gates

Die Pipeline baut, testet, prueft Abhaengigkeiten, erzeugt SBOM und baut ein Runtime Image. Gates machen Qualitaet wiederholbar.

name: ci

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

permissions:
  contents: read
  security-events: write

jobs:
  maven-verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
          cache: maven
      - name: Maven verify
        working-directory: maven-project
        run: mvn -B verify

  dependency-review:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4

  sbom-and-license:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
          cache: maven
      - name: Generate CycloneDX SBOM
        working-directory: maven-project
        run: mvn -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
      - name: License report
        working-directory: maven-project
        run: mvn -B org.codehaus.mojo:license-maven-plugin:aggregate-add-third-party
      - uses: actions/upload-artifact@v4
        with:
          name: compliance-reports
          path: |
            maven-project/target/bom.*
            maven-project/target/generated-sources/license/**

  docker-build:
    runs-on: ubuntu-latest
    needs: [maven-verify]
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - name: Docker build runtime
        run: docker build -f Dockerfile.runtime -t codex-enterprise-maven-runtime:ci .

Praktische Pruefung:

cd 04_PROJEKT/maven-project
mvn -q verify
./scripts/quality/local-quality-gates.sh

Enterprise-Lektion: Was lokal zufaellig klappt, ist noch kein Releaseprozess. Quality Gates machen den Zustand nachvollziehbar.

14. Deployment Profile: lokal, Kubernetes und OpenShift

Die Runtime kann lokal via Docker Compose und spaeter via Kubernetes/OpenShift betrieben werden. Kustomize und Helm bilden verschiedene Betriebsprofile ab.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-runtime
  namespace: order-platform
  labels:
    app: order-runtime
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-runtime
  template:
    metadata:
      labels:
        app: order-runtime
      annotations:
        prometheus.io/scrape: "true"
        prometheus.io/path: /actuator/prometheus
        prometheus.io/port: "8080"
    spec:
      containers:
        - name: order-runtime
          image: ghcr.io/example/codex-enterprise-maven-v2/order-runtime:2.0.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 8080
              name: http
          envFrom:
            - configMapRef:
                name: order-runtime-config
            - secretRef:
                name: order-runtime-secret
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              cpu: 1000m
              memory: 768Mi
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: http
            initialDelaySeconds: 20
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: http
            initialDelaySeconds: 40
            periodSeconds: 20

Praktische Pruefung:

kubectl kustomize 04_PROJEKT/deployment/kubernetes/base
helm template order-runtime 04_PROJEKT/deployment/helm/order-runtime

Enterprise-Lektion: Deployment ist nicht nur YAML. Es ist die Uebersetzung von Architektur in Laufzeitgrenzen, Ressourcen, Secrets und Netzwerkregeln.

15. Datenbankbetrieb: Backup, Restore und Migration

Datenbankbetrieb entscheidet im Ernstfall ueber Wiederherstellbarkeit. Backup-Skripte, Restore-Proben, Retention und Flyway-History gehoeren zum Produkt.

#!/usr/bin/env bash
set -euo pipefail

BACKUP_DIR="${BACKUP_DIR:-./database/backups}"
mkdir -p "${BACKUP_DIR}"
STAMP="$(date +%Y%m%d-%H%M%S)"
FILE="${BACKUP_DIR}/orderdb-${STAMP}.dump"

PGPASSWORD="${POSTGRES_PASSWORD:-order_pass}" pg_dump   --host "${POSTGRES_HOST:-localhost}"   --port "${POSTGRES_PORT:-5432}"   --username "${POSTGRES_USER:-order_user}"   --dbname "${POSTGRES_DB:-orderdb}"   --format custom   --verbose   --file "${FILE}"

echo "Backup written: ${FILE}"

Praktische Pruefung:

./04_PROJEKT/scripts/database/backup-postgres.sh
ls -lh backups/
docker compose exec postgres psql -U order_user -d orderdb -c "select * from flyway_schema_history order by installed_rank;"

Enterprise-Lektion: Ein Backup ist erst dann wertvoll, wenn ein Restore geuebt wurde.

16. Betriebsdiagnose: vom Symptom zur Ursache

Ein echter Betrieb beginnt mit klaren Fragen:

  1. Ist der Container gesund?
  2. Ist die Datenbank erreichbar?
  3. Gibt es neue Outbox Events?
  4. Kommen Messages in RabbitMQ an?
  5. Ist das JWT gueltig?
  6. Stimmen Prometheus Targets?
  7. Gibt es aktuelle Fehlerlogs?
docker compose ps
curl -i http://localhost:8080/actuator/health
docker compose exec postgres psql -U order_user -d orderdb -c "select status, count(*) from outbox.events group by status;"
curl -u order_user:order_pass http://localhost:15672/api/queues
curl http://localhost:9090/api/v1/targets

Enterprise-Lektion: Gute Runbooks sind keine Theorie. Sie fuehren dich von Symptom zu Diagnose und von Diagnose zu Handlung.