ENFR

FoodChéri & Seazon · NX, React, Node.js · Architecture

En finir avec le copier-coller entre deux marques

  • NX
  • React
  • Node.js
  • Express
  • CircleCI
  • Docker
  • GCP
  • ~75 %de déploiements en plus
  • 6 → 1dépôts consolidés

Le problème, en une phrase

FoodChéri et Seazon partageaient du vrai code (logique de paiement, utilitaires, composants) mais vivaient dans des projets séparés. Mettre à jour une fonction partagée voulait dire copier-coller le même changement dans chacun, à la main.

Contexte

On gérait trois produits (Seazon, FoodChéri et un back-office partagé), chacun avec son frontend React et son backend Express : six dépôts au total. La séparation était historique. Seazon et son back-office sont venus en premier, en React/Node. FoodChéri était à l’origine en PHP pur. Quand on a décidé d’unifier toute la stack sur React, Node et MongoDB, on a migré FoodChéri pour l’aligner, en prenant Seazon comme référence, et on a fusionné le back-office de FoodChéri dans celui de Seazon pour qu’un seul back-office serve les deux marques.

Résultat : beaucoup de code partagé entre six dépôts, sans réelle façon de le partager, à part le copier.

Quand le risque est devenu réel

Le problème a cessé d’être théorique le jour où un développeur a mis à jour notre intégration Stripe sur Seazon et a, logiquement, oublié que la même logique existait aussi sur FoodChéri et dans le back-office, où certaines actions déclenchent des paiements et des remboursements. Le changement a atterri à un endroit et pas aux autres. Sur FoodChéri, des paiements ont échoué, et les commandes concernées ce jour-là ont été annulées.

Quand du code de paiement partagé est en réalité trois copies, rester synchronisé dépend de la capacité de chaque développeur à se souvenir des trois, à chaque fois. Ce n’est pas une erreur qui risque d’arriver ; c’est une certitude. Et elle venait d’arriver, sur le chemin du paiement.

Le compromis que j’ai pesé

Un monorepo n’est pas gratuit. Il rend l’outillage, la CI/CD, la gestion des dépendances et les performances de build plus complexes que des dépôts séparés. Les changements peuvent aussi avoir un impact plus large (une modification peut toucher plusieurs applications), donc il faut de bons tests et une ownership claire pour éviter de casser quelque chose. Je savais dès le départ que cela demandait plus d’investissement initial en outillage de build, en automatisation et en organisation du dépôt.

J’ai accepté ce coût pour une raison : on partageait beaucoup de code. Une source de vérité unique pour les composants, utilitaires et types partagés supprimerait la duplication, garderait tout synchronisé et rendrait les changements cross-projets bien plus faciles à coordonner. On échangeait de la complexité opérationnelle contre de la cohérence et de la productivité sur le long terme.

L’alternative que j’ai écartée

Avant de choisir un monorepo, j’ai envisagé de publier le code partagé sous forme de packages npm privés. C’est une approche valable. Mais elle a son propre coût : un workflow de publication, du versioning, de la gestion de dépendances. Chaque changement partagé devient une release, et chaque consommateur doit monter de version pour l’obtenir. Coordonner un changement sur plusieurs applications devient plus lent, pas plus rapide.

Ce coût ne se justifie que lorsque les applications appartiennent à des équipes différentes, avec des cadences de release différentes, et qu’on construit de vraies librairies réutilisables. Ce n’était pas notre cas. Nos applications étaient développées par la même équipe, évoluaient ensemble et partageaient une grande quantité de code. On ne publiait pas des librairies pour des consommateurs externes ; on construisait un seul écosystème. Un monorepo nous a permis de modifier le code partagé et tous ses consommateurs dans un seul commit, de tout valider via une seule pipeline CI, et d’éviter la surcharge permanente du publier-puis-mettre-à-jour.

