Migration hexagonale

Migration hexagonale

Deux étapes fondamentales : la restructuration hexagonale pose le squelette des ports et services applicatifs, puis la purification du domaine élimine toutes les violations DDD.

Restructuration

Restructuration hexagonale

L'application est réorganisée selon les principes de l'architecture hexagonale :
les packages domain/, ports/, application/ et infrastructure/ séparent le domaine de l'infrastructure, et les services existants sont refactorisés en services applicatifs qui orchestrent les cas d'usage via des ports.

Domain

  • Déplacer 13 classes domaine (vers domain/order/, domain/customer/, domain/product/, domain/inventory/, domain/payment/, domain/shipping/)

Ports

  • Définir 7 driving ports (OrderUseCases, CustomerUseCases, ProductUseCases, InventoryUseCases, PaymentUseCases, ShippingUseCases, CartUseCases dans ports/in/)
  • Définir 8 driven ports (OrderRepository, CustomerRepository, ProductRepository, InventoryRepository, PaymentRepository, ShipmentRepository, NotificationSender, PaymentGateway dans ports/out/)

Application

  • Créer 7 services applicatifs (OrderApplicationService, CustomerApplicationService, etc. dans application/)

Adapters

  • Implémenter 6 adaptateurs JPA (JpaOrderRepository extends JpaRepository, OrderRepository dans infrastructure/persistence/)
  • Implémenter 2 adaptateurs externes (PaymentGatewayAdapter, NotificationAdapter dans infrastructure/external/)
  • Driving ports (ports entrants) : interfaces qui exposent les cas d'usage au monde extérieur, par exemple OrderUseCases
  • Driven ports (ports sortants) : interfaces qui abstraient les dépendances externes, par exemple OrderRepository ou NotificationSender

Cette inversion de dépendance permet au domaine de ne dépendre que d'abstractions, pas d'implémentations concrètes.

Code

Exemple de driving port

Un driving port définit un cas d'usage métier sans référence à une technologie.
HexaGlue détecte automatiquement ce pattern et classe l'interface comme DRIVING_PORT.
OrderUseCases.java - Driving Port
public interface OrderUseCases {
OrderId createOrder(CustomerId customerId, String currency);
void addLine(OrderId orderId, ProductId productId, Quantity quantity, Money unitPrice);
void placeOrder(OrderId orderId, Address shippingAddress);
void cancelOrder(OrderId orderId, String reason);
Order getOrder(OrderId orderId);
List<Order> getOrdersByCustomer(CustomerId customerId);
}
  • L'interface ne contient que du métier : pas d'annotation Spring, pas d'import JPA
  • Les types utilisés (OrderId, CustomerId, Money) sont des concepts domaine, pas des primitifs
  • HexaGlue détecte ce pattern et le fait apparaître dans l'inventaire architectural comme DrivingPort
Purification

Purification du domaine

Le domaine est purifié : toutes les annotations JPA sont supprimées et remplacées par des patterns tactiques DDD.
Les identifiants typés et les value objects permettent à HexaGlue d'inférer la classification complète sans aucune annotation explicite.

Domain

  • Créer 6 identifiants typés (OrderId, CustomerId, ProductId, PaymentId, ShipmentId, InventoryId (records Java avec validation))
  • Créer 4 value objects (Money, Address, Email, Quantity avec validation et comportement métier)
  • Créer 1 domain event (OrderPlacedEvent capturant le fait métier avec OrderId, CustomerId et Money)
  • Purifier 9 classes domaine (suppression @Entity / @ManyToOne / extends BaseEntity, ajout logique métier (place(), cancel()...))
  • Supprimer BaseEntity (chaque agrégat définit son propre identifiant typé, la hiérarchie technique disparaît)

Adapters

  • Supprimer 6 repositories JPA (JpaOrderRepository, JpaCustomerRepository, etc. supprimés car le domaine purifié n'est plus compatible)

C'est la transformation fondamentale du DDD : séparer le modèle métier de l'infrastructure.

  • Identifiants typés (records) : remplacent les Long génériques issus de BaseEntity, chaque agrégat a son propre type d'identifiant
  • Value objects : encapsulent les règles de validation et le comportement, par exemple Money sait additionner et multiplier des montants
  • Agrégats purifiés : protègent leurs invariants via des méthodes métier (place(), markPaid(), cancel())
  • Repositories JPA supprimés : le domaine purifié n'est plus compatible avec les anciens adaptateurs, ils seront régénérés par HexaGlue
Code

Identifiants typés et value objects

Deux patterns clés illustrent la purification du domaine.
HexaGlue détecte ces structures automatiquement et classe OrderId comme IDENTIFIER et Money comme VALUE_OBJECT.
OrderId.java - Identifiant typé
public record OrderId(Long value) {
public OrderId {
if (value == null) {
throw new IllegalArgumentException("OrderId value must not be null");
}
}
}
Money.java - Value Object
public record Money(BigDecimal amount, String currency) {
public Money {
if (amount == null) throw new IllegalArgumentException("Amount must not be null");
if (currency == null) throw new IllegalArgumentException("Currency must not be null");
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
public static Money zero(String currency) {
return new Money(BigDecimal.ZERO, currency);
}
public Money add(Money other) {
requireSameCurrency(other);
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int quantity) {
return new Money(this.amount.multiply(BigDecimal.valueOf(quantity)), this.currency);
}
private void requireSameCurrency(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
}
}
  • Le record remplace le Long générique hérité de BaseEntity : on ne peut plus confondre un OrderId avec un CustomerId à la compilation
  • La validation au constructeur garantit qu'aucun identifiant nul ni montant invalide ne circule dans le domaine
  • Money.add() et Money.multiply() retournent une nouvelle instance : l'immutabilité est garantie par le record
  • Ces deux patterns sont détectés automatiquement par HexaGlue sans aucune annotation supplémentaire