• architecture
  • ia

Architecture hexagonale : pourquoi elle rend le code écrit par l'IA plus fiable

Isoler le métier du framework, c'est donner à l'IA un périmètre clair et des tests rapides. Retour d'expérience chiffré sur Le Limier, un SaaS Laravel.

5 min de lecture

métierpur, testéWebAPITestsBaseEmailsIA

Quand une IA écrit du code, le risque n'est pas qu'elle se trompe de syntaxe : les tests et le compilateur l'attrapent tout de suite. Le vrai risque, c'est qu'elle modifie une règle métier sans s'en rendre compte, parce qu'elle a été branchée au mauvais endroit. L'architecture hexagonale trace une frontière nette entre ce qui fait la valeur d'une application et la plomberie technique. Pour un agent IA, cette frontière vaut toutes les consignes du monde.

L'architecture hexagonale en deux phrases

Au centre, le domaine : les règles de votre métier et les cas d'usage (« enregistrer une opposition RGPD », « créer un radar de prospection »). Il ne sait rien de la base de données, du framework ou des API externes.

Autour, les adaptateurs : Laravel, PostgreSQL, l'API du BODACC, le modèle d'IA. Ils se branchent sur le domaine par des ports, de simples interfaces que le domaine définit et que l'infrastructure implémente.

Un cas d'usage dépend d'un port ; deux adaptateurs l'implémententCas d'usageRegisterOptout\HandlerappellePort…RepositoryInterfaceAdaptateur EloquentPostgreSQL, en productionDouble en mémoireun tableau PHP, dans les testsimplémente
Le cas d'usage ne connaît que l'interface. En production, Laravel branche Eloquent ; dans les tests, un double en mémoire.

Ce que ça change quand c'est une IA qui code

Un périmètre que l'on peut nommer

« Ajoute un adaptateur pour la nouvelle API » et « modifie une règle de qualification » sont deux tâches très différentes, et l'architecture les rend visibles dans l'arborescence. Les agents qui travaillent sur Le Limier ont des consignes explicites, écrites dans leur fichier de rôle :

❌ JAMAIS de app() ou Service Location dans Domain
❌ JAMAIS d'Eloquent dans Domain Layer
❌ JAMAIS de Laravel helpers dans Domain
❌ JAMAIS de code sans tests (Domain/Application)

Quand une modification d'un agent touche app/__Domain__/ alors que la tâche concernait un adaptateur, je le vois dans le diff avant même de lire le code.

Des tests qui tournent sans base de données

Comme le cas d'usage ne dépend que d'une interface, on peut lui donner un faux, en mémoire, dans les tests. Pas de base à démarrer, pas de données à préparer : la suite du domaine tourne en quelques secondes. C'est ce qui permet de faire tourner les tests à chaque modification de l'agent, et pas seulement à la fin.

Changer de fournisseur sans toucher au métier

L'API Sirene, le BODACC, le modèle d'IA qui propose des angles de prospection : chacun est un adaptateur. Le jour où un fournisseur change ses conditions ou ses tarifs, on remplace l'adaptateur, et les règles métier, testées, ne bougent pas.

229

fichiers dans le domaine de Le Limier

36

cas d'usage, un dossier chacun

13

ports vers la persistance

7

contextes métier (RGPD, radars, open data…)

Un exemple réel : le registre des oppositions RGPD

Le Limier exploite des données publiques d'entreprises. Une entreprise peut s'opposer au traitement de ses données (article 21 du RGPD), et cette opposition doit survivre à chaque réimport des registres. Le domaine définit le port :

php
interface DataSubjectOptoutRepositoryInterface
{
    public function register(string $siren, OptoutScope $scope, ?string $reason = null, ?string $requestedVia = null): void;
 
    public function revoke(string $siren, OptoutScope $scope): void;
 
    public function isFullyOptedOut(string $siren): bool;
    // …
}

Le cas d'usage ne connaît que cette interface. Il valide le SIREN, la portée de l'opposition, puis enregistre :

php
class Handler
{
    public function __construct(
        private DataSubjectOptoutRepositoryInterface $optouts,
    ) {}
 
