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 découpée selon les principes de l'architecture hexagonale :
des ports (interfaces) séparent le domaine de l'infrastructure, et les services existants sont refactorisés en services applicatifs qui orchestrent les cas d'usage via ces ports.

Ports

  • Définir 5 driving ports (AccountUseCases, CardUseCases, CustomerUseCases, TransactionUseCases, TransferUseCases dans banking-core/port/in/)
  • Définir 8 driven ports (AccountRepository, BeneficiaryRepository, CardRepository, CustomerRepository, TransactionRepository, TransferRepository, FraudDetection, NotificationSender dans banking-core/port/out/)

Application

  • Créer 5 services applicatifs (XxxService refactorisé en XxxApplicationService dans banking-service/application/)

Adapters

  • Déplacer 6 adaptateurs JPA (XxxRepository renommé en JpaXxxRepository dans banking-persistence/)
  • Driving ports (ports entrants) : interfaces qui exposent les cas d'usage au monde extérieur, par exemple AccountUseCases
  • Driven ports (ports sortants) : interfaces qui abstraient les dépendances externes, par exemple AccountRepository ou FraudDetection

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.
AccountUseCases.java - Driving Port
public interface AccountUseCases {
AccountId openAccount(CustomerId customerId, AccountType type, String currency);
void deposit(AccountId accountId, BigDecimal amount);
void withdraw(AccountId accountId, BigDecimal amount);
Account getAccount(AccountId accountId);
List<Account> getAccountsByCustomer(CustomerId customerId);
void closeAccount(AccountId accountId);
}
  • L'interface ne contient que du métier : pas d'annotation Spring, pas d'import JPA
  • Les types utilisés (AccountId, CustomerId) 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 (AccountId, BeneficiaryId, CardId, CustomerId, TransactionId, TransferId (records Java))
  • Créer 4 value objects (Money, Iban, Email, Address avec validation et comportement métier)
  • Purifier 6 entités domaine (suppression @Entity / @Id, ajout logique métier (Account.deposit(), Account.withdraw()))
  • Supprimer 2 classes utilitaires (IbanUtils et MoneyUtils remplacés par les value objects Iban et Money)

Adapters

  • Créer 6 entités JPA séparées (JpaAccount, JpaBeneficiary, JpaCard, JpaCustomer, JpaTransaction, JpaTransfer dans banking-persistence/entity/)

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, 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 vérifier la devise
  • Entités domaine purifiées : protègent leurs invariants via des méthodes métier (deposit(), withdraw())
  • Classes utilitaires supprimées : IbanUtils et MoneyUtils disparaissent au profit des value objects
Code

Identifiants typés et value objects

Deux patterns clés illustrent la purification du domaine.
HexaGlue détecte ces structures automatiquement et classe AccountId comme IDENTIFIER et Money comme VALUE_OBJECT.
AccountId.java - Identifiant typé
public record AccountId(Long value) {
public AccountId {
Objects.requireNonNull(value, "AccountId value must not be null");
}
}
Money.java - Value Object
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount, "amount must not be null");
Objects.requireNonNull(currency, "currency must not be null");
if (amount.scale() > 2) {
throw new IllegalArgumentException("amount scale must be <= 2");
}
}
public Money add(Money other) {
requireSameCurrency(other);
return new Money(amount.add(other.amount), currency);
}
public Money subtract(Money other) {
requireSameCurrency(other);
return new Money(amount.subtract(other.amount), currency);
}
private void requireSameCurrency(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
}
}
  • Le record remplace le Long générique : on ne peut plus confondre un AccountId 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.subtract() 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