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.
| System | Rolle im Lab | Port |
|---|---|---|
| PostgreSQL | Orders und Outbox speichern | 5432 |
| Adminer | Datenbank ansehen | 8081 |
| RabbitMQ | Events und Queues | 5672 / 15672 |
| Keycloak | OAuth2/OIDC | 8082 |
| WireMock | Payment Provider Mock | 8089 |
| MailHog | E-Mail Simulation | 1025 / 8025 |
| Prometheus | Metriken sammeln | 9090 |
| Grafana | Dashboard anzeigen | 3000 |
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-readerorder-adminbilling-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:
- echte RabbitMQ Publisher/Consumer ergänzen
- Outbox Relay implementieren
- Keycloak-Rollen in Controller absichern
- Contract Tests für WireMock ergänzen
- Grafana Dashboard erweitern
- E2E Test für REST + DB + Outbox schreiben
- 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 Rolle | Spring Authority | Bedeutung |
|---|---|---|
order-reader | ROLE_ORDER_READER | Bestellungen lesen |
order-admin | ROLE_ORDER_ADMIN | Bestellungen annehmen |
billing-user | ROLE_BILLING_USER | Billing-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
| Pattern | Aufgabe |
|---|---|
| Timeout | begrenzt Wartezeit |
| Retry | wiederholt kurzzeitig fehlerhafte Aufrufe |
| Circuit Breaker | stoppt Fehlerkaskaden |
| Bulkhead | begrenzt parallele Last |
| Rate Limiter | schützt API vor zu vielen Requests |
| Error Boundary | liefert 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:
| Kennzahl | Bedeutung |
|---|---|
| RTO | Wie lange darf Wiederherstellung dauern? |
| RPO | Wie 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.
| SLO | Beispiel |
|---|---|
| Verfügbarkeit | 99.5 Prozent im Monatsfenster |
| Latenz | p95 unter 500 ms |
| Fehlerquote | 5xx unter 1 Prozent |
| Outbox Backlog | unter 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.