Page chargée : Article

Blog

Monorepo ou multi-repo avec des agents IA : pourquoi j’utilise un repo parent et des submodules

Un seul workspace pour l’agent IA, des repos qui restent indépendants : mon organisation avec un repo parent et des submodules Git, les features qui touchent plusieurs repos et les pièges à éviter.

Récemment, un post LinkedIn prenait clairement parti pour le monorepo. L’argument : avec un agent IA, tout doit être au même endroit, sinon il ne voit qu’une partie du système et il code à moitié à l’aveugle.

J’ai répondu en commentaire que je fonctionnais autrement, avec un repo parent et des submodules Git. Je ne m’attendais pas à grand-chose. Résultat : des questions, des objections, et des gens qui testaient déjà la même approche. Un commentaire LinkedIn, c’est un peu court pour répondre à tout ça, alors voici la version longue.

Le monorepo a un vrai argument

L’argument du post tient la route.

Un agent comme Claude Code ou Codex travaille avec ce qu’il voit dans son workspace. Si tu l’ouvres dans le repo du backend, il ne sait pas comment le front consomme l’endpoint qu’il est en train de modifier. Il renomme un champ dans la réponse, les tests du backend passent, et l’écran casse en silence de l’autre côté.

Dans un monorepo, ce problème disparaît. L’agent voit le DTO C#, le type TypeScript qui le reflète et le composant qui l’affiche. Il peut tout modifier d’un coup, de façon cohérente.

Donc oui, l’agent a besoin du contexte complet. Là où je décroche, c’est sur la conclusion : pour lui donner ce contexte, pas besoin de fusionner tous les repos en un seul.

Ce que je ne veux pas perdre

Mes projets ont souvent plusieurs applications : un backend .NET, un front React, parfois une app mobile. Elles vivent ensemble, mais elles n’ont pas la même vie.

  • Un rythme de release différent. L’API peut partir en prod trois fois dans la journée. L’app mobile passe par la validation des stores, et une partie des utilisateurs garde l’ancienne version pendant des semaines.
  • Une CI différente. Un build .NET avec ses tests d’intégration n’a rien à voir avec un build Vite ou un build mobile. Dans des repos séparés, chaque pipeline reste simple et ne se déclenche que quand il le faut.
  • Un historique lisible. Un git log sur le backend ne parle que du backend. Une PR ne mélange pas une migration EF Core et un changement de CSS.
  • Des accès différents. Un freelance front n’a pas forcément besoin du code du backend, et inversement.

Un monorepo sait gérer tout ça, avec Nx ou Turborepo, des pipelines filtrés par dossier et des CODEOWNERS. Mais c’est de l’outillage à monter et à maintenir pour retrouver ce que des repos séparés donnent d’office.

Mon organisation : un repo parent, des submodules

L’idée est simple. Chaque application garde son propre repo. Au-dessus, un repo parent les référence comme submodules Git.

Le site que tu es en train de lire est organisé comme ça :

cv/                            # repo parent
├── .gitmodules
├── docker-compose.yml
├── docker-compose.override.dev.yml
├── Makefile
└── src/
    ├── backend/               # submodule cv.backend (.NET)
    └── client/                # submodule cv.client (React)

Le fichier .gitmodules dit simplement où trouver chaque repo :

[submodule "src/client"]
    path = src/client
    url = [email protected]:Walfhand/cv.client.git
[submodule "src/backend"]
    path = src/backend
    url = [email protected]:Walfhand/cv.backend.git

Le repo parent ne contient presque pas de code. Il porte ce qui concerne le système entier :

  • le docker-compose.yml qui lance l’API, le front et la base PostgreSQL ensemble ;
  • un Makefile avec les commandes du quotidien (make up-all, make up-backend, make up-db) ;
  • la doc qui explique comment les applications s’articulent.

C’est aussi le bon endroit pour le fichier d’instructions de l’agent (CLAUDE.md, AGENTS.md) : quel front consomme quelle API, comment lancer la stack, quelles conventions valent partout. Chaque repo garde ses propres instructions pour ses conventions à lui.

J’en ai fait un template que je réutilise à chaque nouveau projet. Un clone, et tout est là :

git clone --recurse-submodules [email protected]:mon-org/mon-projet.git

Ensuite, j’ouvre l’agent à la racine du parent. Il voit le backend, le front, le compose et la doc. Le même contexte que dans un monorepo.

Une feature qui touche deux repos

C’est la première question qu’on m’a posée : « Quand l’agent doit modifier deux ou trois submodules, tu te retrouves avec des commits et des PR séparés à coordonner ? »

Oui. C’est le vrai compromis de cette approche.

Prenons un cas courant : afficher la date de livraison prévue d’une commande. Il faut l’exposer dans l’API et l’afficher dans le front. L’agent travaille dans un seul workspace et fait les deux modifications dans la même session. Il garde la cohérence entre le DTO et le type TypeScript, parce qu’il a les deux sous les yeux.

Ce qui change, c’est la fin. Chaque repo reçoit sa branche, son commit et sa PR :

cd src/backend
git switch -c feature/expected-delivery-date
git add -A
git commit -m "Expose expected delivery date on orders"
git push -u origin HEAD
# Même chose dans src/client

Quand les PR sont mergées, le parent fait avancer ses pointeurs :

git submodule update --remote
git commit -am "Update submodules"

