MFormations
Modern Java Engineering

Chapitre 9

09 - Architecture Java

09 - Architecture Java

Cours 09 : Architecture Java

1. Introduction à l'architecture logicielle

1.1 Pourquoi l'architecture ?

L'architecture logicielle est la fondation de tout système maintenable et évolutif. Elle définit :

  • La structure du système (composants, connecteurs, données)
  • Les propriétés non-fonctionnelles (performance, sécurité, maintenabilité)
  • Les contraintes techniques et métier
  • Les décisions de conception fondamentales

1.2 Évolution des architectures Java

  • 2000s : Architecture en couches (Controller → Service → DAO → DB)
  • 2010s : Architecture microservices, hexagonal
  • 2020s : Événementielle, CQRS/ES, Serverless

1.3 Problèmes de l'architecture en couches classique

  • Couplage fort à la base de données
  • Logique métier dispersée dans les services
  • Tests difficiles (nécessité de la DB)
  • Changements technologiques impactant toute l'application
  • Pas d'isolation du domaine métier

2. Architecture Hexagonale (Ports/Adapters)

2.1 Principe fondamental

L'architecture hexagonale, proposée par Alistair Cockburn, repose sur l'idée que l'application doit être pilotée par son domaine métier, indépendamment des technologies externes.

2.2 Structure

┌─────────────────────────────────────┐
│         Ports (interfaces)          │
├─────────────────────────────────────┤
│          Domaine métier             │
│    (Entités, Value Objects,         │
│     Services domaine)               │
├─────────────────────────────────────┤
│         Ports (interfaces)          │
├─────────────────────────────────────┤
│         Adapters (implémentations)  │
│    - Adapters primaires (API)       │
│    - Adapters secondaires (DB, ...) │
└─────────────────────────────────────┘

2.3 Règles d'or

  • Le code du domaine ne doit RIEN importer des infrastructures externes
  • Les dépendances pointent VERS le domaine (règle de dépendance)
  • Les adapters implémentent des interfaces (ports) définies par le domaine
  • Le domaine est complètement testable sans infrastructure

2.4 Ports (interfaces)

Les ports sont des contrats définis par le domaine métier :

  • Ports primaires (inbound) : Cas d'utilisation, services applicatifs
public interface CreateOrderUseCase {
    Order createOrder(CreateOrderCommand command);
}
  • Ports secondaires (outbound) : Dépendances vers l'extérieur
public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
}

2.5 Adapters (implémentations)

  • Adapters primaires : Contrôleurs REST, listeners JMS, scheduled tasks
  • Adapters secondaires : Implémentations JPA, clients REST, repositories
@RestController
public class OrderController { // Primary adapter
    private final CreateOrderUseCase createOrderUseCase;
    
    @PostMapping("/orders")
    public ResponseEntity<OrderResponse> create(@RequestBody CreateOrderRequest request) {
        Order order = createOrderUseCase.create(request.toCommand());
        return ResponseEntity.ok(OrderResponse.from(order));
    }
}

@Repository
public class JpaOrderRepository implements OrderRepository { // Secondary adapter
    private final SpringDataJpaOrderRepository springRepo;
    
    @Override
    public Order save(Order order) {
        OrderEntity entity = OrderEntity.from(order);
        return springRepo.save(entity).toDomain();
    }
}

2.6 Package structure hexagonale

com.orderhub
├── domain/
│   ├── model/           # Aggregates, Entities, Value Objects
│   ├── port/
│   │   ├── inbound/     # Use cases
│   │   └── outbound/    # Repository interfaces
│   └── service/         # Domain services
├── application/
│   └── service/         # Application services (orchestration)
├── adapter/
│   ├── inbound/
│   │   ├── rest/        # REST controllers
│   │   ├── messaging/   # Kafka/JMS listeners
│   │   └── scheduler/   # Scheduled tasks
│   └── outbound/
│       ├── persistence/ # JPA/NoSQL implementations
│       └── client/      # External service clients
└── shared/
    └── annotation/      # Custom annotations

3. Domain-Driven Design (DDD)

3.1 Concepts fondamentaux du DDD

Eric Evans a formalisé le DDD dans son livre "Domain-Driven Design" (2003). Les concepts clés :

3.2 Ubiquitous Language

  • Langage commun entre développeurs et experts métier
  • Utilisé dans le code, les spécifications, les discussions
  • Évolue avec la compréhension du domaine

