Depuis quelques années, sur mon temps libre, je travaille sur NuCorder : une solution permettant aux musiciens de répéter en ligne avec une contrainte de latence faible. Fondé par trois musiciens, j’ai été en charge depuis le début du développement technique et de l’infrastructure.

La stack a naturellement évolué au fil du temps : d’un monolithe Java / Spring + Struts vers Spring Boot, Docker, Kubernetes managé, et l’introduction progressive de micro-services. Comme beaucoup de développeurs, j’ai longtemps cherché à appliquer les patterns que je lisais ou que j’avais vécus dans des contextes à fort trafic. Il m’a fallu du temps pour accepter que la meilleure architecture est celle qui répond aux besoins réels de son projet — pas celle qui impressionne sur le papier.

C’est dans ce contexte que l’accumulation de problèmes a fini par rendre une migration inévitable.

Illustration de la migration de Java vers Go
Illustration générée par IA (ChatGPT / DALL·E).

De petits problèmes au stress quotidien

Sur un projet géré en dehors des heures de travail, chaque friction compte double.

Le build CI était devenu l’une des plus pénalisantes : jusqu’à 30 minutes par pipeline, tests compris. Sur une soirée ou un week-end, c’est du temps de cerveau disponible grignoté entre deux tâches (ou du temps de repos en moins). J’avais fini par contourner le problème en ignorant les tests sur la branche principale — ce qui revenait à sacrifier la résilience pour avancer, et à prendre progressivement de mauvaises habitudes. La dépendance aux crédits gratuits de Gitlab CI ajoutait une couche supplémentaire : en fin de mois, il fallait compter les minutes restantes et basculer sur un runner local moins performant.

Le boot time de Spring Boot sur de petites nodes Kubernetes (un choix délibéré pour contenir les coûts) posait un autre problème. Deux instances pouvaient tenter de démarrer en parallèle, saturer le CPU lors de l’initialisation et finir par se faire tuer par le readiness timeout. Les maintenances Scaleway sur une node provoquaient les mêmes respawns intempestifs. L’impact sur les utilisateurs restait limité, mais pour moi c’était une source d’alerting récurrent qui sonnait au mauvais moment.

À ça s’ajoutait la fragmentation croissante de la codebase : j’avais commencé à expérimenter des micro-services en Go en parallèle du backend Java, avec le résultat prévisible d’une architecture plus difficile à maintenir seul — précisément l’inverse de ce que je cherchais.

Le déclic

La décision de migrer avait mûri pendant plusieurs mois. J’avais le plan en tête, mais pas encore la certitude que j’aurais le courage de m’y atteler seul, sur du temps libre, avec une codebase que je connaissais sur le bout des doigts mais qui représentait quand même des années de développement.

C’est en travaillant sur la refonte du frontend que quelque chose a changé. J’utilisais l’IA générative pour accélérer certaines parties du travail, et j’ai réalisé assez vite que l’approche pouvait s’appliquer au backend. Pas comme un outil qui écrit du code à ma place ; mais comme un collaborateur capable de porter une partie de la charge mécanique de la migration, pendant que je gardais la main sur les décisions et la validation.

Ce que l’IA a (vraiment) fait

La première étape a été de ne pas foncer tête baissée. Avant d’écrire la moindre ligne de Go, j’ai utilisé Claude pour auditer le backend Java existant et produire un plan de migration structuré — domaine par domaine, étape par étape. C’est une leçon que j’ai apprise rapidement : confier une tâche trop large à un modèle de langage produit des résultats incohérents. Mieux vaut un plan détaillé avec des étapes courtes, vérifiées une par une.

Avant de commencer la migration elle-même, j’ai posé une première documentation de l’architecture en Markdown — un format lisible aussi bien par les humains que par les modèles. Mais ce qui a vraiment fait la différence, c’est la façon dont cette documentation a évolué : de façon incrémentale, au fil des tâches. Chaque fois qu’on résolvait un problème ou qu’on abordait un nouveau domaine, je créais ou enrichissais le fichier de documentation correspondant et je le référençais dans l’index. La documentation est ainsi devenue un artefact vivant du travail, pas un prérequis figé.

