Modern Java Engineering
Chapitre 16
16 - Projet Fil Rouge : Enterprise OrderHub
16 - Projet Fil Rouge : Enterprise OrderHub
Cours 16 : Projet Fil Rouge - Enterprise OrderHub
1. Présentation du projet
1.1 Vision
OrderHub est une plateforme B2B de gestion de commandes enterprise. Elle permet aux entreprises de :
- Créer et gérer des commandes avec un workflow complet
- Gérer les stocks et les inventaires multi-entrepôts
- Traiter les paiements avec différents fournisseurs
- Notifier les clients par email, SMS et push
- Générer des rapports et analytics
1.2 Architecture technique
Stack commune à tous les services
- Spring Boot 3.2.x
- Architecture hexagonale (ports/adapters)
- DDD avec Aggregates, Value Objects, Domain Events
- CQRS/Event Sourcing avec Axon Framework (order-service)
- PostgreSQL pour la persistance principale
- Redis pour le cache et les sessions
- Kafka pour la communication inter-services
- Keycloak pour l'authentification/authorisation
- Docker + Kubernetes pour le déploiement
Principes d'architecture
- Isolation des bounded contexts : Chaque service est autonome
- Communication asynchrone : Kafka pour les événements
- Cohérence éventuelle : Pas de transaction distribuée
- API contractuelle : Contrats OpenAPI partagés
- Observabilité : Logs, metrics, traces (ELK + Prometheus + Jaeger)
2. Structure du projet
2.1 Organisation du monorepo
orderhub/
├── docs/ # Documentation
│ ├── architecture.md
│ ├── api-contracts/
│ └── adr/
├── services/
│ ├── order-service/
│ ├── payment-service/
│ ├── inventory-service/
│ └── notification-service/
├── infrastructure/
│ ├── docker-compose/
│ ├── k8s/
│ └── terraform/
├── libs/
│ ├── shared-types/ # DTOs partagés
│ └── common-test/ # Utilitaires de test
├── build/
│ ├── pom.xml # Parent POM
│ └── .github/
│ └── workflows/ # CI/CD pipelines
└── README.md
2.2 Détail d'un service (order-service)
order-service/
├── src/main/java/com/orderhub/order/
│ ├── OrderServiceApplication.java
│ ├── domain/
│ │ ├── model/
│ │ │ ├── Order.java # Aggregate
│ │ │ ├── OrderLine.java # Entity
│ │ │ ├── OrderId.java # Value Object
│ │ │ ├── Money.java # Value Object
│ │ │ ├── OrderStatus.java # Enum
│ │ │ └── events/
│ │ │ ├── OrderCreatedEvent.java
│ │ │ ├── OrderSubmittedEvent.java
│ │ │ └── OrderCancelledEvent.java
│ │ ├── port/
│ │ │ ├── inbound/
│ │ │ │ ├── CreateOrderUseCase.java
│ │ │ │ ├── SubmitOrderUseCase.java
│ │ │ │ └── GetOrderQuery.java
│ │ │ └── outbound/
│ │ │ ├── OrderRepository.java
│ │ │ └── EventPublisher.java
│ │ └── service/
│ │ └── PricingService.java
│ ├── application/
│ │ └── service/
│ │ ├── CreateOrderService.java
│ │ └── OrderQueryService.java
│ ├── adapter/
│ │ ├── inbound/
│ │ │ ├── rest/
│ │ │ │ ├── OrderController.java
│ │ │ │ └── dto/
│ │ │ └── messaging/
│ │ │ └── OrderEventConsumer.java
│ │ └── outbound/
│ │ ├── persistence/
│ │ │ ├── JpaOrderRepository.java
│ │ │ ├── entity/
│ │ │ └── mapper/
│ │ └── client/
│ │ ├── InventoryServiceClient.java
│ │ └── PaymentServiceClient.java
│ └── config/
│ ├── AxonConfig.java
│ ├── KafkaConfig.java
│ └── SecurityConfig.java
├── src/test/java/
│ ├── domain/ # Tests unitaires (pure domaine)
│ ├── adapter/ # Tests d'intégration (Testcontainers)
│ └── e2e/ # Tests end-to-end
└── pom.xml
3. Implémentation du cœur métier
3.1 Order Aggregate
@Aggregate
public class OrderAggregate {
@AggregateIdentifier
private OrderId orderId;
private OrderStatus status;
private CustomerId customerId;
private Money totalAmount;
private List<OrderLine> lines;
@CommandHandler
public OrderAggregate(CreateOrderCommand cmd) {
AggregateLifecycle.apply(new OrderCreatedEvent(
cmd.orderId(), cmd.customerId(), cmd.lines()));
}
@CommandHandler
public void handle(AddProductCommand cmd) {
if (status != OrderStatus.DRAFT) {
throw new OrderNotModifiableException(orderId, status);
}
AggregateLifecycle.apply(new ProductAddedEvent(
orderId, cmd.productId(), cmd.quantity()));
}
@CommandHandler
public void handle(SubmitOrderCommand cmd) {
if (status != OrderStatus.DRAFT) {
throw new InvalidOrderStateException(orderId, status, "submit");
}
AggregateLifecycle.apply(new OrderSubmittedEvent(orderId, customerId, totalAmount));
}
@EventSourcingHandler
public void on(OrderCreatedEvent event) {
this.orderId = event.orderId();
this.status = OrderStatus.DRAFT;
this.customerId = event.customerId();
this.totalAmount = Money.ZERO;
this.lines = new ArrayList<>();
}
@EventSourcingHandler
public void on(ProductAddedEvent event) {
var line = new OrderLine(event.productId(), event.productName(),
event.price(), event.quantity());
lines.add(line);
totalAmount = totalAmount.add(line.getSubtotal());
}
@EventSourcingHandler
public void on(OrderSubmittedEvent event) {
this.status = OrderStatus.SUBMITTED;
}
}
4. Configuration infrastructure
4.1 Docker Compose global
version: '3.8'
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_MULTIPLE_DATABASES: orders,payments,inventory,notifications
volumes:
- ./init-dbs.sh:/docker-entrypoint-initdb.d/init-dbs.sh
ports:
- "5432:5432"
kafka:
image: confluentinc/cp-kafka:7.6.0
environment:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092,CONTROLLER://0.0.0.0:9093
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
ports:
- "9092:9092"
redis:
image: redis:7-alpine
ports:
- "6379:6379"
keycloak:
image: quay.io/keycloak/keycloak:23.0
environment:
KC_DB: postgres
KC_DB_URL: jdbc:postgresql://postgres:5432/keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
ports:
- "8080:8080"
command: start-dev
prometheus:
image: prom/prometheus:v2.50.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
image: grafana/grafana:latest
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
ports:
- "3000:3000"
4.2 Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
labels:
app: order-service
spec:
replicas: 3
selector:
matchLabels:
app: order-service
template:
metadata:
labels:
app: order-service
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/actuator/prometheus"
spec:
containers:
- name: order-service
image: ghcr.io/orderhub/order-service:latest
ports:
- containerPort: 8080
env:
- name: SPRING_PROFILES_ACTIVE
value: "k8s"
- name: SPRING_DATASOURCE_URL
value: "jdbc:postgresql://postgres:5432/orders"
- name: SPRING_KAFKA_BOOTSTRAP_SERVERS
value: "kafka:9092"
- name: SPRING_REDIS_HOST
value: "redis"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: order-service
spec:
selector:
app: order-service
ports:
- port: 8080
targetPort: 8080
5. CI/CD Pipeline
name: OrderHub CI/CD
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
kafka:
image: confluentinc/cp-kafka:7.6.0
env:
KAFKA_NODE_ID: 1
KAFKA_PROCESS_ROLES: broker,controller
KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
ports:
- 9092:9092
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- run: mvn verify -B -Pintegration-tests
- run: mvn sonar:sonar -Dsonar.host.url=${{ secrets.SONAR_HOST_URL }} -Dsonar.login=${{ secrets.SONAR_TOKEN }}
- run: mvn org.cyclonedx:cyclonedx-maven-plugin:makeBom
- uses: actions/upload-artifact@v4
with:
name: sbom
path: '**/target/bom.json'
native:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: graalvm/setup-graalvm@v1
with:
java-version: '21'
distribution: 'graalvm'
- run: mvn -Pnative native:compile -pl services/order-service
docker:
needs: [build, native]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- run: docker build -t order-service:${{ github.sha }} .
- run: docker tag order-service:${{ github.sha }} ghcr.io/orderhub/order-service:latest
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- run: docker push ghcr.io/orderhub/order-service:latest
6. Monitoring
6.1 Micrometer configuration
@Configuration
public class MonitoringConfig {
@Bean
public MeterRegistry meterRegistry() {
return new CompositeMeterRegistry();
}
@Bean
public TimedAspect timedAspect(MeterRegistry registry) {
return new TimedAspect(registry);
}
}
// Custom metrics
@Component
public class OrderMetrics {
private final Counter orderCreatedCounter;
private final Timer orderProcessingTimer;
private final DistributionSummary orderAmountSummary;
public OrderMetrics(MeterRegistry registry) {
this.orderCreatedCounter = Counter.builder("order.created")
.description("Number of created orders")
.register(registry);
this.orderProcessingTimer = Timer.builder("order.processing.time")
.description("Time to process an order")
.register(registry);
this.orderAmountSummary = DistributionSummary.builder("order.amount")
.description("Distribution of order amounts")
.baseUnit("EUR")
.register(registry);
}
@Timed(value = "order.create", percentiles = {0.5, 0.95, 0.99})
public Order createOrder(CreateOrderCommand cmd) {
return orderCreatedCounter.increment();
// ...
}
}
6.2 Prometheus configuration
# prometheus.yml
scrape_configs:
- job_name: 'orderhub-services'
metrics_path: '/actuator/prometheus'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
Points clés
- Monorepo avec Maven multi-module
- Architecture hexagonale pour chaque service
- CQRS/ES avec Axon pour le service commande
- Communication asynchrone via Kafka
- Sécurité centralisée avec Keycloak
- Tests avec Testcontainers pour tous les services
- GraalVM native-image pour les démarrages rapides
- Kubernetes avec health checks et auto-scaling
- Monitoring complet (logs, métriques, traces)