Codex Enterprise Maven Lehrbuch V2 - Real Systems Lab

← Zurueck zum Index

1. Ziel von Version 2

Version 2 macht aus dem bisherigen Lernprojekt ein echtes Enterprise-Lab. Das Ziel ist nicht mehr nur, Maven-Module, Ports, Adapter und Use Cases zu verstehen, sondern sie mit echten Systemen zu verbinden.

Version 2 zeigt:

  • PostgreSQL als echte relationale Datenbank
  • Flyway für Schema-Migrationen
  • Spring Boot Runtime als ausführbare Anwendung
  • WireMock als externer Payment Provider
  • RabbitMQ als Messaging-System
  • Keycloak als Identity Provider
  • MailHog als E-Mail-Simulation
  • Prometheus und Grafana als Observability-Basis
  • Testcontainers als realistische Integrations-Testumgebung
2. Architekturüberblick
Client
  |
  v
Spring Boot Runtime
  |
  +--> Application Layer
  |       |
  |       +--> Domain Model
  |       +--> Ports
  |
  +--> PostgreSQL Adapter
  +--> Outbox Adapter
  +--> Payment Gateway Adapter
  +--> RabbitMQ Publisher
  +--> Keycloak Resource Server
  +--> Prometheus Metrics
3. Container-Stack

Der Container-Stack bildet eine kleine Enterprise-Systemlandschaft ab.

SystemRolle im LabPort
PostgreSQLOrders und Outbox speichern5432
AdminerDatenbank ansehen8081
RabbitMQEvents und Queues5672 / 15672
KeycloakOAuth2/OIDC8082
WireMockPayment Provider Mock8089
MailHogE-Mail Simulation1025 / 8025
PrometheusMetriken sammeln9090
GrafanaDashboard anzeigen3000
4. Docker Compose starten
cp .env.example .env
docker compose up -d
docker compose ps

Wenn alle Container laufen, ist die lokale Systemlandschaft bereit.

5. Maven-Projektstruktur
maven-project/
├── pom.xml
├── domain/
├── ports/
├── application/
├── adapters/
├── runtime/
└── integration-tests/

Die Struktur folgt Ports-and-Adapters. Die Fachlogik kennt keine Container. Nur Adapter und Runtime kennen PostgreSQL, WireMock, RabbitMQ und Spring Boot.

6. Domain: Aggregate Root
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;
    }
}
7. Ports
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);
}
8. Application Service
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;
    }
}
9. PostgreSQL und Outbox
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
);
10. REST Flow
curl -X POST http://localhost:8080/api/orders/ORD-1001/accept

Erwartete Antwort:

{
  "orderId": "ORD-1001",
  "status": "ACCEPTED"
}
11. WireMock Payment Provider

Der Payment Provider ist in Version 2 absichtlich ein Mock. Dadurch lernt man externe Systemintegration ohne echten Zahlungsanbieter.

{
  "request": {
    "method": "POST",
    "url": "/payment/authorize"
  },
  "response": {
    "status": 200,
    "jsonBody": {
      "authorizationId": "PAY-AUTH-1001",
      "status": "AUTHORIZED"
    }
  }
}
12. Keycloak

Keycloak stellt Rollen bereit:

  • order-reader
  • order-admin
  • billing-user

In Version 2 ist Keycloak vorbereitet. Die Runtime enthält bereits die Resource-Server-Konfiguration.

13. Observability

Prometheus liest /actuator/prometheus. Grafana enthält ein erstes Dashboard. In späteren Ausbaustufen kann man fachliche Metriken ergänzen:

  • angenommene Bestellungen pro Minute
  • abgelehnte Bestellungen
  • Payment-Fehler
  • Outbox-Rückstand
  • REST-Latenz
14. Testcontainers

Testcontainers prüft echte Infrastruktur im Test, ohne dass lokal manuell eine Datenbank laufen muss.

try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")) {
    postgres.start();
    assertTrue(postgres.isRunning());
}
15. Nächste Ausbaustufen

