Retour aux articles

Mettre en œuvre l'Architecture Hexagonale avec Spring Boot et une base de données PostgreSQL

Mettre en œuvre l'Architecture Hexagonale avec Spring Boot et une base de données PostgreSQL | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular

Dans l'écosystème du développement logiciel moderne, la complexité des applications ne cesse de croître, exigeant des architectures robustes, maintenables et évolutives. Parmi les approches les plus plébiscitées pour relever ces défis, l'Architecture Hexagonale, également connue sous le nom de "Ports and Adapters", s'est imposée comme un pilier du design logiciel. Cette méthodologie vise à isoler le cœur métier de l'application des détails techniques de l'infrastructure, garantissant une flexibilité et une testabilité accrues.

L'adoption de l'Architecture Hexagonale avec des technologies comme Spring Boot et une base de données PostgreSQL permet aux développeurs de construire des systèmes résilients. Cette combinaison est particulièrement pertinente pour les développeurs Full Stack expérimentés, tels que Laty Gueye Samba, basé à Dakar, qui travaille sur des applications métier complexes où la pérennité et l'adaptabilité sont cruciales.

Cet article explorera comment mettre en œuvre efficacement l'Architecture Hexagonale au sein d'un projet Spring Boot, en intégrant une base de données PostgreSQL, tout en adhérant aux principes de la Clean Architecture. L'objectif est de montrer comment préserver le domaine applicatif pur des influences externes, facilitant ainsi les changements technologiques et la maintenance à long terme.

Comprendre l'Architecture Hexagonale : Ports et Adaptateurs

