MFormations
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

  1. Isolation des bounded contexts : Chaque service est autonome
  2. Communication asynchrone : Kafka pour les événements
  3. Cohérence éventuelle : Pas de transaction distribuée
  4. API contractuelle : Contrats OpenAPI partagés
  5. 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)