    public function handle(Input $input): Output
    {
        $siren = preg_replace('/\s+/', '', $input->siren) ?? '';
 
        if (! Siren::isValid($siren)) {
            return Output::failure("SIREN invalide : {$input->siren}");
        }
        // … portée de l'opposition, puis :
        $this->optouts->register($siren, $scope, $input->reason, $input->requestedVia);
 
        return Output::success($siren, $scope->value);
    }
}

En production, Laravel branche un adaptateur Eloquent. Dans les tests, un double en mémoire, avec ce commentaire qui dit tout : « le handler ne doit dépendre que du port ». Six tests couvrent les cas qui comptent : SIREN avec espaces, SIREN invalide, portée inconnue, opposition limitée aux dirigeants, et le fait qu'enregistrer deux fois la même opposition ne crée pas de doublon.

php
public function test_is_idempotent(): void
{
    $this->handler->handle(new Input(siren: '732829320', reason: 'premier motif'));
    $output = $this->handler->handle(new Input(siren: '732829320', reason: 'motif mis à jour'));
 
    $this->assertTrue($output->success);
    $this->assertCount(1, $this->optouts->all());
}

Ce que l'IA fait mal, même avec des règles

Je préfère être franc : des consignes dans un prompt restent des intentions. En relisant le code de Le Limier pour cet article, j'ai compté à la main 9 fichiers du domaine qui importaient Laravel ou l'infrastructure : un client d'API branché directement dans un cas d'usage, une façade DB pour vérifier la fraîcheur des données. Mon propre agent de planification l'écrivait d'ailleurs dans son fichier : « 5 couplages Domain→Infrastructure », score 83/100.

Le problème venait aussi du contexte de l'agent, qui vieillit. Ce même fichier décrit un domaine de 59 fichiers ; il en compte 229 aujourd'hui. Un agent qui travaille avec une carte périmée prend de mauvaises décisions, en toute confiance.

La frontière vérifiée par la machine

J'ai donc ajouté un test d'architecture avec PHPat, qui s'exécute dans l'analyse statique PHPStan déjà en place. Une seule règle, lisible par n'importe qui :

php
PHPat::rule()
    ->classes(Selector::inNamespace('App\__Domain__'))
    ->shouldNotDependOn()
    ->classes(
        Selector::inNamespace('App\__Infrastructure__'),
        Selector::inNamespace('App\__Application__'),
        Selector::inNamespace('Illuminate'),
        // … contrôleurs, jobs, commandes, providers
    )
    ->because("le Domain définit ses propres interfaces, implémentées dans l'Infrastructure.");

Premier passage : la règle a trouvé plus de violations que mon comptage à la main, dont des dépendances vers la couche Application que mon grep n'avait pas vues. La correction a surtout consisté à remettre chaque chose à sa place :

  • des objets métier qui vivaient dans l'infrastructure (un résumé d'import, un résultat de géocodage, les exceptions des API publiques) ont rejoint le domaine ;
  • une interface de service définie côté Application est devenue un port du domaine ;
  • les cas d'usage qui appelaient directement la base ou le cache passent désormais par des interfaces.

Résultat : zéro violation, et surtout, la prochaine dépendance interdite, qu'elle vienne de moi ou d'un agent, fait échouer l'analyse avant d'arriver dans le code.

Et si vous êtes client, pas développeur ?

Ce que vous y gagnez est concret :

  • Une application qui évolue sans tout casser. Les règles de votre métier sont testées à part, elles ne bougent pas quand on change d'outil.
  • Pas de dépendance à un fournisseur. Changer de prestataire d'emails, de paiement ou de modèle d'IA est un chantier circonscrit.
  • Un code qu'un autre développeur peut reprendre. La structure dit où se trouve quoi.

C'est la base de notre méthode, avec des specs écrites avant le code et des tests à chaque étage. Pour voir l'architecture appliquée, la page Laravel et le projet Le Limier en donnent un aperçu.

Portrait illustré d'Antoine Lemariey

Antoine Lemariey

Développeur et fondateur de Not a Bad Company. Je conçois et je code moi-même chaque projet, de la spécification à la mise en ligne : PostOwl, Le Limier, BookFlow, AntiScam Detector…

À lire ensuite

Un projet à construire avec cette rigueur ?

Décrivez-moi votre besoin, je vous dis comment je le construirais.

Parlons de votre projet