Image mise en avant - Travailleurs tirant une charette aux roues carrés - Crédit : https://performancemanagementcompanyblog.com/tag/square-wheels-too-busy-to-improve/
Expertise | Refonte

Comment développer de nouvelles fonctionnalités quand la dette technique s’accumule ?

Publié le : 9 janvier 2023
Temps de lecture : 10 minutes

«Nous n’arrivons plus à produire de nouvelles fonctionnalités sans régression»

«Ajouter ce bouton prendra une semaine»

«Chaque modification du code implique des bugs»

Quand on édite un logiciel depuis des années, il n’est pas rare d’entendre ce genre de discours. C’est d’ailleurs un problème récurrent dans les sociétés qui grossissent vite. Plus la masse de code est importante, plus elle est difficile à maintenir.

Arrivé à un certain stade, il devient compliqué, voire impossible de faire évoluer le produit et la moindre demande d’évolution peut prendre des semaines avant d’être implémentée. L’application prend du retard face aux nouvelles fonctionnalités proposées par la concurrence ; les idées novatrices doivent être laissées de côté ; la direction voit ses projets courts et moyens termes refusés par les équipes.

Les développeurs quant à eux sont sous tension, doivent subir la responsabilité des bugs qui apparaissent et parfois même, préfèrent quitter le navire.

Ce fléau a un nom : la dette technique.

Dans cette situation, comment rectifier le tir ? Comment se sortir de l’enlisement que cause la dette accumulée sur une application ?

On fait le point dans cet article avec Tommy Alexandre, Lead Developer chez theTribe.


Sommaire :


Quelles sont les causes de la dette technique ? 🔎

Si vous êtes dans cette situation, rassurez-vous : elle n’a rien d’extraordinaire, et de nombreuses sociétés y font face.

La première cause qui rend impossible l’évolution d’une application est appelée entropie logicielle

L’entropie logicielle est un principe qui veut que plus une application évolue, plus la dette technique augmente

Dans un monde idéal, la dette est remboursée à mesure que celle-ci s’accumule. Car plus la dette est résorbée au fur et à mesure, moins la qualité de l’application se dégradera au cours du temps. Mais dans les faits, la dette s’accumule plus vite que notre capacité à la rembourser.

Une fois que la dette a atteint un certain niveau et qu’il devient compliqué pour les développeurs de faire leur travail, les choses empirent, car plus la dette s’accumule, plus on crée de la nouvelle dette.

En effet, le produit ne peut pas s’arrêter d’évoluer. Les développeurs doivent sortir de nouvelles fonctionnalités, même si les conditions pour travailler correctement ne sont pas réunies. .. Ils doivent donc faire les choses à moitié, voire d’une manière peu optimisée pour continuer à faire évoluer le produit.  Les développeurs font donc ce qu’ils peuvent, et ajoutent à leur tour de la dette. 

C’est le phénomène connu sous le nom de broken windows. Car un logiciel, c’est un peu comme un immeuble : si une vitre est cassée, il y a de fortes chances que ses murs soient également dégradés par la suite, car il est déjà jugé comme insalubre.

Selon la fréquence à laquelle la dette est résorbée, le moment fatidique où l’application deviendra difficile à maintenir peut arriver plus ou moins tôt.

La dette technique s’accumule encore plus vite dans les environnements de type startup, dans lesquels il n’est pas rare de voir un POC (Proof of Concept), destiné à la base à servir de démo, se retrouver en production faute de temps et de moyens.

Dans ce cas, l’application continuera à évoluer sur une base de code legacy – un terme utilisé en informatique pour qualifier du code ancien, hérité du passé, mais que l’on conserve car on ne peut pas faire autrement. Dans ce genre de situation, on entend des choses comme  «Pas besoin que ça soit propre, on refera les choses proprement plus tard». Sauf que ce  «plus tard» n’arrive jamais.

Mais pourquoi ce problème est-il rarement traité à temps ? 

Parce que souvent, la dette technique est  la dernière des priorités de la direction. Elle ne comprend pas vraiment ce que ce terme signifie, ni les implications que cela peut avoir sur l’application et donc sur la société elle-même quelques années plus tard.

La refonte totale : une solution pour faire face à la dette technique ? 🥊

Quand il devient difficile de produire des fonctionnalités et que les développeurs n’arrivent plus à travailler dans de bonnes conditions, la question d’une refonte totale de l’application se pose.