Sinnvolle nächste Schritte:

  1. echte RabbitMQ Publisher/Consumer ergänzen
  2. Outbox Relay implementieren
  3. Keycloak-Rollen in Controller absichern
  4. Contract Tests für WireMock ergänzen
  5. Grafana Dashboard erweitern
  6. E2E Test für REST + DB + Outbox schreiben
  7. OpenTelemetry Collector und Jaeger/Tempo ergänzen
16. Phase 2: Outbox Relay und RabbitMQ

In Phase 1 wurde ein Outbox Event in PostgreSQL gespeichert. Phase 2 macht daraus einen echten Event Flow.

Der neue Ablauf ist:

REST Request
  |
  v
AcceptOrderUseCase
  |
  +--> orders.save(order)
  +--> outbox.append(OrderAccepted)
  |
  v
PostgreSQL Commit
  |
  v
OutboxRelayJob
  |
  v
RabbitMQ exchange order.events
  |
  v
billing.order-accepted queue
  |
  v
BillingOrderAcceptedConsumer
17. RabbitMQ Exchanges und Queues

Die Datei containers/rabbitmq/definitions.json legt Exchange, Queue, Binding und DLQ an.

{
  "exchanges": [
    {
      "name": "order.events",
      "type": "topic",
      "durable": true
    }
  ],
  "queues": [
    {
      "name": "billing.order-accepted",
      "durable": true
    }
  ]
}
18. Publisher Port
package com.example.order.ports;

// Pattern: Publisher Port - Application bleibt unabhängig von RabbitMQ.
public interface OrderEventPublisher {
    void publish(String routingKey, String payloadJson);
}
19. RabbitMQ Adapter
package com.example.order.adapters.events;

import com.example.order.ports.OrderEventPublisher;
import org.springframework.amqp.rabbit.core.RabbitTemplate;

// Pattern: Messaging Adapter - RabbitMQ Details bleiben am Systemrand.
public final class RabbitOrderEventPublisher implements OrderEventPublisher {
    public static final String EXCHANGE = "order.events";

    private final RabbitTemplate rabbitTemplate;

    public RabbitOrderEventPublisher(RabbitTemplate rabbitTemplate) {
        this.rabbitTemplate = rabbitTemplate;
    }

    @Override
    public void publish(String routingKey, String payloadJson) {
        rabbitTemplate.convertAndSend(EXCHANGE, routingKey, payloadJson);
    }
}
20. Outbox Relay Job
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());
            }
        }
    }
}
21. Billing Consumer
package com.example.order.runtime;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

// Pattern: Event Consumer - Billing reagiert lose gekoppelt auf OrderAccepted.
@Component
public final class BillingOrderAcceptedConsumer {
    @RabbitListener(queues = "billing.order-accepted")
    public void onOrderAccepted(String payloadJson) {
        System.out.println("Billing received OrderAccepted event: " + payloadJson);
    }
}
22. Warum dieser Schritt wichtig ist

Dieser Schritt zeigt eine echte Enterprise-Realität: Ein fachlicher Use Case ist nicht beendet, wenn ein REST Controller eine Antwort sendet. Nachgelagerte Systeme wie Billing, Reporting oder Notification müssen informiert werden. RabbitMQ übernimmt die lose Kopplung. Die Outbox schützt davor, dass Datenbank und Message Broker auseinanderlaufen.

23. Phase 3: Keycloak Security aktivieren

Phase 3 ergänzt eine echte Security Boundary. Die Anwendung wird nicht mehr als offener REST-Service betrachtet, sondern als geschützte Resource-Server-Anwendung.

24. Rollenmodell
Keycloak RolleSpring AuthorityBedeutung
order-readerROLE_ORDER_READERBestellungen lesen
order-adminROLE_ORDER_ADMINBestellungen annehmen
billing-userROLE_BILLING_USERBilling-Funktionen nutzen
25. Security-Konfiguration
package com.example.order.runtime.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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)))
                .build();
    }
}
26. Keycloak-Rollen übersetzen

Keycloak liefert Rollen als Claim. Spring Security erwartet Authorities. Der Converter ist eine kleine Anti-Corruption-Schicht.

package com.example.order.runtime.security;

import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.oauth2.jwt.Jwt;

// 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());
    }
}
27. Geschützter Billing-Endpunkt
package com.example.order.runtime;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;