Voici à quoi ressemble la structure finale :

├── brevo-contact-updates.md
├── crons.md
├── database-objects.md
├── domain-layouts.md
├── handlers.md
├── i18n.md
├── k8s-deployment.md
├── pitfalls.md
├── redis-cache.md
├── sql-migrations.md
├── storage.md
├── swagger.md
└── testing.md

Le fichier CLAUDE.md à la racine sert de guide d’orientation rapide pour le modèle — point d’entrée du serveur, règles de workflow, et un tableau indiquant quel fichier lire selon la tâche en cours :

markdown
# CLAUDE.md - nuapp Go Internals Guide

## Quick orientation

- Go server entrypoint: `cmd/server/main.go`
- Core API code: `internal/domain/<domain>/`
- All API routes are served by Go.
- Request locale is resolved centrally by `internal/middleware/locale.go` + `internal/i18n/`.

## Core workflow rules

1. Read only the relevant handler/schema context before coding — don't explore broadly first.
2. Use `rtk` for commands.
3. After each handler/store change, run `go test ./...` (or `rtk go test ./...`).
4. Keep changes small and shippable.
5. Keep stores domain-owned (no cross-domain SQL in consumer stores).

## Reference docs

Before working on a task, read the doc files relevant to what you're about to touch.

| Doc | Read when… |
|-----|------------|
| `docs/domain-layouts.md` | adding or modifying a domain package |
| `docs/handlers.md` | writing or modifying handlers, routes, or services |
| `docs/storage.md` | adding or modifying stores, SQL queries, or persistence-facing interfaces |
| `docs/i18n.md` | adding or modifying translated messages |
| `docs/testing.md` | writing or modifying tests |
| `docs/pitfalls.md` | working with DB columns or error handling |
| `docs/database-objects.md` | working with DB schema or unfamiliar tables |
| `docs/sql-migrations.md` | creating or modifying SQL migrations |
| `docs/redis-cache.md` | using Redis or the coordinator pattern |
| `docs/crons.md` | adding or modifying cron jobs |
| `docs/brevo-contact-updates.md` | pushing Brevo contact attributes from a domain |
| `docs/swagger.md` | updating API documentation |
| `docs/k8s-deployment.md` | deploying or modifying Kubernetes config |

En pratique, ça évite que le modèle parte explorer la codebase dans tous les sens : il sait exactement où regarder avant de commencer.

À noter : Codex utilise un fichier AGENTS.md pour donner une configuration équivalente à l’agent. Dans mon cas, il se contente de sourcer le CLAUDE.md, ce qui évite de maintenir deux documentations distinctes.

La migration elle-même s’est faite en alternant Claude Code et Codex, selon le plafond de session atteint — une contrainte pratique qui s’est finalement révélée utile, puisqu’elle forçait à travailler par blocs cohérents. La codebase Java servait de référence explicite à chaque étape. Un document de tracking permettait de savoir exactement où reprendre le travail à chaque nouvelle session. Pour la revue, j’utilisais systématiquement l’outil qui n’avait pas produit le code pour relire ce que l’autre avait écrit — une forme de revue croisée entre modèles, efficace pour attraper les incohérences les plus grossières.

Ce qui a le moins bien fonctionné : vouloir aller trop vite et donner trop à faire en une seule fois. Les sessions où j’avais défini des périmètres trop larges produisaient un code fonctionnel en apparence, mais stylistiquement incohérent d’un domaine à l’autre — ce qui a nécessité une deuxième passe complète de standardisation.

Une semaine de migration, une semaine de nettoyage

La migration du backend a pris environ une semaine de travail intensif. Mais terminer la migration ne voulait pas dire avoir un backend propre — loin de là. J’avais obtenu un code fonctionnel, mais produit par des sessions différentes, avec des modèles différents, sur des jours différents. Le résultat était prévisible : des incohérences de style, des approches légèrement différentes d’un domaine à l’autre, quelques raccourcis pris en cours de route.