3.3 Bounded Context

  • Frontière explicite autour d'un sous-domaine
  • Chaque BC a son propre langage ubiquitaire
  • Les BC communiquent via des événements ou des APIs

3.4 Aggregate

Racine d'un groupe cohérent d'entités et de value objects. Règles :

  • L'Aggregate Root est le point d'entrée unique
  • Les modifications se font via l'Aggregate Root uniquement
  • Un aggregate est une unité de consistence transactionnelle
public class Order {
    private OrderId id;
    private OrderStatus status;
    private CustomerId customerId;
    private Money totalAmount;
    private List<OrderLine> lines;
    
    public Order(CustomerId customerId) {
        this.id = OrderId.generate();
        this.status = OrderStatus.DRAFT;
        this.customerId = customerId;
        this.lines = new ArrayList<>();
        this.totalAmount = Money.ZERO;
    }
    
    public void addProduct(ProductId productId, String productName, Money price, int quantity) {
        if (status != OrderStatus.DRAFT) {
            throw new OrderNotModifiableException(id);
        }
        OrderLine line = new OrderLine(productId, productName, price, quantity);
        lines.add(line);
        totalAmount = totalAmount.add(line.getSubtotal());
        registerEvent(new ProductAddedToOrderEvent(id, productId, quantity));
    }
    
    public void submit() {
        if (lines.isEmpty()) {
            throw new EmptyOrderException(id);
        }
        this.status = OrderStatus.SUBMITTED;
        registerEvent(new OrderSubmittedEvent(id, customerId, totalAmount));
    }
}

3.5 Value Object

Objet immutable défini par ses attributs, sans identité propre.

@Value
public class Money {
    BigDecimal amount;
    Currency currency;
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException(this.currency, other.currency);
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
}

@Value
public class Address {
    String street;
    String city;
    String zipCode;
    String country;
}

3.6 Domain Event

Événement représentant un fait passé dans le domaine.

public record OrderSubmittedEvent(
    OrderId orderId,
    CustomerId customerId,
    Money totalAmount,
    Instant occurredAt
) implements DomainEvent {
    public OrderSubmittedEvent {
        Objects.requireNonNull(orderId);
        Objects.requireNonNull(customerId);
        Objects.requireNonNull(totalAmount);
    }
}

3.7 Domain Service

Service sans état qui encapsule une logique métier ne trouvant pas naturellement sa place dans un Aggregate.

public class PricingService {
    private final List<DiscountStrategy> discountStrategies;
    
    public Money calculateFinalPrice(Order order, Customer customer) {
        Money basePrice = order.getTotalAmount();
        Money discount = discountStrategies.stream()
            .map(s -> s.apply(basePrice, customer))
            .reduce(Money.ZERO, Money::add);
        return basePrice.subtract(discount);
    }
}

3.8 Repository (port)

Interface de persistance définie dans le domaine.

public interface OrderRepository {
    Order save(Order order);
    Optional<Order> findById(OrderId id);
    Page<Order> findByCustomerId(CustomerId customerId, Pageable pageable);
    void delete(OrderId id);
}

3.9 Factory

Pattern pour encapsuler la création d'objets complexes.

public class OrderFactory {
    public static Order createDraftOrder(CustomerId customerId) {
        return Order.builder()
            .id(OrderId.generate())
            .status(OrderStatus.DRAFT)
            .customerId(customerId)
            .createdAt(Instant.now())
            .build();
    }
}

4. CQRS (Command Query Responsibility Segregation)

4.1 Principe

CQRS sépare les opérations de lecture (queries) des opérations d'écriture (commands).

4.2 Commandes

public record CreateOrderCommand(
    CustomerId customerId,
    List<OrderLineCommand> lines
) implements Command {}

public record AddProductCommand(
    OrderId orderId,
    ProductId productId,
    int quantity
) implements Command {}

4.3 Queries

public record GetOrderQuery(OrderId orderId) implements Query {}
public record FindOrdersByCustomerQuery(CustomerId customerId, Pageable pageable) implements Query {}

4.4 Command Handlers