Mais un chantier comme celui-ci est difficile à mettre en œuvre, pour plusieurs raisons.

Tout d’abord, l’ampleur du travail est colossale. Souvent, la question de la refonte se pose plusieurs années après que la première brique du logiciel ait été posée, car c’est à ce moment-là que la dette technique atteint un stade où on ne peut plus la résorber. La refonte implique alors de développer à nouveau ce qui a déjà été développé pendant plusieurs années. Une refonte de ce type, si on la lance de façon globale, se chiffre alors en mois, voire en années de travail et les coûts sont très élevés, pour au final ne produire aucune valeur ajoutée.

Admettons que la direction choisisse tout de même cette option (ce qui est peu concevable). L’histoire se répètera, on sera obligé de développer encore plus vite que la première fois, et la dette s’accumulera de la même manière qu’elle s’est accumulée lors de la première itération. 

Sans compter que les règles métiers et les users stories (la connaissance métier) utilisées pour développer l’application auront sûrement été perdues entre deux, ce qui ne facilite pas le projet de refonte.

Pour ces raisons, une refonte complète du SI ou de votre logiciel n’est pas forcément la bonne solution ; il vaut mieux y aller brique par brique, comme on l’explique dans cet article : Refonte Technique : quand et comment y aller ?

Une refonte globale peut éventuellement être envisageable quand il y a un besoin de changement de technologie (passer d’un client lourd à une application web par exemple). Mais, même dans ce cas, il est souhaitable de délimiter un petit périmètre à porter sur la nouvelle application plutôt que de tout reprendre de 0.

Comment faire évoluer l’application sans alimenter la dette technique ? ⚙️

Alors que faire, si la refonte totale n’est pas une solution ? Comment rembourser la dette technique tout en continuant à maintenir et à faire évoluer  la solution ? Pour cela, il y a plusieurs stratégies à mettre en place.

👉 Ne plus ajouter de code à la base de code legacy

La priorité absolue, c’est que l’application historique cesse d’évoluer. La dette technique doit arrêter de s’accumuler dans le système, et le seul moyen d’y arriver est de figer les développements sur l’ancien code. Aucune nouvelle fonctionnalité ne doit être ajoutée à la base de code legacy.

En effet, implémenter  des fonctionnalités sur une base de code non maintenable aurait de nombreux impacts négatifs :

  • Le code produit aurait de fortes chances de provoquer des effets de bord, et donc des régressions sur d’autres parties de l’application
  • La complexité continuerait à augmenter (et donc la dette technique également)
  • Sans parler de l’impact sur la santé mentale des développeurs !

Les nouvelles fonctionnalités doivent donc être développées sur une base saine, indépendante de l’application legacy.

Il existe plusieurs moyens de partir d’une base saine. 

Si on souhaite partir sur une application micro-services, il est possible de créer un ou plusieurs nouveaux services dédiés à un contexte particulier et de développer les nouvelles fonctionnalités sur ces nouveaux services.

Si on souhaite rester sur du monolithique, il faut virtuellement figer la base de code legacy, par exemple en créant un dossier nommé legacy ou v1 qui contiendra le code obsolète. Les fichiers présents dans ce dossier doivent être indiqués comme deprecated. L’utilisation des méthodes provenant de ces fichiers peuvent également loguer un message qui va dans ce sens dans la console.

👉 Figer par les tests : technique du Golden Master

Même si la base de code legacy ne doit plus évoluer, on ne compte pas l’abandonner car c’est elle qui fait tourner l’application à l’instant T. L’objectif est de la garder, mais de ne plus y ajouter de nouvelles fonctionnalités. 

Dans ce sens, on veut figer la base de code legacy. Malheureusement il est impossible qu’elle reste figée tant que des bugs existent sur l’application. Que faire alors quand on est amené à corriger un bug sur une fonctionnalité legacy ?

Ici, il y a deux possibilités :

  • soit on a la capacité de profiter du ticket pour redévelopper la fonctionnalité entière sur une base saine ;
  • soit on corrige la fonctionnalité précédente en ajoutant des tests unitaires qui viendront valider son comportement. Une fois testée, la base de code implémentant la fonctionnalité legacy est figée par le test, et on peut la laisser dans son coin.

👉 Réparer les fenêtres en supprimant le code mort

Faire un coup de propre ne fait jamais de mal. Bien que l’on souhaite progressivement abandonner la base de code legacy, celle-ci reste toujours en vigueur tant que les fonctionnalités qu’elle porte tournent encore dans l’application. 