La deuxième semaine a donc été consacrée à une revue soigneuse, en traitant les défauts par thèmes plutôt que domaine par domaine : sécurité, conformité comportementale par rapport au backend Java de référence, standardisation du style, et découpage modulaire. J’ai construit mes tests en parallèle pour éviter les régressions, en m’appuyant là aussi sur l’IA pour accélérer leur écriture.

Quelques régressions ont tout de même émergé — des subtilités qu’on ne voit que sur un vrai environnement, pas en test. Le traitement des valeurs nulles dans certains payloads en est un bon exemple : un comportement identique en apparence, mais légèrement différent dans les cas limites. Je les ai corrigées sur le même modèle que la migration elle-même — plan, exécution, revue.

Ce que cette phase m’a confirmé : même avec une bonne documentation et un workflow structuré, une phase de standardisation dédiée est inévitable. Mieux vaut la prévoir dès le départ que de la découvrir en fin de parcours.

Ce que ça a changé concrètement

Le résultat le plus immédiat — et celui qui m’a le plus soulagé au quotidien — est le temps de build CI. Une pipeline qui prenait entre 15 et 30 minutes tourne maintenant en 5 minutes environ, tests compris. Sur un projet géré sur le temps libre, c’est une différence qui change la façon dont on travaille : on relance un build sans hésiter, on ne triche plus avec les tests, on reprend de bonnes habitudes.

Le boot time est devenu quasi immédiat. Plus de timeout sur les readiness probes, plus d’alerting intempestif lors des maintenances Scaleway, plus de deux instances qui se marchent dessus au démarrage. Le cluster est plus stable, et je dors mieux.

Les coûts d’infrastructure ont évolué favorablement — Go consomme sensiblement moins de CPU et de RAM que Spring Boot pour une charge équivalente. En pratique, une partie de ce gain a été réallouée vers de meilleures nodes Kubernetes plutôt que traduite en économie nette, mais c’est un arbitrage que je n’aurais pas pu me permettre avant.

Enfin, la codebase est à nouveau unifiée. Les micro-services Go expérimentaux ont été absorbés dans le monolithe modulaire, et je maintiens aujourd’hui un seul backend en production — plus simple à faire évoluer, plus simple à déboguer, plus simple à opérer seul.

Le backend Go est en production depuis plusieurs semaines. Il reste des petites choses à peaufiner, comme sur tout projet, mais la migration est un succès.

Ce que j’en retiens

L’IA générative m’a permis de réaliser en quelques semaines une migration que j’aurais repoussée indéfiniment, faute de temps et d’énergie. Mais elle n’a pas fait le travail à ma place — et c’est important de le dire clairement.

Ce qui a rendu cette migration possible, c’est d’abord une bonne connaissance du code source. Je connaissais ce backend dans ses moindres détails : ses bizarreries, ses cas limites, ses comportements attendus. Sans ça, je n’aurais pas pu valider ce que l’IA produisait, ni détecter les régressions subtiles qui n’apparaissent qu’en production. L’IA amplifie ce qu’on sait déjà — elle ne comble pas ce qu’on ne maîtrise pas.

Quelques principes que je garderais pour une prochaine migration du même type :

  • Documenter avant de migrer, pas pendant. Une documentation en Markdown, construite incrémentalement et référencée dans un fichier d’index, change radicalement la cohérence des sessions.
  • Travailler en petites étapes, vérifier après chacune. Les sessions trop larges produisent du code incohérent. La granularité est une discipline, pas une contrainte.
  • Prévoir une phase de standardisation dédiée. Le code produit par plusieurs sessions successives sera inévitablement hétérogène — c’est normal, il faut juste l’anticiper.
  • Un document de tracking est indispensable. Il permet de reprendre le travail sans perte de contexte entre les sessions, et de garder une vision d’ensemble pendant la migration.

Ce que l’IA m’a apporté, au fond, c’est du temps — le temps de faire quelque chose que j’aurais autrement repoussé encore longtemps. Le reste, c’était de l’artisanat.

Vous travaillez sur un sujet similaire ?

Si vous envisagez une migration comparable, que votre stack technique commence à peser sur votre capacité à livrer, ou simplement si ce retour d’expérience vous a donné des idées, n’hésitez pas à me contacter !

Parlons-en