🏗️ Architecture Hexagonale en Java : Structurer vos APIs pour la maintenabilité

Après avoir exploré les performances de Quarkus, parlons d’un aspect tout aussi crucial : comment bien structurer votre code d’API. L’architecture hexagonale (ou “Ports & Adapters”) n’est pas qu’un concept théorique - c’est une approche pragmatique qui transforme la maintenabilité de vos projets Java.

🎯 Le problème des architectures traditionnelles

Combien de fois avez-vous vu des projets Spring Boot qui finissent comme ça :

  • Controllers obèses : Logique métier mélangée avec la gestion HTTP
  • Services anémiques : Juste des CRUD qui mappent vers la base de données
  • Couplage fort : Impossible de tester sans démarrer toute l’application
  • Dette technique : Chaque nouvelle feature devient plus compliquée à implémenter
  • Tests fragiles : Modification d’une dépendance = explosion des tests

Le problème ? La logique métier est dispersée partout, couplée aux détails techniques (base de données, HTTP, etc.). Résultat : un code difficile à maintenir et faire évoluer.

🔄 L’architecture hexagonale : une séparation claire

Le principe fondamental

L’idée est simple : isoler la logique métier au centre, et découpler tout le reste (base de données, API REST, messaging, etc.) via des interfaces.

1
2
3
4
5
6
7
┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│   Controllers   │───▶│   Use Cases     │───▶│   Repositories  │
│   (Adapters)    │    │   (Domain)      │    │   (Adapters)    │
└─────────────────┘    └─────────────────┘    └─────────────────┘
        │                       │                       │
        ▼                       ▼                       ▼
   HTTP/REST              Logique métier           Base de données

Les avantages concrets

Tests isolés : Testez votre logique métier sans démarrer l’appli
Flexibilité : Changez de base de données sans impacter le métier
Évolutivité : Ajoutez facilement de nouveaux canaux d’entrée (GraphQL, gRPC, etc.)
Maintenance : Code organisé, responsabilités claires
Réutilisabilité : La logique métier peut être réutilisée dans différents contextes

🏛️ Structure concrète d’un projet hexagonal

Organisation des packages

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
org.acme.demo/
├── domain/
│   ├── model/          # Entités métier
│   ├── port/           # Interfaces (ports)
│   └── service/        # Use cases / Services métier
├── infrastructure/
│   ├── adapter/
│   │   ├── in/         # Adapters entrants (REST, GraphQL)
│   │   └── out/        # Adapters sortants (BDD, APIs externes)
│   └── config/         # Configuration Spring/Quarkus
└── application/        # Point d'entrée de l'application

Les couches expliquées

Domain (le cœur) :

  • Entités : Objets métier avec leurs règles
  • Ports : Interfaces définissant les contrats
  • Use Cases : Logique métier pure, sans dépendances techniques

Infrastructure (la périphérie) :

  • Adapters In : Controllers REST, endpoints GraphQL, consumers de messages
  • Adapters Out : Repositories JPA, clients HTTP, publishers de messages
  • Config : Injection de dépendances, configuration technique

🛠️ Exemple concret avec Quarkus

1. Entité métier (Domain)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// domain/model/User.java
public class User {
    private final UserId id;
    private final Email email;
    private final UserStatus status;
    
    // Logique métier dans l'entité
    public void activate() {
        if (this.status == UserStatus.SUSPENDED) {
            throw new BusinessException("Cannot activate suspended user");
        }
        this.status = UserStatus.ACTIVE;
    }
}

2. Port (Interface du domaine)

1
2
3
4
5
6
// domain/port/UserRepository.java
public interface UserRepository {
    Optional<User> findById(UserId id);
    void save(User user);
    List<User> findByStatus(UserStatus status);
}

3. Use Case (Logique métier)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// domain/service/ActivateUserUseCase.java
@ApplicationScoped
public class ActivateUserUseCase {
    private final UserRepository userRepository;
    
    public void execute(UserId userId) {
        User user = userRepository.findById(userId)
            .orElseThrow(() -> new UserNotFoundException(userId));
        
        user.activate(); // Logique métier dans l'entité
        userRepository.save(user);
    }
}

4. Adapter REST (Infrastructure)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// infrastructure/adapter/in/UserController.java
@RestController
@Path("/users")
public class UserController {
    private final ActivateUserUseCase activateUserUseCase;
    
    @POST
    @Path("/{id}/activate")
    public Response activateUser(@PathParam("id") String id) {
        activateUserUseCase.execute(new UserId(id));
        return Response.ok().build();
    }
}

