Accueil / Articles / Sept questions à résoudre avant d’écrire la première ligne d’un article

Sept questions à résoudre avant d’écrire la première ligne d’un article

Une liste de contrôle préalable à la mise en œuvre couvrant le véritable problème utilisateur, les garanties de finalisation, la responsabilité des règles, les données héritées, les tentatives répétées et la concurrence, l’observabilité ainsi que la sécurité du déploiement.

2797 mots

Le moyen le plus rapide de se sentir productif sur une nouvelle tâche est d’ouvrir un éditeur et de commencer à travailler : ajouter l’endpoint, la colonne, le composant. Le problème, c’est que la première implémentation répond silencieusement à toutes les questions que personne n’a posées, et ces réponses se transforment en schémas, en contrats API et en tests dont il est coûteux de revenir en arrière. Ce guide passe en revue sept questions auxquelles les ingénieurs expérimentés répondent avant de coder, ce qui se passe lorsqu’elles sont ignorées, et comment garder l’exercice proportionné afin qu’il accélère plutôt que de ralentir la livraison.

Pourquoi la première implémentation a autant d’importance

Lorsqu’une demande arrive, commencer par le code permet de rendre une tâche abstraite concrète et plus gérable. Cependant, les questions en suspens n’ont pas disparu pour autant. Quelqu’un doit encore décider à qui appartient une règle métier, ce que signifie « terminé » pour une opération en plusieurs étapes, et ce qu’il advient des enregistrements créés avant le changement. Si personne ne prend cette décision, c’est le code qui la prend par hasard.

Cette décision accidentelle reste rarement confinée à un seul endroit. La structure de la première version devient généralement le plan de la table, le format de réponse, l’outil commun que tout le monde importe ainsi que le modèle d’état sur lequel l’interface utilisateur s’appuie. Une fois que d’autres codes l’invoquent et que les données de production y correspondent, changer de direction implique des migrations, des correctifs de compatibilité et des mises à jour coordonnées. Passer une heure à se poser les bonnes questions au préalable reste, en comparaison, peu coûteux.

D’extérieur, cela peut sembler de l’hésitation : lire les workflows existants, se demander ce que l’utilisateur cherche réellement à accomplir, vérifier le comportement des données anciennes, discuter des échecs partiels. En pratique, il s’agit du même processus de résolution de problèmes que le code devrait de toute façon effectuer, simplement réalisé tant qu’il est encore peu coûteux de changer d’avis.

1. Séparer la fonctionnalité demandée du problème sous-jacent

Les demandes arrivent souvent sous forme de solutions : ajouter un bouton, un filtre, une fonction d’exportation, un nouveau statut. La demande peut être tout à fait raisonnable, mais elle décrit ce que quelqu’un imagine pouvoir être construit, et non la frustration ou le résultat business qui en découle.

Comprendre ce besoin fondamental change ce que l’on conçoit. Une demande d’export en CSV provient souvent de managers qui ne peuvent pas comparer les chiffres hebdomadaires entre départements. Un export est utile, mais il génère également des tâches manuelles répétitives avec des tableurs, que un rapport enregistré ou un résumé planifié pourraient éliminer complètement. Une demande d’une valeur de statut supplémentaire peut révéler qu’un seul champ est déjà surchargé, servant en même temps à gérer les paiements, les approbations et l’exécution des commandes. Ajouter cette valeur clôture le dossier, mais rend le modèle de données encore plus difficile à comprendre.