@Component
public class CreateOrderCommandHandler implements CommandHandler<CreateOrderCommand, Order> {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    @Override
    public Order handle(CreateOrderCommand command) {
        Order order = new Order(command.customerId());
        command.lines().forEach(line -> {
            Product product = productRepository.findById(line.productId())
                .orElseThrow(() -> new ProductNotFoundException(line.productId()));
            order.addProduct(product.getId(), product.getName(), product.getPrice(), line.quantity());
        });
        order.submit();
        return orderRepository.save(order);
    }
}

4.5 Query Handlers

@Component
public class GetOrderQueryHandler implements QueryHandler<GetOrderQuery, OrderView> {
    private final OrderViewRepository orderViewRepository;
    
    @Override
    public OrderView handle(GetOrderQuery query) {
        return orderViewRepository.findById(query.orderId())
            .orElseThrow(() -> new OrderNotFoundException(query.orderId()));
    }
}

4.6 Axon Framework

Axon facilite l'implémentation de CQRS/ES.

@Aggregate
public class OrderAggregate {
    @AggregateIdentifier
    private OrderId orderId;
    private OrderStatus status;
    
    @CommandHandler
    public OrderAggregate(CreateOrderCommand cmd) {
        apply(new OrderCreatedEvent(cmd.orderId(), cmd.customerId()));
    }
    
    @EventSourcingHandler
    public void on(OrderCreatedEvent event) {
        this.orderId = event.orderId();
        this.status = OrderStatus.DRAFT;
    }
    
    @CommandHandler
    public void handle(AddProductCommand cmd) {
        apply(new ProductAddedEvent(orderId, cmd.productId(), cmd.quantity()));
    }
}

4.7 Avantages et inconvénients de CQRS

Avantages :

  • Séparation des responsabilités lecture/écriture
  • Optimisation indépendante des modèles de lecture
  • Scalabilité (lecture et écriture évoluent séparément)

Inconvénients :

  • Complexité accrue
  • Cohérence éventuelle (eventual consistency)
  • Duplication de code

5. Event Sourcing

5.1 Principe

Au lieu de stocker l'état courant, on stocke la séquence d'événements qui ont conduit à cet état.

5.2 Stockage d'événements

@Entity
public class DomainEventEntry {
    @Id
    private String eventId;
    private String aggregateId;
    private String aggregateType;
    private String eventType;
    @Lob
    private String payload; // JSON serialized
    private Instant timestamp;
    private int version;
}

5.3 Event Store

public interface EventStore {
    void save(List<DomainEvent> events);
    List<DomainEvent> findByAggregateId(String aggregateId);
    List<DomainEvent> findByType(String eventType, Instant from, Instant to);
}

5.4 Reconstituer l'état (Rehydration)

public class OrderProjection {
    public static Order recreateFrom(List<DomainEvent> events) {
        Order order = new Order();
        events.forEach(event -> {
            if (event instanceof OrderCreatedEvent e) {
                order = new Order(e.orderId(), e.customerId());
            } else if (event instanceof ProductAddedEvent e) {
                order.addProduct(e.productId(), e.quantity());
            } else if (event instanceof OrderSubmittedEvent e) {
                order.submit();
            }
        });
        return order;
    }
}

5.5 Snapshots

Pour éviter de rejouer tous les événements à chaque fois :

public record Snapshot(
    String aggregateId,
    int version,
    byte[] state
) {}

6. Clean Architecture

6.1 Principe (Robert C. Martin)

La Clean Architecture est une généralisation des architectures Hexagonale, Onion, et Ports/Adapters.

6.2 Règle de dépendance

Les dépendances source pointent vers l'intérieur. Rien dans le cercle intérieur ne peut connaître quoi que ce soit du cercle extérieur.

6.3 Les cercles

┌───────────────────────────────┐
│   Frameworks & Drivers        │  Couche externe
│  (DB, UI, Devices)            │
├───────────────────────────────┤
│   Interface Adapters          │
│  (Controllers, Presenters,    │
│   Gateways)                   │
├───────────────────────────────┤
│   Application Business Rules  │
│  (Use Cases)                  │
├───────────────────────────────┤
│   Enterprise Business Rules   │
│  (Entities, Value Objects)    │  Centre
└───────────────────────────────┘

6.4 Exemple de cas d'utilisation

public class CreateOrderUseCase {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final EventBus eventBus;
    