L'Architecture Hexagonale, introduite par Alistair Cockburn, propose une organisation du code qui place le domaine métier au centre (le "noyau hexagonal"). Autour de ce noyau, des "ports" définissent les interfaces d'interaction, et des "adaptateurs" sont chargés d'implémenter ces interfaces pour interagir avec le monde extérieur. Cette séparation nette est la clé de son efficacité.

  • Ports : Ce sont des interfaces Java (ou leurs équivalents dans d'autres langages) qui se situent à la frontière du domaine. Ils définissent ce que le cœur de l'application offre (ports primaires ou "driving ports") et ce qu'il attend du monde extérieur (ports secondaires ou "driven ports"). Par exemple, un port pourrait être une interface UserRepositoryPort que le domaine utilise pour persister et récupérer des utilisateurs.
  • Adaptateurs : Ce sont des implémentations concrètes des ports. Ils connectent le cœur de l'application aux technologies spécifiques de l'infrastructure. Un adaptateur peut être une API REST exposant un port primaire, ou une implémentation de UserRepositoryPort utilisant Spring Data JPA et PostgreSQL comme adaptateur pour un port secondaire. Les adaptateurs sont les "chevilles ouvrières" qui permettent au domaine de communiquer sans connaître les détails d'implémentation.

Cette approche garantit que le code métier est indépendant des frameworks UI, des bases de données ou des services externes. Si un changement de base de données est requis, seul l'adaptateur de persistance doit être modifié, laissant le cœur de l'application intact.

Structurer un Projet Spring Boot Hexagonal

Pour un Développeur Full Stack Dakar Sénégal, la structure d'un projet Spring Boot basé sur l'Architecture Hexagonale peut être organisée en trois couches principales, correspondant aux concepts de "core", "application" et "infrastructure".

1. Le Couche Domaine (Core)

C'est le cœur de l'application. Elle contient toute la logique métier, les entités, les agrégats, les services de domaine et les ports (interfaces) que le domaine utilise ou expose. Cette couche ne doit avoir aucune dépendance envers Spring Boot ou d'autres frameworks d'infrastructure. Elle est constituée de classes Java pures.


// src/main/java/com/latysamba/hexagonal/domain/model/User.java
package com.latysamba.hexagonal.domain.model;

import java.util.UUID;

public class User {
    private UUID id;
    private String username;
    private String email;

    // Constructeur, getters, setters
    public User(UUID id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }

    public UUID getId() { return id; }
    public String getUsername() { return username; }
    public String getEmail() { return email; }

    public void updateEmail(String newEmail) {
        if (newEmail == null || newEmail.isBlank()) {
            throw new IllegalArgumentException("Email cannot be empty");
        }
        this.email = newEmail;
    }
}

// src/main/java/com/latysamba/hexagonal/domain/port/out/UserRepositoryPort.java
package com.latysamba.hexagonal.domain.port.out;

import com.latysamba.hexagonal.domain.model.User;
import java.util.Optional;
import java.util.UUID;

// Port de sortie (driven port) pour la persistance des utilisateurs
public interface UserRepositoryPort {
    User save(User user);
    Optional<User> findById(UUID id);
    Optional<User> findByUsername(String username);
}

2. La Couche Application

Cette couche gère les cas d'utilisation (use cases) et orchestre les interactions entre le domaine et les adaptateurs. Elle contient les services d'application (souvent annotés @Service dans Spring Boot) qui implémentent les ports primaires (driving ports) et appellent les ports secondaires (driven ports). Elle est le "chef d'orchestre" qui utilise le domaine pour accomplir des tâches spécifiques.


// src/main/java/com/latysamba/hexagonal/application/port/in/UserServicePort.java
package com.latysamba.hexagonal.application.port.in;

import com.latysamba.hexagonal.application.dto.UserDTO;
import java.util.UUID;

// Port d'entrée (driving port) pour les opérations utilisateur
public interface UserServicePort {
    UserDTO createUser(UserDTO userDTO);
    UserDTO getUserById(UUID id);
    UserDTO updateUserEmail(UUID id, String newEmail);
}

// src/main/java/com/latysamba/hexagonal/application/service/UserService.java
package com.latysamba.hexagonal.application.service;

import com.latysamba.hexagonal.application.port.in.UserServicePort;
import com.latysamba.hexagonal.application.dto.UserDTO;
import com.latysamba.hexagonal.domain.model.User;
import com.latysamba.hexagonal.domain.port.out.UserRepositoryPort;
import org.springframework.stereotype.Service;
import java.util.UUID;

@Service
public class UserService implements UserServicePort {

    private final UserRepositoryPort userRepositoryPort;

    public UserService(UserRepositoryPort userRepositoryPort) {
        this.userRepositoryPort = userRepositoryPort;
    }

    @Override
    public UserDTO createUser(UserDTO userDTO) {
        User newUser = new User(UUID.randomUUID(), userDTO.getUsername(), userDTO.getEmail());
        User savedUser = userRepositoryPort.save(newUser);
        return new UserDTO(savedUser.getId(), savedUser.getUsername(), savedUser.getEmail());
    }

    @Override
    public UserDTO getUserById(UUID id) {
        return userRepositoryPort.findById(id)
                .map(user -> new UserDTO(user.getId(), user.getUsername(), user.getEmail()))
                .orElseThrow(() -> new RuntimeException("User not found"));
    }

    @Override
    public UserDTO updateUserEmail(UUID id, String newEmail) {
        User existingUser = userRepositoryPort.findById(id)
                .orElseThrow(() -> new RuntimeException("User not found"));
        existingUser.updateEmail(newEmail);
        User updatedUser = userRepositoryPort.save(existingUser);
        return new UserDTO(updatedUser.getId(), updatedUser.getUsername(), updatedUser.getEmail());
    }
}

3. La Couche Infrastructure

Cette couche contient tous les adaptateurs. Elle implémente les ports secondaires (pour la persistance, les appels API externes, la messagerie) et fournit les adaptateurs pour les ports primaires (contrôleurs REST, implémentations de ligne de commande). C'est ici que Spring Boot, Spring Data JPA, les drivers PostgreSQL et d'autres technologies spécifiques sont utilisés.


// src/main/java/com/latysamba/hexagonal/infrastructure/adapter/out/persistence/data/UserData.java
package com.latysamba.hexagonal.infrastructure.adapter.out.persistence.data;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import java.util.UUID;

@Entity
@Table(name = "users")
public class UserData {
    @Id
    private UUID id;
    private String username;
    private String email;

    // Constructeur, getters, setters
    public UserData() {}
    public UserData(UUID id, String username, String email) {
        this.id = id;
        this.username = username;
        this.email = email;
    }
    public UUID getId() { return id; }
    public void setId(UUID id) { this.id = id; }
    public String getUsername() { return username; }
    public void setUsername(String username) { this.username = username; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
}

// src/main/java/com/latysamba/hexagonal/infrastructure/adapter/out/persistence/jpa/JpaUserRepository.java
package com.latysamba.hexagonal.infrastructure.adapter.out.persistence.jpa;

import com.latysamba.hexagonal.infrastructure.adapter.out.persistence.data.UserData;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
import java.util.UUID;

// JpaRepository comme interface de persistance Spring Data JPA
public interface JpaUserRepository extends JpaRepository<UserData, UUID> {
    Optional<UserData> findByUsername(String username);
}

// src/main/java/com/latysamba/hexagonal/infrastructure/adapter/out/persistence/UserRepositoryAdapter.java
package com.latysamba.hexagonal.infrastructure.adapter.out.persistence;

import com.latysamba.hexagonal.domain.model.User;
import com.latysamba.hexagonal.domain.port.out.UserRepositoryPort;
import com.latysamba.hexagonal.infrastructure.adapter.out.persistence.data.UserData;
import com.latysamba.hexagonal.infrastructure.adapter.out.persistence.jpa.JpaUserRepository;
import org.springframework.stereotype.Component;
import java.util.Optional;
import java.util.UUID;

// Adaptateur implémentant le port de sortie du domaine
@Component
public class UserRepositoryAdapter implements UserRepositoryPort {

    private final JpaUserRepository jpaUserRepository;

    public UserRepositoryAdapter(JpaUserRepository jpaUserRepository) {
        this.jpaUserRepository = jpaUserRepository;
    }

    @Override
    public User save(User user) {
        UserData userData = toUserData(user);
        UserData savedUserData = jpaUserRepository.save(userData);
        return toUser(savedUserData);
    }

    @Override
    public Optional<User> findById(UUID id) {
        return jpaUserRepository.findById(id).map(this::toUser);
    }

    @Override
    public Optional<User> findByUsername(String username) {
        return jpaUserRepository.findByUsername(username).map(this::toUser);
    }

    private User toUser(UserData userData) {
        return new User(userData.getId(), userData.getUsername(), userData.getEmail());
    }

    private UserData toUserData(User user) {
        return new UserData(user.getId(), user.getUsername(), user.getEmail());
    }
}

Intégration avec PostgreSQL et Spring Data JPA

L'intégration de PostgreSQL dans une architecture hexagonale avec Spring Boot est un exemple parfait d'adaptateur. Le cœur du domaine (la couche `domain`) n'a aucune connaissance de PostgreSQL ou même de la persistance relationnelle. Il ne connaît que l'interface UserRepositoryPort.

Dans la couche d'infrastructure, le UserRepositoryAdapter agit comme le pont. Il prend les requêtes du domaine, les traduit en opérations compréhensibles par Spring Data JPA (qui à son tour communique avec PostgreSQL), puis traduit les résultats de la base de données en objets de domaine. Ce découplage permet de changer la technologie de base de données (par exemple, vers MongoDB ou un autre SGBDR) sans impacter les couches domain et application, seulement l'adaptateur de persistance et ses dépendances.

Pour configurer Spring Boot avec PostgreSQL, il est nécessaire d'ajouter la dépendance au driver PostgreSQL et à Spring Data JPA dans le fichier pom.xml (ou build.gradle) et de configurer la connexion dans application.properties ou application.yml :


# application.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/mydatabase
spring.datasource.username=myuser
spring.datasource.password=mypassword
spring.datasource.driver-class-name=org.postgresql.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

Point de vue : développeur full stack à Dakar

Pour un développeur travaillant sur des systèmes comme des applications de gestion hospitalière, des plateformes de services numériques ou des systèmes ERP, la maîtrise de l'Architecture Hexagonale et de la Clean Architecture représente un avantage concurrentiel réel sur le marché technologique africain, en pleine expansion. Laty Gueye Samba, en tant qu'Expert Java Spring Boot Angular, a vu l'impact direct de ces principes sur la maintenabilité et l'évolutivité des projets dans des contextes exigeants.

Conclusion

L'Architecture Hexagonale est bien plus qu'une simple structure de projet ; c'est une philosophie de conception qui promeut le découplage, la testabilité et la flexibilité. En l'appliquant avec Spring Boot et PostgreSQL, les développeurs peuvent créer des applications robustes dont le cœur métier reste pur et indépendant des technologies d'infrastructure.

Cette approche est particulièrement bénéfique pour les développeurs Full Stack expérimentés comme Laty Gueye Samba, qui doivent constamment s'adapter aux nouvelles exigences et aux évolutions technologiques. Elle permet de construire des systèmes résilients, faciles à faire évoluer et à maintenir, un atout majeur pour tout projet de développement logiciel à Dakar et au-delà.

Pour approfondir le sujet, il est recommandé de consulter les ressources officielles :

À propos de l'auteur

Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.

Contact : latygueyesamba@gmail.com  |  Dakar, Sénégal