Tout cela ne signifie pas qu’il faille interroger chaque petite demande ou transformer un simple changement en atelier de développement. L’objectif est d’en apprendre suffisamment sur la situation de l’utilisateur pour déterminer si le changement proposé améliore réellement les résultats. Quelques questions ciblées suffisent généralement :

  • Qui ne peut pas terminer son travail aujourd’hui ?
  • Que font-ils actuellement à la main ?
  • Quelle décision cette nouvelle information permettra-t-elle de prendre ?
  • Qu’est-ce qui devient possible une fois cette fonctionnalité disponible ?
  • En sautant cette étape, les ingénieurs optimisent naturellement la demande telle qu’elle est formulée. Le bouton est simple, le composant réutilisable, l’API bien structurée, mais la fonctionnalité déçoit malgré tout car elle résout le ticket de manière plus précise que le problème lui-même. Il ne s’agit pas de retarder l’écriture du code, mais de s’assurer que ce dernier constitue bien la solution adéquate.

    2. Définir ce que signifie le succès pour toute l’opération

    De nombreuses exigences décrivent une action sans préciser quand elle est terminée. Soumettre une demande, approuver un enregistrement, dupliquer un projet ou synchroniser des données semblent tous simples tant qu’il n’y a que quelques étapes impliquées et que l’une d’elles ne échoue pas.

    Prenez un flux d’approbation qui persiste le changement, écrit une ligne dans le journal d’audit, émet un événement et notifie la personne qui a demandé l’approbation. Si l’écriture réussit mais que la notification échoue, l’approbation a-t-elle eu lieu ? Si l’utilisateur réessaie, le dossier pourrait-il être approuvé deux fois ? Si l’entrée du journal d’audit ne peut pas être écrite, faut-il annuler le changement de statut ? Si l’événement est retardé, l’approbation est-elle terminée ou en attente ? Ce ne sont pas des détails d’implémentation anodins. Ils définissent ce que le produit promet à ses utilisateurs.

    L’exercice utile consiste à définir les limites de finalisation avant d’écrire le flux de travail :

    • Les effets qui doivent réussir ou échouer ensemble, car un résultat partiel constituerait un état invalide, doivent faire partie d’une seule transaction.
  • Les effets qui sont précieux mais secondaires, tels qu’un e-mail de bienvenue, ne devraient pas déterminer si l’action principale a réussi. Ils relèvent souvent d’une tâche en arrière-plan avec des tentatives répétées et un enregistrement explicite « en attente ».
  • La même question se pose pour les petites fonctionnalités. Lorsque quelqu’un lance une exportation, est-elle considérée comme réussie dès que le fichier existe et que la tâche a été acceptée, ou un lien apparaîtra-t-il plus tard ? Lorsque l’interface affiche « Enregistré », le serveur a-t-il confirmé la persistance des données ou seul l’état local a-t-il changé ?

    Laisser cela vague fait que chaque couche invente sa propre définition. L’interface utilisateur indique un succès alors que le backend est encore en train de fonctionner, un processus tente à nouveau quelque chose que l’utilisateur pense déjà avoir échoué, et le suivi rapporte une requête saine même si un effet secondaire essentiel a disparu. Définir d’abord les garanties permet généralement de simplifier la mise en œuvre, car chaque étape a alors une tâche claire. Cela influence également le contrat de réponse : une API qui renvoie « accepted » est différente d’une autre qui renvoie « done », et les clients doivent savoir lequel ils reçoivent.

    3. Déterminer quelle couche est responsable de chaque décision

    Une fonctionnalité peut produire des résultats corrects tout en restant dangereuse si la décision se trouve dans la mauvaise couche. Exemples courants :

    • L’interface utilisateur cache un bouton aux utilisateurs sans autorisation, mais l’API accepte la requête lorsqu’elle est envoyée directement.
  • Dans chaque cas, la règle existe, mais tous les chemins ne sont pas contraints de passer par elle.

    La solution consiste à trouver la couche disposant d’assez d’autorité pour gérer cette règle. L’interface utilisateur peut refléter les permissions pour une meilleure utilisation, mais elle ne constitue jamais la frontière de sécurité. Un contrôleur est un endroit approprié pour valider la structure d’une requête HTTP, tandis que les règles métier doivent généralement être placées à un niveau plus profond afin que les jobs en arrière-plan et les appels internes présentent un comportement identique. Une vérification de unicité au niveau de l’application génère des erreurs explicites, mais ce n’est qu’une contrainte de base de données qui protège réellement l’invariant lorsque des écritures ont lieu simultanément.

    La propriété s’applique tant à l’état qu’aux règles. Les filtres qui doivent être partageables et survivre à un renouvellement conviennent naturellement à l’URL. Les entrées temporaires appartiennent au formulaire. Les permissions et le statut persistant proviennent du serveur, et le client ne doit pas reconstruire sa propre version à partir de suppositions locales.

    Lorsque l’autorité n’est pas claire, on se retrouve avec des codes de coordination : vérifications redondantes à plusieurs niveaux, des copies de la même valeur qui doivent rester synchronisées, et des modifications qui se transforment en recherches de remplacement. Une mise à jour de la politique affecte alors l’interface utilisateur, le contrôleur, le service, un travailleur ainsi qu’une ou deux requêtes, sans garantie que chaque copie signifie toujours la même chose. Assignez à chaque décision importante un responsable déterminé. Les autres niveaux peuvent afficher, mettre en cache, appliquer ou transmettre le résultat, mais ils ne doivent pas le redéfinir. Cela réduit à la fois les risques de sécurité et les coûts de maintenance, car tout le monde sait où se trouve la source de vérité.

    4. Tenir compte des données déjà existantes

    Un nouveau code est écrit pour le modèle souhaité. Cependant, les données de production contiennent également des traces de tous les modèles précédents.

    Rendre un champ obligatoire est simple pour les enregistrements créés après la mise à jour, mais des milliers de lignes plus anciennes ne le possèdent pas nécessairement. Un modèle de statut redessiné peut bien décrire les flux de travail futurs, tout en laissant les lignes historiques bloquées dans des statuts que le nouveau code ne reconnaît plus. Une relation devenant obligatoire peut faire référence à une entité qui n’existait tout simplement pas au moment où les lignes anciennes ont été enregistrées.

    Au avant de mettre en place des validations ou de modifier le schéma, demandez-vous :

    • Les enregistrements existants peuvent-ils être migrés de manière fidèle ?
    • Un état temporaire « inconnu » est-il nécessaire ?
    • Ce changement réécrit-il l’histoire, ou ne modifie-t-il que le comportement futur ?

    La sincérité est le mot clé. Compléter chaque lacune par une valeur par défaut pratique peut satisfaire une contrainte NOT NULL tout en introduisant des données fausses. Si le département responsable d’un ancien enregistrement n’a jamais été indiqué, l’attribuer au département actuel simplifie les requêtes mais rend les rapports historiques moins fiables. Parfois, un schéma honnête peut prévoir des valeurs telles que « inconnu » ou « héritage », car l’ignorance fait réellement partie du passé de cet enregistrement.

    Les anciennes structures existent également en dehors de la base de données. Les clients plus anciens peuvent encore envoyer des formats de données antérieurs, les tâches planifiées dépendent parfois de valeurs d’état que le nouveau flux souhaite supprimer, et les rapports interprètent parfois les colonnes selon des règles qui ont changé il y a longtemps.

    Vous n’êtes pas obligé de maintenir indéfiniment tous les comportements anciens, mais la décision de migration doit être explicite. Certaines données peuvent être transformées en toute sécurité, d’autres nécessitent une révision manuelle, certains anciens clients méritent une fenêtre de compatibilité et d’autres peuvent être retirés délibérément. Ignorer cette question ne la fait pas disparaître : elle réapparaît sous forme de solutions de secours éparses, de colonnes pouvant être nulles que personne ne comprend, de migrations échouées et d’incidents de support après déploiement. Prendre une décision tôt permet à l’équipe d’assurer une transition cohérente plutôt que de compter sur de nombreuses suppositions locales.

    5. Partir du principe que le travail sera répété et concurrentiel

    Les descriptions de fonctionnalités imaginent généralement un utilisateur, un clic et une séquence simple : la demande arrive une fois, rien d’autre ne touche le enregistrement entre-temps, et la réponse parvient au client. En production, aucune de ces garanties n’existe.

    • Un utilisateur clique à nouveau parce que la page semble bloquée.
  • La connexion mobile se coupe après que le serveur ait terminé son travail mais avant l’arrivée de la réponse.
  • Une file d’attente envoie deux fois le même message.
  • Deux administrateurs approuvent le même élément en attente à quelques secondes d’intervalle.
  • Une tâche planifiée met à jour un enregistrement que l’utilisateur consulte encore dans une version plus ancienne.
  • La question à se poser au préalable est de savoir si l’opération peut être exécutée plus d’une fois sans risque et de manière concurrentielle. Si une répétition n’a aucun effet néfaste, des mécanismes supplémentaires peuvent être inutiles. Si une répétition crée un deuxième paiement, une invitation, un fichier ou une réservation de stock, le système doit disposer d’un moyen de reconnaître que plusieurs tentatives représentent une seule action logique.

    La boîte à outils comprend des clés d’idempotence, des contraintes uniques, des mises à jour conditionnelles, des colonnes de version pour le verrouillage optimiste, des transactions ainsi qu’un tableau des IDs de messages traités. Le choix dépend du lieu où réside le risque. Ce qui ne fonctionne jamais, c’est l’approche « nous avons vérifié d’abord », comme si aucun autre processus ne pouvait intervenir entre la vérification et l’écriture.

    Les bugs de concurrence sont particulièrement dangereux car chaque étape semble correcte lors de l’examen. Le défaut réside dans l’écart entre la lecture et l’écriture : deux requêtes lisent un snapshot identique et valide, chacune passe ses vérifications, et chacune enregistre un résultat qui n’aurait dû apparaître qu’une seule fois. Il est bien préférable que la base de données rejette l’une des deux opérations concurrentes en signalant un conflit visible, plutôt que de stocker deux vérités contradictoires qu’il faudra ensuite résoudre manuellement.

    6. Planifier la manière dont la fonctionnalité s’expliquera en production

    Sur votre machine, vous disposez de points d’arrêt, de la possibilité de répéter une action et d’une mémoire fraîche du design. En production, l’équipe n’obtient parfois qu’un message de support indiquant que quelque chose ne fonctionne pas.

    Imaginez donc l’enquête à mener avant d’écrire le code. Si l’opération échoue, comment savoir quel étape a posé problème ? Un seul appel peut-il être suivi à travers plusieurs services ? Les journaux d’activité indiqueront-ils si l’action a été exécutée une fois ou trois fois ? Pouvez-vous distinguer les états « jamais démarré », « en cours », « partiellement terminé » et « échoué » ?

    Ce n’est pas une invitation à enregistrer tout ce qui se passe. Un volume non structuré rend les enquêtes plus difficiles, et non plus faciles. Une observabilité utile ne recueille que ce qui est nécessaire pour reconstituer le déroulement d’une opération importante :

    • Un identifiant de demande ou de corrélation
    • L’identifiant du ressource et le nom de l’opération
    • La durée
  • La transition d’état qui a eu lieu
  • Le numéro de tentative
  • Une catégorie d’erreur stable
  • Il faut également préserver la signification de l’échec. Une requête qui a échoué ne doit pas se transformer silencieusement en une liste vide. Un délai d’attente du fournisseur ne doit pas faire ignorer le fait que la partie distante a pu terminer son travail. Un bloc de gestion des erreurs généraliste ne doit pas réduire chaque cause à un message générique avant qu’elle n’atteigne le système de journalisation.

    Pensez également au processus de récupération. Est-il sûr de relancer une tâche qui a échoué ? Le service peut-il connaître l’état actuel d’une opération sans devoir interroger manuellement plusieurs tables ? L’utilisateur peut-il essayer à nouveau en toute sécurité, et quelqu’un peut-il lui fournir un compte rendu précis du résultat ?

    Les fonctionnalités opaques deviennent coûteuses dès qu’un problème survient, et l’ajout ultérieur de mécanismes d’enregistrement des événements est souvent trop tardif, car le contexte pertinent n’existait que pendant l’exécution de l’opération. Concevoir les moyens de preuve dès le départ permet de les intégrer à la fonctionnalité elle-même plutôt que de devoir appliquer des correctifs d’urgence après un incident.

    7. Déterminer comment prouver que le changement est sûr

    Lorsque les tests sont écrits après le code, ils ont tendance à refléter sa structure actuelle. Un outil d’aide existe, donc le test vérifie qu’il a été appelé ; une solution de secours existe, donc un test s’assure qu’elle est utilisée ; un répertoire est simulé, donc le test confirme que la simulation a renvoyé ce qui lui avait été demandé. De tels tests réussissent sans apporter de preuves significatives.

    Décider d’abord de la preuve révèle souvent des faiblesses dans la conception. Si une opération doit être idempotente, le test doit la exécuter plusieurs fois. Si les approbations concurrentes d’un même enregistrement doivent être impossibles, le test nécessite des mises à jour véritablement concurrentes. Si « non trouvé » et « échec » sont des résultats distincts, le contrat doit permettre de les observer tous deux. Si la règle est imposée par une contrainte de base de données, aucun test unitaire simulé ne peut prouver qu’elle est respectée.

    Cela ne signifie pas que chaque fonctionnalité nécessite un ensemble complet de tests bout en bout. Choisissez le niveau de test en fonction de la couche qui impose réellement la garantie :

    • Une transformation pure peut être testée directement au niveau unitaire.
    • Un contrat API nécessite généralement un test d’intégration.
    • Une invariante imposée dans la base de données doit être testée sur une base réelle.
  • Une migration risquée peut nécessiter un suivi, un déploiement par étapes, ou une flag de fonctionnalité avec une date de suppression prévue.
  • La réversibilité fait également partie de cette discussion. Si le changement présente des problèmes, peut-il être désactivé ou annulé sans perdre les données écrites entre-temps ? La version précédente de l’application fonctionnera-t-elle encore après le changement de schéma, ou la migration nécessite-t-elle une séquence d’expansion et de contraction ? Pouvez-vous le déployer auprès d’un petit groupe avant que tout le monde ne s’y fie ?

    Si un design est difficile à tester ou à inverser, c’est souvent le signe qu’une opération assume trop de responsabilités, ou que la modification nécessite une étape intermédiaire plus simple. Une preuve absolue n’est pas l’objectif, car le logiciel comporte toujours une part d’incertitude. Ce que l’on souhaite, c’est que les garanties essentielles soient visibles dans les tests et les métriques, et que les choix dangereux puissent être annulés, afin qu’une erreur devienne une leçon plutôt que des dommages permanents.

    Garantir une proportionnalité dans le travail préalable

    Ces questions ne prouvent pas pour autant que la planification l’emporte toujours sur l’exécution. Une analyse excessive peut ralentir des tâches simples et aboutir à une architecture conçue pour faire face à des risques qui ne se concrétiseront jamais. Un critère raisonnable consiste à ne se concentrer que sur les décisions qui seraient coûteuses en cas d’erreur : tout ce qui concerne des données persistantes, de l’argent, des permissions, des effets secondaires externes ou des contrats publics. Une simple modification de copie ou un refactoring interne derrière une interface stable nécessite rarement la liste complète.

    Sous une forme simplifiée, cette liste de contrôle peut être intégrée dans la description d’une tâche :

    • Le véritable problème, en une phrase, et qui en est responsable
    • Ce que signifie « terminé », et quels effets sont secondaires
    • Le responsable unique de chaque règle métier
    • La stratégie concernant les données existantes et les anciens clients
    • Le comportement en cas de tentative répétée et d’accès concurrent
    • Ce qui est enregistré et comment les pannes sont résolues
  • Comment la garantie sera testée et comment le changement pourra être annulé
  • Conclusion

    Lorsque ces questions ont des réponses, le code devient généralement très clair et direct. Le modèle d’état comporte moins de combinaisons impossibles, chaque règle a une place bien définie, la base de données garantit les invariants, les réponses indiquent si le travail est terminé ou simplement accepté, et les tests ciblent la garantie plutôt que l’organisation actuelle des fonctions.

    C’est pourquoi des ingénieurs prudents peuvent sembler lents au début d’une tâche tout en la terminant plus rapidement : ils refusent de laisser l’ambiguïté du produit, les données héritées, la concurrence et les points aveugles opérationnels se transformer silencieusement en décisions techniques permanentes. Le moment opportun pour aborder ces problèmes est avant que la première implémentation pratique ne bénéficie des appels, des tests et des données de production. Écrire la fonction est rarement la partie difficile ; décider de ce qu’elle est autorisée à signifier l’est.

    Lectures complémentaires

  • Conception de système backend par le nœud d’échec : de l’accourreur de URLs à l’e-commerce — Une approche axée sur les exigences pour concevoir des backends Node.js : quand ajouter des balanceurs de charge, Redis, des réplicas, des files d’attente et des limites de vitesse, ainsi que le coût de chacun d’eux.