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
- Le domaine ne dépend d'aucun framework
- Les repositories sont des interfaces dans le domaine
- Les entités JPA sont des implémentations techniques
- Les use cases orchestrent le domaine
- Les événements sont le seul couplage entre bounded contexts
- 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
- Livre : "Domain-Driven Design" d'Eric Evans
- Livre : "Implementing Domain-Driven Design" de Vaughn Vernon
- Livre : "Clean Architecture" de Robert C. Martin
- Site : https://axonframework.org
- Article : https://alistair.cockburn.us/hexagonal-architecture/