// Pattern: Method-Level Authorization - fachliche Operationen werden rollenbasiert geschützt.
@RestController
@RequestMapping("/api/billing")
public class BillingController {
    @GetMapping("/status/{orderId}")
    @PreAuthorize("hasRole('BILLING_USER')")
    BillingStatus status(@PathVariable String orderId) {
        return new BillingStatus(orderId, "READY_FOR_INVOICE");
    }

    record BillingStatus(String orderId, String status) {}
}
28. Token holen
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'])"
)"
29. Geschützten REST Flow aufrufen
curl -i \
  -X POST "http://localhost:8080/api/orders/ORD-3001/accept" \
  -H "Authorization: Bearer ${TOKEN}"
30. Warum Phase 3 wichtig ist

Ohne Security ist ein Enterprise-Lab unvollständig. Fachliche Prozesse wie Bestellung annehmen, Billing starten oder Reports lesen sind fast immer rollenbasiert geschützt. Wichtig ist dabei: Die Domain bleibt frei von Security-Technik. OAuth2, JWT und Rollenprüfung sitzen am Runtime-Rand.

31. Phase 4: Testcontainers und E2E Tests

Phase 4 ergänzt eine realistische Teststrategie. Ziel ist nicht, jeden fachlichen Sonderfall als E2E Test zu prüfen. Ziel ist, die kritischen Systempfade mit echter Infrastruktur abzusichern.

32. Testpyramide
Domain Tests
  |
Application Tests mit Fake Ports
  |
Adapter Tests mit Testcontainers
  |
Contract Tests gegen WireMock
  |
Security Tests mit Rollen/JWT
  |
Wenige E2E Tests
33. RealSystemTestEnvironment
package com.example.order.it.support;

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

// 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("rabbitmq:3-management-alpine")
                .withUser("order_user", "order_pass");

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

    public void start() {
        postgres.start();
        rabbitMq.start();
        wireMock.start();
    }
}
34. E2E Test
package com.example.order.it.e2e;

import com.example.order.it.support.RealSystemTestEnvironment;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

// Pattern: End-to-End Test - REST, DB, Outbox und Broker werden gemeinsam betrachtet.
class OrderAcceptanceE2EIT {
    @Test
    void documentsRealSystemAcceptanceFlow() {
        try (RealSystemTestEnvironment env = new RealSystemTestEnvironment()) {
            env.start();

            assertThat(env.jdbcUrl()).contains("jdbc:postgresql");
            assertThat(env.rabbitPort()).isGreaterThan(0);
            assertThat(env.wireMockBaseUrl()).startsWith("http://");
        }
    }
}
35. Failsafe statt Surefire

Integrationstests heißen in diesem Projekt *IT.java. Maven Failsafe führt diese Tests in der Integration-Test-Phase aus.