De plus, sans documentation fournie, seule la base de code legacy peut renseigner sur les règles métier qui régissent l’application. 

Retirer la totalité du code mort de l’application legacy est une première étape pour y faire du propre. Cela n’a pas d’impact sur le fonctionnel, prend peu de temps, et réduit grandement l’effet broken windows.

👉 Ne jamais coupler les nouvelles fonctionnalités au code legacy

Maintenant que notre base de code est figée, on souhaite développer de nouvelles fonctionnalités sur une base de code saine. 

La première étape consiste à mettre en place la nouvelle base de code (nouveau serveur web REST, nouveau framework front…).

La seconde consiste à implémenter les nouvelles fonctionnalités sans toucher à la base de code legacy (qui aura au préalable été isolée).

À partir de là, l’important est de ne jamais se coupler au legacy.

Prenons l’exemple d’un site e-commerce qui tourne sur un code legacy, et pour lequel on veut créer une nouvelle fonctionnalité.


Exemple : Je veux afficher le top 5 des achats du client

Approche legacy :

Je vais utiliser la fonction getCustomerPurchases du système legacy, qui renvoie la totalité des achats du client avec les métadonnées associées (adresse de livraison, coût d’achat…), puis je vais ajouter un élément HTML responsable de l’affichage de ces commandes à ma page web legacy.

Approche refactoring :

Je vais développer une nouvelle fonction getLastCutomerPurchases sur ma base de code saine (nouveau service REST), qui ira chercher seulement les informations nécessaires à afficher (miniature de l’achat, titre de l’achat…). Je vais développer une nouvelle page web (nouveau framework front) sur ma base de code saine, qui sera responsable de l’affichage de ces achats.

De cette manière, on ne se couple pas à l’application legacy.

👉 Fonctionnalités dépendantes de la base de code legacy

Il peut arriver qu’une nouvelle fonctionnalité soit dépendante d’un morceau de code legacy.

Par exemple, je veux ajouter une fonctionnalité qui permette d’effectuer une action réservée à certains profils d’utilisateurs de mon application. Comment faire, si la fonction qui me permet de calculer la permission de le faire est implémentée dans ma base de code legacy et que celle-ci est trop compliquée à refactorer sur ma base de code saine ?

Dans ce cas, je peux éviter de me coupler à ma base de code legacy tout en appelant la fonction en passant par une couche d’anticorruption. Cette couche fera office d’adapter et traitera le système legacy comme un service tiers, sans corrompre ma base de code saine.

Je pourrais donc appeler la fonction hasRightToPerformAction de ma couche d’anticorruption sans toucher à la base de code legacy, car c’est ma couche d’anticorruption qui s’occupera d’interagir avec la base de code legacy.

👉 Autre approche pour gérer les dépendances

Il peut arriver qu’une nouvelle fonctionnalité soit dépendante d’une action effectuée sur une base de code historique. Dans ce cas, comment implémenter notre nouvelle fonctionnalité sans ajouter de code à la base de code legacy ? Il est possible de passer par un système de publication d’événements.


Exemple : Je dois développer une nouvelle fonctionnalité de recommandations d’achat sur mon site e-commerce. Pour cela, je souhaite que la réalisation d’un achat sur la plateforme enregistre la catégorie de l’objet acheté comme catégorie recommandée pour mon utilisateur.

Approche legacy :

Je vais ajouter à la méthode buyItem legacy un morceau de code qui va ajouter aux préférences de mon utilisateur la catégorie recommandée.

Approche refactoring :

La méthode buyItem legacy va publier un message itemBought sur le réseau. Je vais implémenter un nouveau service dédié qui ne dépend pas de la code base legacy recommendedCategories, celui-ci va lire le message et le traiter en conséquence en ajoutant à mon utilisateur la catégorie recommandée.

Cette approche se base sur l’event-driven architecture, et permet de continuer à développer de nouvelles fonctionnalités dépendantes des événements legacy sans toucher à la base de code legacy.

On peut tout à fait coupler cette approche avec l’architecture microservice, dans ce cas l’événement sera traité par un nouveau service indépendant dans son exécution du système legacy.

👉 Refactorer petit à petit

Maintenant que l’application peut évoluer sereinement, que les nouvelles fonctionnalités sortent à temps et sans régression, et que les développeurs reprennent goût à la vie, on peut refactorer petit à petit les briques legacy.