    public Order execute(CreateOrderInput input) {
        CustomerId customerId = new CustomerId(input.customerId());
        Order order = new Order(customerId);
        
        for (ProductInput productInput : input.products()) {
            Product product = productRepository.findById(new ProductId(productInput.productId()))
                .orElseThrow(() -> new ProductNotFoundException(productInput.productId()));
            order.addProduct(product.getId(), product.getName(), product.getPrice(), productInput.quantity());
        }
        
        order.submit();
        Order saved = orderRepository.save(order);
        eventBus.publish(new OrderCreatedEvent(saved.getId(), saved.getCustomerId()));
        return saved;
    }
}

7. Structure par feature

7.1 Principe

Organiser le code par fonctionnalité métier plutôt que par couche technique.

7.2 Avantages

  • Cohésion forte : tout ce qui concerne une feature est au même endroit
  • Couplage faible entre features
  • Navigation facilitée
  • Parallélisation du développement

7.3 Exemple de structure

com.orderhub
├── order/
│   ├── Order.java              # Aggregate
│   ├── OrderLine.java          # Entity
│   ├── OrderId.java            # Value Object
│   ├── OrderStatus.java        # Enum
│   ├── OrderRepository.java    # Port
│   ├── CreateOrderUseCase.java # Use case
│   ├── OrderController.java    # Adapter REST
│   ├── JpaOrderRepository.java # Adapter JPA
│   └── OrderMapper.java
├── product/
│   ├── Product.java
│   ├── ProductId.java
│   ├── ProductRepository.java
│   └── ProductController.java
├── customer/
│   ├── Customer.java
│   ├── CustomerId.java
│   ├── CustomerRepository.java
│   └── CustomerController.java
└── shared/
    ├── Money.java
    ├── Address.java
    └── DomainEvent.java

7.4 Comparaison : par couche vs par feature

Par couche :                    Par feature :
├── controller/                 ├── order/
│   ├── OrderController.java    │   ├── OrderController.java
│   ├── ProductController.java  │   ├── OrderService.java
├── service/                    │   ├── OrderRepository.java
│   ├── OrderService.java       │   ├── Order.java
│   ├── ProductService.java     │   └── OrderLine.java
├── repository/                 ├── product/
│   ├── JpaOrderRepository.java │   ├── ProductController.java
│   ├── JpaProductRepository.java│  ├── ProductService.java
├── model/                      │   ├── ProductRepository.java
│   ├── Order.java              │   └── Product.java
│   ├── Product.java            └── ...

8. Cas pratique : OrderHub

8.1 Architecture globale

┌──────────┐    ┌──────────┐    ┌──────────┐
│  Mobile  │    │   Web    │    │  API     │
└────┬─────┘    └────┬─────┘    └────┬─────┘
     └───────────────┼───────────────┘
                     ▼
           ┌─────────────────┐
           │   API Gateway   │
           └────────┬────────┘
                    ▼
           ┌─────────────────┐
           │   Order Service │
           │  (Hexagonal)    │
           └──┬──────────┬───┘
              ▼          ▼
      ┌─────────┐  ┌─────────┐
      │Postgres │  │  Kafka  │
      └─────────┘  └─────────┘

8.2 Structure du projet

order-service/
├── src/main/java/com/orderhub/
│   ├── OrderServiceApplication.java
│   ├── order/
│   │   ├── domain/
│   │   │   ├── model/
│   │   │   ├── port/
│   │   │   └── service/
│   │   ├── application/
│   │   ├── adapter/
│   │   │   ├── inbound/
│   │   │   └── outbound/
│   │   └── config/
│   └── shared/
└── src/test/java/
    ├── domain/   # Tests unitaires (unitaire, sans infra)
    ├── adapter/  # Tests d'intégration
    └── e2e/      # Tests end-to-end

8.3 Règles de conception

  1. Le domaine ne dépend d'aucun framework
  2. Les repositories sont des interfaces dans le domaine
  3. Les entités JPA sont des implémentations techniques
  4. Les use cases orchestrent le domaine
  5. Les événements sont le seul couplage entre bounded contexts
  6. Les DTOs de API sont distincts des objets du domaine

Points clés à retenir

  • L'architecture hexagonale isole le domaine métier des concerns techniques
  • DDD fournit les concepts pour modéliser proprement le métier
  • CQRS sépare lectures et écritures
  • Event Sourcing persiste les événements plutôt que l'état
  • La Clean Architecture généralise ces principes
  • La structure par feature améliore la navigabilité et la cohésion

Aller plus loin