Si ces applications avaient appartenu à des équipes indépendantes avec des cadences de release différentes, j’aurais choisi les packages privés. Comme les nôtres étaient fortement couplées et changeaient ensemble, le monorepo était le modèle opérationnel le plus simple, même s’il était le plus complexe à mettre en place.

Comment la migration s’est déroulée

Six dépôts séparés avec la logique de paiement dupliquée dans trois d’entre eux, consolidés en un monorepo avec un seul package partagé

La structure était simple : les six applications vivent sous apps/, et le code partagé sous packages/, séparé en shared-client et shared-server. La logique de paiement, les utilitaires, les composants et les types qui étaient copiés-collés existent désormais une seule fois, dans packages/, et chaque app les consomme de là.

J’ai séquencé la migration délibérément plutôt que de tout déplacer d’un coup : Seazon d’abord (front et back), puis le back-office, puis FoodChéri. L’ordre était une décision de risque. Seazon était l’implémentation de référence : FoodChéri tournait sur la même stack et en grande partie le même code, donc faire fonctionner Seazon dans le monorepo prouvait de fait le pattern pour FoodChéri avant même que j’y touche. Chaque étape rendait la suivante plus sûre.

En parallèle de la migration, j’ai introduit une CI/CD automatisée via CircleCI (Docker → CircleCI → GCP), remplaçant les processus de déploiement manuels et séparés que chaque projet avait avant. Le graphe des affected de NX faisait que la CI ne rebuild et ne redéploie que ce qui a réellement changé, donc consolider dans un seul dépôt ne nous a pas coûté de temps de build.

Résultats

  • La duplication a disparu. Logique de paiement, utilitaires, composants et types partagés vivent désormais une seule fois, dans packages/, consommés par chaque app. L’échec précis qui avait annulé une journée de commandes FoodChéri (un changement Stripe appliqué à une copie et pas aux autres) ne peut plus se produire ainsi, car il n’y a plus trois copies à synchroniser.
  • La fréquence de déploiement a augmenté d’environ 75 %, une fois la CI/CD automatisée en place à la place des déploiements manuels séparés.
  • L’onboarding est devenu nettement plus rapide : un nouveau développeur configure et comprend un seul dépôt au lieu de six.
  • Cohérence entre marques par défaut. Un changement sur un composant partagé atteint désormais FoodChéri et Seazon dans le même commit, au lieu d’être copié entre les deux et de diverger lentement.
  • La CI est restée rapide. Le graphe des affected de NX ne rebuild et ne redéploie que ce qui a changé, donc la consolidation n’a pas coûté de temps de build.

Ce que je ferais différemment

J’ai mené la migration en un seul basculement. Ça a marché, mais c’était plus difficile que nécessaire. Les apps en production ne pouvaient pas s’arrêter : l’équipe continuait de livrer des fonctionnalités et des correctifs sur les projets séparés, parce que ces apps servaient les clients et prenaient les commandes. Le monorepo était donc une cible mouvante : chaque changement sur les anciens projets périmait ma migration.

Alors, une fois la structure validée, j’ai planifié le basculement pour un week-end : la seule fenêtre sans commandes en production et sans développeurs au travail. J’ai copié la dernière version de chaque projet dans le monorepo, poussé en preprod, et testé tout le week-end. Le basculement est passé en production le samedi soir. Le lundi a fait apparaître quelques bugs, mais rien de grave et tous corrigeables en quelques minutes.

Ça a réussi, mais je séquencerais les choses différemment aujourd’hui. Plutôt que de migrer les trois projets et de basculer d’un coup, je mènerais chacun jusqu’en production avant de commencer le suivant : Seazon, puis le back-office, puis FoodChéri. Même ordre, mais un seul basculement en production à la fois au lieu d’un seul gros pari. Cela réduirait le rayon d’impact de chaque étape à une seule app et supprimerait le week-end du tout-ou-rien où tout bouge en même temps. La migration a marché ; c’est le profil de risque que j’améliorerais.