5. Adapter Repository (Infrastructure)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// infrastructure/adapter/out/JpaUserRepository.java
@ApplicationScoped
public class JpaUserRepository implements UserRepository {
    @Inject
    EntityManager em;
    
    @Override
    public Optional<User> findById(UserId id) {
        UserEntity entity = em.find(UserEntity.class, id.getValue());
        return entity != null ? Optional.of(entity.toDomain()) : Optional.empty();
    }
}

🧪 Tests : le vrai avantage

Test unitaire du Use Case

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Test
void should_activate_user_when_user_exists() {
    // Given
    User user = new User(userId, email, UserStatus.INACTIVE);
    when(userRepository.findById(userId)).thenReturn(Optional.of(user));
    
    // When
    activateUserUseCase.execute(userId);
    
    // Then
    verify(userRepository).save(argThat(u -> u.getStatus() == UserStatus.ACTIVE));
}

Avantages :

  • Pas de démarrage d’application
  • Test rapide (< 100ms)
  • Logique métier isolée
  • Mocks simples des dépendances

Test d’intégration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@QuarkusTest
class UserControllerIT {
    @Test
    void should_activate_user_via_rest_api() {
        given()
            .when().post("/users/123/activate")
            .then().statusCode(200);
        
        // Vérifier en base que l'utilisateur est activé
    }
}

💡 Quarkus + Architecture Hexagonale : le combo gagnant

Pourquoi ça marche bien ensemble ?

Injection de dépendances native :

  • @ApplicationScoped pour les use cases
  • @Inject pour l’injection automatique des ports
  • Configuration par annotations, pas de XML

Performance :

  • Compilation native préserve l’architecture
  • Startup rapide même avec une structure complexe
  • Reflection eliminée, interfaces optimisées

Testing intégré :

  • @QuarkusTest pour les tests d’intégration
  • @TestProfile pour différents environnements de test
  • Mocking natif avec Mockito

📊 Cas d’usage idéaux

APIs complexes avec logique métier riche

  • Applications e-commerce (gestion commandes, pricing, stock)
  • Systèmes de gestion (CRM, ERP, etc.)
  • APIs avec règles métier évolutives

Projets multi-équipes

  • Séparation claire des responsabilités
  • Équipe domain vs équipe infrastructure
  • Intégration facilitée

Applications avec multiples canaux

  • REST + GraphQL + gRPC
  • Batch + API + Events
  • Migration progressive d’anciens systèmes

⚠️ Quand ne pas l’utiliser ?

CRUD simples

Pour une API basique sans logique métier complexe, c’est peut-être overkill. Un simple Controller → Service → Repository peut suffire.

Prototypes rapides

Pour un POC ou une démo, la structure complète peut ralentir le développement initial.

Équipes junior

La courbe d’apprentissage existe. Il faut former l’équipe aux concepts DDD et architecture hexagonale.

🚀 Mise en pratique

Étapes pour commencer

  1. Identifiez votre domaine métier : Quelles sont vos entités principales ?
  2. Définissez vos use cases : Que fait vraiment votre application ?
  3. Créez les ports : Quelles interfaces votre domaine a-t-il besoin ?
  4. Implémentez les adapters : REST, database, etc.
  5. Testez par couche : Domain → Use Cases → Adapters

Migration progressive

Pas besoin de refactoriser tout d’un coup :

  • Commencez par les nouvelles features
  • Isolez progressivement la logique métier existante
  • Refactorisez les parties les plus critiques

💡 Exemple concret

Pour voir tous ces concepts en action, consultez le repository de démonstration qui implémente une API Quarkus complète avec l’architecture hexagonale :

🔗 Repository GitHub - Quarkus Demo API

🔮 Et après ?

Domain Driven Design (DDD)

L’architecture hexagonale s’marie parfaitement avec DDD :

  • Aggregates, Value Objects, Domain Events
  • Bounded contexts pour les gros projets
  • Event Sourcing pour l’historisation

CQRS (Command Query Responsibility Segregation)

Séparer les commandes (write) des queries (read) :

  • Use cases de commande vs use cases de lecture
  • Optimisation séparée des performances
  • Évolutivité améliorée

🎉 En résumé

L’architecture hexagonale + Quarkus, c’est :

  • Code maintenable et évolutif
  • Tests rapides et fiables
  • Flexibilité technologique
  • Performance préservée

Oui, ça demande un peu plus d’effort initial. Mais sur la durée, vous gagnez énormément en productivité et en sérénité.

L’architecture hexagonale n’est pas qu’un pattern académique - c’est un outil pragmatique pour des APIs robustes et durables.

Vous testez déjà ? Partagez vos retours d’expérience !