Deux PR au lieu d’une, donc. En pratique, ça me gêne beaucoup moins qu’on pourrait le croire, pour une raison simple : le commit atomique du monorepo ne survit pas au déploiement. Ton backend et ton front ne partent jamais en prod à la même milliseconde. Et une app mobile peut tourner en ancienne version pendant des semaines. Le contrat entre les applications doit rester compatible pendant la transition, monorepo ou pas.

Je fais donc ce que j’aurais fait de toute façon. Le backend ajoute le champ sans rien casser et part en prod. Le front l’utilise ensuite. Avec des repos séparés, cette contrainte se voit. Dans un monorepo, elle existe tout autant, elle est juste masquée par un seul commit.

Je découpe par cycle de vie, pas par couche technique

Autre question qui revient : « Tes repos sont séparés par préoccupation technique ? Pour chaque feature, tu dois modifier plusieurs repos ? »

Pas vraiment. Je ne découpe pas par couche, avec un repo pour le domaine, un pour l’infra et un pour les contrats. Là, ce serait l’enfer : chaque petite modif traverserait trois repos.

Je découpe par responsabilité et par cycle de vie. Un repo, c’est une application qui se build, se teste et se déploie seule : le backend, le front, le mobile. Le backend reste un seul repo, même quand c’est un modular monolith avec plusieurs modules.

Résultat : beaucoup de features ne touchent qu’un seul repo. Une règle métier, un nouvel écran sur une API existante, un fix de perf. Quand une feature traverse le backend et le front, ça fait deux PR, et l’agent les prépare ensemble.

Un submodule n’est pas une bibliothèque partagée

On m’a aussi fait remarquer que les submodules deviennent pénibles au moment de refactorer, et que du code partagé par plusieurs applications a sa place dans un package NuGet privé, pas dans un submodule.

Je suis entièrement d’accord. On ne parle simplement pas du même usage.

Si du code est utilisé par plusieurs applications, c’est une bibliothèque. Elle a son repo, son versioning, son package NuGet ou npm, et chaque application choisit quand elle monte de version. Mettre ce code dans un submodule que trois projets compilent directement, c’est se créer un couplage invisible. Et là oui, chaque refacto devient pénible.

Chez moi, aucun submodule n’en compile un autre. Le backend ne référence pas le front, le front ne référence pas le backend. Le parent ne build rien non plus : il orchestre. Un refacto à l’intérieur d’une application reste dans son repo. Et un changement qui traverse deux applications, c’est un changement de contrat d’API, qui mérite de toute façon d’être fait proprement.

Les pièges des submodules

Les submodules ont mauvaise réputation, et pas complètement à tort. Voici ceux qui m’ont piqué au début.

Le git pull qui oublie les submodules. Tu pulls le parent, les pointeurs bougent, mais le code dans src/backend reste à l’ancienne version. Et tu passes une heure à chercher pourquoi ton changement n’apparaît pas.

Le pointeur vers un commit introuvable. Tu commits dans un submodule, tu mets à jour le parent, tu pushes le parent... mais pas le submodule. Ton collègue clone, et Git lui répond qu’il ne trouve pas le commit référencé.

Le detached HEAD. Après un git submodule update, le submodule n’est sur aucune branche. Si tu commits directement, ton travail n’est rattaché à rien, et le prochain update le fait disparaître de ta vue.

Trois lignes de config règlent l’essentiel :

# pull, checkout et switch mettent aussi à jour les submodules
git config --global submodule.recurse true

# refuse de pusher le parent si un commit référencé n'est pas pushé dans son submodule
git config --global push.recurseSubmodules check

# git status résume les commits en attente dans chaque submodule
git config --global status.submoduleSummary true

Pour le detached HEAD, le réflexe est simple : je crée toujours une branche dans le submodule avant de toucher au code. Je l’écris aussi dans les instructions de l’agent, pour qu’il ait le même réflexe.

Dernier détail, plus cosmétique : l’historique du parent. Sur le repo parent de ce site, 29 commits sur 32 sont une variante de « update submodules ». Ce n’est pas très beau, mais c’est exactement son rôle : dire quelles versions des applications vont ensemble. L’historique intéressant vit dans chaque repo.

Quand je choisirais un monorepo

Le monorepo n’est pas une mauvaise idée. Je le choisirais sans hésiter dans plusieurs cas :

  • une petite équipe qui livre tout ensemble, avec un seul déploiement ;
  • beaucoup de code réellement partagé, par exemple des types TypeScript communs à un front web et une app React Native ;
  • une équipe déjà à l’aise avec Nx ou Turborepo.

Si tes applications vivent au même rythme, les séparer n’apporte pas grand-chose. Mon organisation a du sens quand elles ont des vies différentes.

Le mot de la fin

Le post LinkedIn avait raison sur un point : un agent IA a besoin de voir tout le système pour faire du bon travail. Ce besoin, c’est le workspace qui y répond. L’organisation de tes repos, elle, dépend du rythme de vie de tes applications.

Le repo parent donne à l’agent sa vue d’ensemble. Les submodules gardent chaque application indépendante, avec son historique, sa CI et son rythme. Les deux cohabitent très bien.

Si tu as testé une autre organisation avec tes agents, ou si tu hésites pour ton projet, écris-moi. Je suis curieux de voir comment d’autres s’y prennent.