Éviter les enregistrements en double dans les applications CRUD nécessite plusieurs couches : contraintes d'unicité en base, clés d'idempotence et états UI qui empêchent les soumissions doubles.

Un enregistrement en double, c'est lorsque votre application stocke la même chose deux fois. Cela peut être deux commandes pour le même checkout, deux tickets de support avec les mêmes détails, ou deux comptes créés depuis le même flux d'inscription. Dans une appli CRUD, les doublons ressemblent souvent à des lignes normales prises isolément, mais elles sont incorrectes quand on regarde l'ensemble des données.
La plupart des doublons commencent par un comportement normal. Quelqu'un clique sur Créer deux fois parce que la page semble lente. Sur mobile, un double tap est facile à rater. Même des utilisateurs précautionneux réessaieront si le bouton paraît encore actif et qu'il n'y a pas d'indication claire qu'une action est en cours.
Ensuite vient le milieu chaotique : réseaux et serveurs. Une requête peut expirer et être renvoyée automatiquement. Une bibliothèque cliente peut répéter un POST si elle pense que la première tentative a échoué. La première requête peut réussir, mais la réponse se perd, alors l'utilisateur réessaie et crée une seconde copie.
Vous ne pouvez pas résoudre ça avec une seule couche parce que chaque couche ne voit qu'une partie de l'histoire. L'interface peut réduire les doubles soumissions accidentelles, mais elle ne peut pas empêcher les retransmissions liées à une mauvaise connexion. Le serveur peut détecter les répétitions, mais il a besoin d'un moyen fiable pour reconnaître « c'est la même création qu'avant ». La base de données peut appliquer des règles, mais seulement si vous définissez ce que « la même chose » signifie.
L'objectif est simple : rendre les créations sûres même quand la même requête arrive deux fois. La seconde tentative doit devenir une opération nulle, une réponse claire « déjà créé », ou un conflit contrôlé, pas une seconde ligne.
Beaucoup d'équipes traitent les doublons comme un problème de base de données. En pratique, les doublons naissent souvent plus tôt, quand la même action de création est déclenchée plusieurs fois.
Un utilisateur clique sur Créer et rien ne semble se passer, alors il clique encore. Ou il appuie sur Entrée, puis clique sur le bouton juste après. Sur mobile, on peut avoir deux taps rapides, des événements tactile et clic qui se chevauchent, ou un geste qui s'enregistre deux fois.
Même si l'utilisateur ne soumet qu'une fois, le réseau peut répéter la requête. Un timeout peut déclencher une retransmission. Une appli hors-ligne peut mettre en file un « Sauvegarder » et le renvoyer à la reconnexion. Certaines bibliothèques HTTP réessaient automatiquement sur certaines erreurs, et vous ne le remarquerez qu'en voyant des lignes en double.
Les serveurs répètent parfois le travail volontairement. Les files de travaux réessaient les jobs échoués. Les fournisseurs de webhooks livrent souvent le même événement plusieurs fois, surtout si votre endpoint est lent ou renvoie un statut non-2xx. Si votre logique de création est déclenchée par ces événements, considérez que des doublons arriveront.
La concurrence crée les doublons les plus sournois. Deux onglets soumettent le même formulaire à quelques millisecondes d'intervalle. Si votre serveur fait « existe-t-il ? » puis insert, les deux requêtes peuvent passer la vérification avant qu'une insertion n'ait lieu.
Considérez le client, le réseau et le serveur comme des sources distinctes de répétitions. Vous aurez besoin de défenses dans les trois.
Si vous voulez un endroit fiable pour arrêter les doublons, mettez la règle en base de données. Les correctifs UI et les vérifications serveur aident, mais ils peuvent échouer face aux retransmissions, à la latence ou à deux utilisateurs agissant en même temps. Une contrainte d'unicité en base est l'autorité finale.
Commencez par choisir une règle d'unicité réaliste qui correspond à la façon dont les gens pensent l'enregistrement. Exemples courants :
Faites attention aux champs qui semblent uniques mais ne le sont pas, comme un nom complet.
Une fois la règle définie, appliquez-la avec une contrainte d'unicité (ou un index unique). Ainsi, la base rejettera une seconde insertion qui violerait la règle, même si deux requêtes arrivent au même moment.
Quand la contrainte se déclenche, décidez de l'expérience utilisateur. Si créer un doublon est toujours incorrect, bloquez avec un message clair (« Cet e-mail est déjà utilisé »). Si les retries sont fréquents et que l'enregistrement existe déjà, il est souvent préférable de traiter le retry comme une réussite et de retourner l'enregistrement existant (« Votre commande a déjà été créée »).
Si votre opération de création signifie vraiment « créer ou réutiliser », un upsert peut être le pattern le plus propre. Exemple : « créer un client par email » peut insérer une nouvelle ligne ou retourner la ligne existante. N'utilisez ceci que si cela correspond au sens métier. Si des payloads légèrement différents peuvent arriver pour la même clé, décidez quels champs peuvent être mis à jour et lesquels doivent rester inchangés.
Les contraintes d'unicité ne remplacent pas les clés d'idempotence ni de bons états UI, mais elles vous donnent un arrêt strict sur lequel tout le reste peut s'appuyer.
Une clé d'idempotence est un jeton unique qui représente une intention utilisateur, par exemple « créer cette commande une fois ». Si la même requête est renvoyée (double clic, retransmission réseau, reprise mobile), le serveur la considère comme un retry, pas comme une nouvelle création.
C'est l'un des outils les plus pratiques pour sécuriser les endpoints de création quand le client ne sait pas si la première tentative a réussi.
Les endpoints qui en bénéficient le plus sont ceux où un doublon coûte cher ou prête à confusion : commandes, factures, paiements, invitations, abonnements, et formulaires qui déclenchent des e-mails ou des webhooks.
Lors d'un retry, le serveur doit renvoyer le résultat original de la première tentative réussie, y compris le même ID créé et le même code de statut. Pour cela, stockez un petit enregistrement d'idempotence indexé par (utilisateur ou compte) + endpoint + clé d'idempotence. Sauvegardez à la fois le résultat (ID, body de réponse) et un état « en cours » pour que deux requêtes quasi simultanées ne créent pas deux lignes.
Conservez les enregistrements d'idempotence assez longtemps pour couvrir les retries réels. Une base commune est 24 heures. Pour les paiements, beaucoup d'équipes gardent 48–72 heures. Un TTL maintient le stockage borné et correspond à la durée probable d'un retry.
Si vous générez des APIs avec un assistant comme Koder.ai, il faut quand même rendre l'idempotence explicite : acceptez une clé fournie par le client (header ou champ) et appliquez la règle « même clé, même résultat » côté serveur.
L'idempotence rend une requête de création sûre à répéter. Si le client réessaie à cause d'un timeout (ou d'un double clic), le serveur renvoie le même résultat au lieu de créer une seconde ligne.
Idempotency-Key), mais l'inclure dans le JSON peut aussi convenir.Le point clé est que « vérifier + stocker » doit être sûr face à la concurrence. En pratique, vous stockez l'enregistrement d'idempotence avec une contrainte d'unicité sur (scope, key) et traitez les conflits comme un signal pour réutiliser l'enregistrement.
if idempotency_record exists for (user_id, key):
return saved_response
else:
begin transaction
create the row
save response under (user_id, key)
commit
return response
Exemple : un client clique sur « Créer facture », l'application envoie la clé abc123, et le serveur crée la facture inv_1007. Si le téléphone perd le signal et réessaye, le serveur répond avec le même inv_1007, pas inv_1008.
Quand vous testez, n'en restez pas au « double clic ». Simulez une requête qui timeoute côté client mais qui aboutit côté serveur, puis réessayez avec la même clé.
Les défenses côté serveur sont importantes, mais beaucoup de doublons commencent par un humain qui répète une action normale. Une bonne UI rend le chemin sûr évident.
Désactivez le bouton de soumission dès que l'utilisateur soumet. Faites-le au premier clic, pas après la validation ou après le départ de la requête. Si le formulaire peut être soumis par plusieurs contrôles (bouton et Entrée), verrouillez tout l'état du formulaire, pas seulement un bouton.
Affichez un état de progression clair qui répond à une question : est-ce que ça fonctionne ? Un simple « Enregistrement… » ou un spinner suffit. Gardez la mise en page stable pour que le bouton ne bouge pas et n'incite pas à cliquer de nouveau.
Un petit ensemble de règles évite la plupart des doubles soumissions : mettez un flag isSubmitting au début du handler de soumission, ignorez les nouvelles soumissions tant qu'il est vrai (pour clic et Entrée), et ne le réinitialisez que lorsque vous avez une vraie réponse.
Les réponses lentes sont là où beaucoup d'applis dérapent. Si vous réactivez le bouton sur un timer fixe (par exemple après 2 secondes), les utilisateurs peuvent resoumettre alors que la première requête est toujours en vol. Réactivez uniquement quand la tentative est terminée.
Après une réussite, rendez la resoumission improbable. Naviguez ailleurs (vers la page du nouvel enregistrement ou la liste) ou affichez un état de succès clair avec l'enregistrement créé visible. Évitez de laisser le même formulaire rempli à l'écran avec le bouton activé.
Les bugs de doublons persistants viennent de comportements « bizarres mais courants » : deux onglets, un rafraîchissement, ou un téléphone qui perd le signal.
D'abord, scoplez correctement l'unicité. « Unique » signifie rarement « unique dans toute la base ». Cela peut vouloir dire une seule par utilisateur, une par workspace, ou une par tenant. Si vous synchronisez avec un système externe, vous pouvez avoir besoin d'unicité par source externe + ID externe. Une approche sûre est d'écrire la phrase exacte que vous voulez appliquer (par exemple « Un numéro de facture par tenant par année »), puis de l'imposer.
Le comportement multi-onglet est un piège classique. Les états de chargement aident dans un onglet, mais ils ne font rien entre onglets. C'est là que les défenses serveur doivent tenir.
Le bouton Retour et le rafraîchissement peuvent déclencher des resoumissions accidentelles. Après une création réussie, les utilisateurs rafraîchissent souvent pour « vérifier », ou appuient sur Retour et soumettent à nouveau un formulaire qui paraît encore éditable. Préférez une vue dédiée au résultat créé plutôt que le formulaire original, et faites en sorte que le serveur gère les rejets sûrs.
Le mobile ajoute des interruptions : mise en arrière-plan, réseaux instables et retries automatiques. Une requête peut réussir mais l'app ne reçoit pas la réponse, donc elle réessaie à la reprise.
Le mode d'échec le plus courant est de considérer l'UI comme la seule garde-fou. Un bouton désactivé et un spinner aident, mais ils ne couvrent pas les rafraîchissements, les réseaux mobiles instables, l'ouverture d'un second onglet ou un bug client. Le serveur et la base doivent pouvoir dire « cette création a déjà eu lieu ».
Un autre piège est de choisir le mauvais champ pour l'unicité. Si vous imposez une contrainte sur quelque chose qui n'est pas vraiment unique (un nom de famille, un timestamp arrondi, un titre libre), vous bloquerez des enregistrements valides. Utilisez plutôt un identifiant réel (comme un ID externe) ou une règle scindée (unique par utilisateur, par jour, ou par enregistrement parent).
Les clés d'idempotence sont faciles à mal implémenter. Si le client génère une nouvelle clé à chaque retry, vous obtenez une nouvelle création à chaque fois. Gardez la même clé pour toute l'intention utilisateur, du premier clic aux retries.
Surveillez aussi ce que vous renvoyez sur un retry. Si la première requête a créé l'enregistrement, un retry doit renvoyer le même résultat (ou au moins le même ID), pas une erreur vague qui pousse l'utilisateur à réessayer.
Si une contrainte d'unicité bloque un doublon, ne cachez pas ça derrière « Quelque chose s'est mal passé ». Dites clairement ce qui s'est passé : « Ce numéro de facture existe déjà. Nous avons conservé l'original et n'avons pas créé de second enregistrement. »
Avant la mise en prod, faites un passage rapide spécifiquement pour les chemins de création. Les meilleurs résultats viennent d'empiler des défenses afin qu'un clic manqué, un retry ou un réseau lent ne puisse pas créer deux lignes.
Confirmez trois choses :
Un test intuitif : ouvrez le formulaire, cliquez deux fois rapidement, puis rafraîchissez en plein envoi et réessayez. Si vous pouvez créer deux enregistrements, des utilisateurs réels pourront le faire aussi.
Imaginez une petite appli de facturation. Un utilisateur remplit une facture et appuie sur Créer. Le réseau est lent, l'écran ne change pas immédiatement, et il appuie à nouveau sur Créer.
Avec seulement une protection UI, vous pouvez désactiver le bouton et afficher un spinner. Ça aide, mais ce n'est pas suffisant. Un double tap peut encore passer sur certains appareils, une retransmission peut survenir après un timeout, ou l'utilisateur peut soumettre depuis deux onglets.
Avec seulement une contrainte d'unicité en base, vous pouvez bloquer les doublons exacts, mais l'expérience peut être brutale. La première requête réussit, la seconde heurte la contrainte, et l'utilisateur voit une erreur alors que la facture a bien été créée.
Le résultat propre est idempotence plus contrainte d'unicité :
Un message UI simple après le second tap : « Facture créée — nous avons ignoré la soumission en double et conservé votre première requête. »
Une fois la base en place, les gains suivants viennent de la visibilité, du nettoyage et de la cohérence.
Ajoutez du logging léger autour des chemins de création pour pouvoir distinguer une action utilisateur réelle d'un retry. Loggez la clé d'idempotence, les champs uniques impliqués et le résultat (créé vs retourné existant vs rejeté). Vous n'avez pas besoin d'outils lourds pour commencer.
Si des doublons existent déjà, nettoyez-les selon une règle claire et conservez une trace d'audit. Par exemple, conservez l'enregistrement le plus ancien comme « gagnant », rattachez les lignes liées (paiements, lignes de facture), et marquez les autres comme fusionnés plutôt que de les supprimer. Cela facilite le support et le reporting.
Écrivez vos règles d'unicité et d'idempotence en un seul endroit : ce qui est unique et dans quel scope, combien de temps vivent les clés d'idempotence, à quoi ressemblent les erreurs, et ce que l'UI doit faire sur les retries. Cela empêche de nouveaux endpoints de contourner silencieusement les garde-fous.
Si vous générez rapidement des écrans CRUD avec Koder.ai (koder.ai), ça vaut la peine d'inclure ces comportements dans votre template par défaut : contraintes uniques dans le schéma, endpoints de création idempotents dans l'API, et états de chargement clairs dans l'UI. Ainsi, la vitesse n'ira pas au détriment de données propres.
Un enregistrement en double se produit lorsque la même chose réelle est stockée deux fois, par exemple deux commandes pour un seul paiement ou deux tickets pour le même problème. Cela arrive généralement parce que la même action de “création” s'exécute plus d'une fois à cause de doubles soumissions utilisateur, de retransmissions ou de requêtes concurrentes.
Parce qu'une seconde création peut être déclenchée sans que l'utilisateur ne s'en rende compte : un double tap sur mobile, appuyer sur Entrée puis cliquer sur le bouton, etc. Même si l'utilisateur soumet une seule fois, le client, le réseau ou le serveur peuvent relancer la requête après un timeout, et le serveur ne peut pas supposer que "POST signifie une seule fois".
Pas de façon fiable. Désactiver le bouton et afficher « Enregistrement… » diminue les doubles soumissions accidentelles, mais n'empêche pas les retransmissions causées par un réseau instable, les rafraîchissements, plusieurs onglets, des workers en arrière-plan ou des réémissions de webhooks. Il faut aussi des protections côté serveur et en base de données.
Une contrainte d'unicité est la dernière ligne de défense qui empêche l'insertion de deux lignes même si deux requêtes arrivent en même temps. Elle fonctionne mieux quand vous définissez une règle d'unicité réelle (souvent scindée par scope, comme par tenant ou workspace) et que vous l'appliquez directement en base.
Elles répondent à des problèmes différents. Les contraintes d'unicité bloquent les doublons basés sur un champ (comme un numéro de facture), tandis que les clés d'idempotence rendent une tentative de création spécifique sûre à répéter (même clé = même résultat). Les utiliser ensemble apporte sécurité et meilleure expérience lors des retransmissions.
Générez une clé par intention utilisateur (un appui sur « Créer »), réutilisez-la pour toutes les retransmissions de cette intention, et envoyez-la à chaque tentative. La clé doit rester stable malgré les timeouts et les reprises d'application, mais ne doit pas être réutilisée pour une autre création.
Conservez un enregistrement d'idempotence identifié par le scope (par exemple utilisateur ou compte), l'endpoint et la clé d'idempotence, et stockez la réponse renvoyée lors de la première requête réussie. Si la même clé arrive à nouveau, renvoyez la réponse enregistrée avec le même ID créé au lieu de créer une nouvelle ligne.
Utilisez une approche sûre face à la concurrence « vérifier + stocker », typiquement en imposant une contrainte d'unicité sur l'enregistrement d'idempotence (scope + clé). Ainsi, deux requêtes presque simultanées ne pourront pas toutes deux se déclarer "premières" ; l'une devra réutiliser le résultat déjà stocké.
Conservez-les assez longtemps pour couvrir les retransmissions réalistes : une valeur courante est 24 heures, plus longue pour des flux comme les paiements. Ajoutez un TTL pour que le stockage ne croisse pas indéfiniment et choisissez une durée qui correspond au délai pendant lequel un client est susceptible de relancer.
Considérez une retransmission comme un retry réussi quand l'intention est clairement la même, et retournez l'enregistrement original (même ID) plutôt qu'une erreur vague. Si la création doit être absolument unique (par ex. un email), renvoyez un message de conflit clair qui explique ce qui existe déjà et ce qui a été fait.