<plugin>
  <artifactId>maven-failsafe-plugin</artifactId>
  <version>3.3.1</version>
  <configuration>
    <includes>
      <include>**/*IT.java</include>
    </includes>
  </configuration>
</plugin>
36. Warum Phase 4 wichtig ist

Ohne E2E Tests kann die Architektur schön aussehen, aber trotzdem an Integration scheitern. Phase 4 zeigt, wie man die wichtigsten Risiken testbar macht:

  • falsche Datenbankmigration
  • RabbitMQ nicht erreichbar
  • WireMock Providervertrag falsch
  • Security Rollen falsch gemappt
  • Outbox Relay fehlerhaft
37. Phase 5: Observability ausbauen

Phase 5 macht das System betreibbar. Ein Enterprise-System ist nicht fertig, wenn es kompiliert und lokal startet. Es muss beobachtbar sein.

38. Observability Architektur
Spring Boot Runtime
  |
  +-- BusinessMetrics
  +-- OutboxHealthIndicator
  +-- OutboxMetricsBinder
  +-- RequestCorrelationFilter
  |
  v
/actuator/prometheus
  |
  v
Prometheus
  |
  +-- Alert Rules
  |
  v
Grafana Dashboard
39. Business Metrics
package com.example.order.runtime.observability;

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

// 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);
    }
}
40. Outbox Health Indicator
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 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).build();
        }

        return Health.up().withDetail("outboxBacklog", backlog == null ? 0 : backlog).build();
    }
}
41. Prometheus Alerts
groups:
  - name: order-platform-alerts
    rules:
      - alert: OutboxBacklogHigh
        expr: outbox_events_new > 100
        for: 5m
        labels:
          severity: warning
42. Grafana Dashboard as Code

Das Dashboard liegt im Projekt unter:

containers/grafana/provisioning/dashboards/order-observability-dashboard.json

Dadurch ist das Dashboard nicht nur manuell in Grafana gebaut, sondern versioniert und reproduzierbar.

43. Correlation ID
package com.example.order.runtime.observability;

import org.springframework.web.filter.OncePerRequestFilter;

// Pattern: Correlation ID - Logs, Requests und Fehler werden nachvollziehbar verbunden.
public final class RequestCorrelationFilter extends OncePerRequestFilter {
    public static final String HEADER = "X-Correlation-Id";
}
44. Warum Phase 5 wichtig ist

Observability beantwortet Betriebsfragen:

  • Läuft die Runtime?
  • Ist die Outbox im Rückstand?
  • Gibt es Payment-Fehler?
  • Wie langsam ist der Order-Acceptance-Use-Case?
  • Wie viele Bestellungen wurden angenommen?
  • Welche Request-ID gehört zu welchem Fehler?
37. Phase 6: Resilience

Phase 6 schützt die Runtime vor typischen Fehlern externer Systeme. In Enterprise-Systemen ist nicht die Frage, ob ein Partner-System irgendwann langsam oder fehlerhaft ist. Die Frage ist, wie kontrolliert die eigene Anwendung darauf reagiert.

38. Resilience Patterns im Überblick
PatternAufgabe
Timeoutbegrenzt Wartezeit
Retrywiederholt kurzzeitig fehlerhafte Aufrufe
Circuit Breakerstoppt Fehlerkaskaden
Bulkheadbegrenzt parallele Last
Rate Limiterschützt API vor zu vielen Requests
Error Boundaryliefert stabile Fehlerantworten
39. ResilientPaymentGateway
package com.example.order.adapters.resilience;

import com.example.order.domain.Order;
import com.example.order.ports.PaymentAuthorization;
import com.example.order.ports.PaymentGateway;

// 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;

    @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));
        } catch (Exception ex) {
            throw new PaymentProviderUnavailableException("Payment Provider ist aktuell nicht zuverlässig erreichbar.", ex);
        }
    }
}
40. Rate Limiting
package com.example.order.runtime.resilience;

import org.springframework.web.filter.OncePerRequestFilter;

// Pattern: Rate Limiter - schützt die Runtime vor zu vielen gleichzeitigen oder wiederholten Aufrufen.
public final class RateLimitingFilter extends OncePerRequestFilter {
    private final int maxRequestsPerMinute;

    public RateLimitingFilter(int maxRequestsPerMinute) {
        this.maxRequestsPerMinute = maxRequestsPerMinute;
    }
}
41. Stabile Fehlerantworten
package com.example.order.runtime.resilience;

import com.example.order.adapters.resilience.PaymentProviderUnavailableException;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// Pattern: Error Boundary - technische Resilience-Fehler werden in stabile API-Antworten übersetzt.
@RestControllerAdvice
public class ResilienceAdvice {
    @ExceptionHandler(PaymentProviderUnavailableException.class)
    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    ApiError paymentUnavailable(PaymentProviderUnavailableException ex) {
        return new ApiError("PAYMENT_PROVIDER_UNAVAILABLE", ex.getMessage());
    }
}
42. Warum Phase 6 wichtig ist

Resilience ist keine Zusatzdekoration. Sie verhindert, dass ein langsamer Payment Provider die ganze Plattform blockiert. Sie verhindert, dass Retry-Stürme entstehen. Und sie sorgt dafür, dass Clients kontrollierte Fehlermeldungen bekommen.

37. Phase 7: CI/CD und Quality Gates

Phase 7 macht das Projekt lieferfähig. Ein Enterprise-System ist nicht nur Code. Es braucht reproduzierbare Builds, Tests, Artefakte, Security-Prüfungen, Lizenzberichte und Container-Builds.

38. Pipeline als Code
name: ci

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

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
39. SBOM und Lizenzbericht
mvn -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom
mvn -B org.codehaus.mojo:license-maven-plugin:aggregate-add-third-party

SBOM und Lizenzbericht sind für Enterprise wichtig, weil man wissen muss, welche Komponenten, Versionen und Lizenzen im System stecken.

40. Docker Build Gate
FROM eclipse-temurin:21-jre
WORKDIR /app
COPY runtime.jar /app/runtime.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/runtime.jar"]
41. Lokale Quality Gates
./scripts/quality/run-quality-gates.sh

Dieses Script prüft Docker Compose, Maven Verify, SBOM, Lizenzbericht und Docker Build.

42. Warum Phase 7 wichtig ist

Ohne CI/CD bleibt ein Projekt ein lokales Experiment. Mit Quality Gates wird daraus ein prüfbares Lieferpaket. Genau das unterscheidet ein Lernbeispiel von einem Enterprise-nahen Lab.

37. Phase 8: Deployment-Profile

Phase 8 verschiebt den Fokus vom Code in den Betrieb. Eine Enterprise-Anwendung ist erst vollständig verständlich, wenn klar ist, wie sie paketiert, konfiguriert, gestartet, überwacht und aktualisiert wird.

38. Deployment-Zielbild
Developer Laptop
  |
  +-- Docker Compose
  |
  +-- Local Kubernetes
  |       +-- Kustomize overlay local
  |
  +-- OpenShift
  |       +-- Kustomize overlay openshift
  |       +-- Route
  |       +-- restriktiver Security Context
  |
  +-- Helm Release
          +-- values.yaml
          +-- templates
39. Kubernetes Base
apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-runtime
  namespace: order-platform
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
          ports:
            - containerPort: 8080
              name: http
40. Kustomize Overlays

Kustomize trennt gemeinsame Basis und Umgebungsspezifika.

base
  |
  +-- deployment.yaml
  +-- service.yaml
  +-- configmap.yaml
  +-- secret.yaml

overlays/local
  +-- replicas: 1
  +-- imagePullPolicy: Never

overlays/openshift
  +-- Route
  +-- Security Context

overlays/prod
  +-- replicas: 3
  +-- größere Ressourcen
41. OpenShift Route
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: order-runtime
  namespace: order-platform
spec:
  to:
    kind: Service
    name: order-runtime
  port:
    targetPort: http
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
42. Helm Chart

Helm ist sinnvoll, wenn Deployments parametrisiert werden sollen.

replicaCount: 2

image:
  repository: ghcr.io/example/codex-enterprise-maven-v2/order-runtime
  tag: "2.0.0"

service:
  type: ClusterIP
  port: 8080
43. Betriebsprüfungen

Nach einem Deployment sind diese Prüfungen wichtig:

kubectl -n order-platform get deploy,svc,pod
kubectl -n order-platform rollout status deployment/order-runtime
kubectl -n order-platform logs deploy/order-runtime --tail=80
44. Warum Phase 8 wichtig ist

Viele Lernprojekte enden beim Code. Enterprise-Systeme enden dort nicht. Betrieb bedeutet Konfiguration, Secrets, Images, Health Checks, Ressourcen, Netzwerkregeln, Rollouts und Rückfallebene. Phase 8 macht diese Themen sichtbar und dateitauglich.

37. Phase 9: Datenbankbetrieb

Phase 9 ergänzt den Teil, der in vielen Lernprojekten fehlt: den echten Betrieb der Datenbank. In Enterprise-Systemen sind Migrationen, Backup, Restore, Retention und Produktionschecks oft kritischer als der reine Java-Code.

38. Backup erstellen
docker compose up -d postgres
./scripts/database/backup-postgres.sh

Das Script verwendet pg_dump im Custom Format.

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}"
39. Restore durchführen
./scripts/database/restore-postgres.sh database/backups/orderdb-YYYYMMDD-HHMMSS.dump

Ein Backup ist erst dann wertvoll, wenn ein Restore-Test erfolgreich durchgeführt wurde.

40. Flyway History prüfen
./scripts/database/check-flyway-history.sh

Die Flyway History zeigt, welche Migrationen installiert wurden und ob sie erfolgreich waren.

select installed_rank, version, description, type, success, installed_on
from flyway_schema_history
order by installed_rank;
41. Seed-Daten

Seed-Daten gehören getrennt von Produktionsmigrationen behandelt. In diesem Projekt liegen Demo-Daten unter database/seed.

insert into order_platform.orders(order_id, status, total_amount)
values
    ('SEED-ORD-1001', 'ACCEPTED', 49.90),
    ('SEED-ORD-1002', 'ACCEPTED', 129.00)
on conflict(order_id) do nothing;
42. Expand/Contract Migration

Sichere Datenbankänderungen passieren schrittweise.

Release A: neue Spalte hinzufügen
Release B: Anwendung schreibt alte und neue Spalte
Release C: Daten migrieren
Release D: Anwendung liest nur neue Spalte
Release E: alte Spalte entfernen
43. Outbox Retention

Die Outbox darf nicht unbegrenzt wachsen. Veröffentlichte Events können nach einer definierten Zeit archiviert oder gelöscht werden.

select count(*) as deletable_published_events
from outbox.events
where status = 'PUBLISHED'
  and published_at < now() - interval '30 days';
44. Produktionscheckliste

Vor einem Datenbankrelease müssen mindestens diese Punkte erfüllt sein:

  • Backup erfolgreich erstellt
  • Restore-Test durchgeführt
  • Flyway Migrationen geprüft
  • Rollback- oder Forward-Fix-Plan dokumentiert
  • Lock-Risiken bewertet
  • Outbox Backlog geprüft
  • Monitoring und Alerts aktiv
45. Warum Phase 9 wichtig ist

Eine Anwendung kann fachlich korrekt und technisch sauber gebaut sein, aber durch eine schlechte Migration trotzdem ausfallen. Phase 9 macht deshalb Datenbankbetrieb zu einem sichtbaren, dokumentierten und testbaren Teil des Projekts.

46. Phase 10: Produktionsreife und Betriebsabschluss

Phase 10 rundet das Projekt ab. Ein Enterprise-System ist nicht fertig, wenn es lokal startet. Es braucht Betriebsziele, Wiederherstellungspläne, Incident Response, Kostenverständnis und finale Checklisten.

47. Disaster Recovery
Incident erkennen
  |
  v
Schreibzugriffe stoppen
  |
  v
Backup auswählen
  |
  v
Restore isoliert testen
  |
  v
Produktionsrestore ausführen
  |
  v
Outbox Backlog prüfen
  |
  v
Smoke Tests durchführen

Wichtige Kennzahlen:

KennzahlBedeutung
RTOWie lange darf Wiederherstellung dauern?
RPOWie viele Daten dürfen maximal verloren gehen?
48. Incident Response

Ein Incident wird nicht nur technisch gelöst. Er wird geführt, dokumentiert und nachbereitet.

Erkennen -> Eindämmen -> Analysieren -> Beheben -> Verifizieren -> Nachbereiten
49. SLA und SLO

SLOs übersetzen technische Messwerte in Betriebsziele.

SLOBeispiel
Verfügbarkeit99.5 Prozent im Monatsfenster
Latenzp95 unter 500 ms
Fehlerquote5xx unter 1 Prozent
Outbox Backlogunter 100 offene Events
50. Kostenmodell

Kosten entstehen nicht nur durch Server. Typische Kostentreiber sind Hochverfügbarkeit, Speicher, Monitoring-Retention, Backup-Aufbewahrung, Support und Betriebsteam.

51. Finaler Abschluss

Das Projekt enthält jetzt:

  • fachliche Domain und Maven Modulstruktur
  • echte Container-Infrastruktur
  • REST Flow
  • PostgreSQL und Flyway
  • Outbox und RabbitMQ
  • Keycloak Security
  • Testcontainers und E2E Tests
  • Observability
  • Resilience
  • CI/CD
  • Deployment Profile
  • Datenbankbetrieb
  • Disaster Recovery und Incident Response

Damit ist Version 2 als großes Enterprise-Lernprojekt abgeschlossen.