C’est l’occasion de créer des nouveaux tickets refactoring, qui pourront être intégrés tranquillement dans le sprint. La meilleure manière d’absorber cette dette est de profiter d’un ticket de fonctionnalité étant dépendant d’une fonctionnalité legacy pour la refaire à neuf.


Exemple : Je souhaite implémenter une nouvelle page web qui liste les adresses entrées par l’utilisateur.

Approche legacy :

Je vais utiliser la méthode getUserInformations legacy…à mes risques et périls.

Approche refactoring :

Je vais développer une nouvelle méthode getUserAddress pleinement testée et respectant les bonnes pratiques de développement. Une fois celle-ci prête, je l’utiliserai dans les prochains cas d’utilisation qui auront besoin des adresses de l’utilisateur. 

Étude de cas : nouveau parcours d’onboarding pour Needhelp 🏆

Nous avons été choisis par Needhelp pour développer un nouveau parcours d’onboarding pour ses utilisateurs. 

L’équipe de Needhelp avait du mal à délivrer des fonctionnalités, car ils étaient bloqués par une application legacy dont la dette avait atteint un stade important.

Ils pensaient qu’il serait impossible pour nous de livrer une fonctionnalité avant plusieurs mois… et pourtant on l’a fait !

Comment avons-nous procédé ?

Le CTO a isolé l’ancienne application web (à base de pages PHP renvoyant du contenu HTML) et a ajouté un nouveau dossier React au repository.

Dans ce dossier, les bases d’une application React.js, (un router, un système de gestion d’état…) ont été implémentées.

Nous avons développé les nouvelles pages d’onboarding en React, sans toucher à l’application legacy.

L’équipe de développeurs backend a, quant à elle, implémenté un nouveau serveur web REST.

Quand nous avions besoin d’une nouvelle route, l’équipe de développeurs backend a pris en charge le développement de cette nouvelle route. Même quand la fonction existait déjà dans la base de code legacy, l’équipe backend l’a réécrite en suivant les nouveaux standards.

Ainsi, nous  avons pu développer le parcours d’onboarding sans toucher à aucun moment aux briques de code legacy, et le parcours complet est sorti en à peu près deux mois.

L’équipe de développement de Needhelp a été soulagée  de sortir de l’impasse dans laquelle elle se trouvait, et a repris goût à la valeur de son métier : délivrer efficacement de nouvelles fonctionnalités à forte valeur ajoutée.

Si le sujet vous intéresse, vous pouvez aussi lire l’article de Bastien, UX designer, qui raconte le rôle des tests utilisateurs dans le projet Needhelp.

Comment rendre son application maintenable au cours du temps ? ♻️

Pour ne pas que l’histoire se répète, il convient de suivre certaines bonnes pratiques.. 

Pour conclure, je vous propose donc quelques méthodes à découvrir pour éviter de tomber dans le piège de la dette technique.

La Clean Architecture par exemple, vise à réduire les dépendances aux détails d’implémentation, tels que les services de mailing, de base de données … et à se concentrer sur la valeur métier de l’application. Pour en savoir plus vous pouvez regarder le replay de notre webinaire Introduction à l’Architecture & la Clean Architecture.

Les tests automatisés sont également un des piliers d’une application maintenable au cours du temps. Une fonctionnalité legacy est une fonctionnalité qui n’est pas testée. Les tests permettent à la fois de valider un comportement, mais aussi de prévenir les régressions lorsque la base de code doit évoluer.

L’approche TDD (Test Driven Development) aide également à produire des applications de qualité, et surtout à éviter les régressions.

Les principes SOLID quant à eux sont une bonne base au développement d’applications maintenables dans le temps.

L’approche craftman permet également de réduire l’entropie logicielle.

Il existe bien d’autres patterns, techniques ou stratégiques, visant à garantir la maintenabilité du système au cours du temps.

Mais la clé, c’est que chacun dans l’entreprise prenne conscience de l’enjeu qui se cache derrière la notion de dette technique. 

Loin de concerner uniquement les développeurs, la dette technique a un impact durable sur la performance du produit, sa compétitivité, la satisfaction des utilisateurs et la productivité des équipes. Idéalement, la résorption de la dette devrait donc occuper les développeurs autant que le développement de nouvelles fonctionnalités.

Tommy Alexandre
Lead developer @theTribe