Verrou optimiste pour éviter les mises à jour perdues dans les applications web
Apprenez comment le verrou optimiste évite les mises à jour perdues lorsque deux onglets ou utilisateurs modifient le même enregistrement, en utilisant une colonne de version ou des ETag et une gestion claire des conflits.

Le problème des mises à jour perdues en termes simples
Une « mise à jour perdue » survient lorsque deux personnes (ou deux onglets de navigateur) modifient la même chose, et que la deuxième sauvegarde écrase silencieusement la première. Personne ne voit d'erreur. Les deux utilisateurs pensent que leur modification a été prise en compte. Mais seule la dernière sauvegarde finit dans la base de données.
Un exemple simple : vous ouvrez votre profil dans un onglet et changez votre nom d'affichage de « Sam » à « Samantha », mais vous ne cliquez pas encore sur Enregistrer. Dans un autre onglet, vous changez votre adresse e‑mail et cliquez sur Enregistrer. Ensuite, vous revenez au premier onglet et cliquez sur Enregistrer. Si l'application utilise le « dernier écrit gagne », cette soumission plus ancienne peut écraser le changement d'e‑mail plus récent, même si vous n'avez jamais modifié le champ d'e‑mail dans le premier onglet.
Cela passe souvent inaperçu parce que tout semble normal. Le serveur renvoie « 200 OK », l'interface affiche une notification de succès et la page se rafraîchit. Le bug n'apparaît que plus tard, quand quelqu'un constate qu'un réglage a été remis en arrière, qu'une adresse a changé ou qu'une modification d'admin a disparu. À ce stade, cela paraît aléatoire et les clients peuvent accuser un logiciel « instable ».
Vous le verrez surtout dans les écrans CRUD du quotidien où des gens laissent une page ouverte un moment : profils et paramètres de compte, panneaux d'administration (utilisateurs, produits, permissions), pages de configuration d'équipe (rôles, infos de facturation), éditeurs de contenu (titres, métadonnées, descriptions), et tout formulaire d'édition qui charge les données une fois puis enregistre plus tard.
Le « dernier écrit gagne » est risqué parce qu'il considère chaque sauvegarde comme aussi fraîche que les autres, même si elle repose sur des données obsolètes. Cela crée aussi un problème de confiance : les utilisateurs ont fait ce qu'il fallait, mais le système a silencieusement jeté leur travail.
Le verrou optimiste est une façon courante d'empêcher cela. Quand vous enregistrez, le serveur vérifie si l'enregistrement a changé depuis que vous l'avez chargé. S'il a changé, l'application empêche l'écrasement et vous demande de résoudre le conflit au lieu de faire comme si de rien n'était.
Situations courantes qui causent des écrasements silencieux
Les écrasements silencieux arrivent quand votre application permet à quelqu'un d'éditer des données basées sur un instantané ancien, puis de les sauvegarder sans vérifier ce qui a changé entre-temps. Le résultat est le « dernier enregistrement gagne », même quand cette dernière sauvegarde est la mauvaise.
La cause la plus fréquente est d'avoir deux onglets (ou fenêtres) ouverts sur la même page. Vous mettez à jour un enregistrement dans l'onglet A, oubliez que l'onglet B est encore ouvert, puis l'onglet B sauvegarde plus tard et remet sans le savoir des valeurs anciennes. Cela apparaît souvent dans les panneaux d'administration, les tableaux de bord et les pages « Modifier le profil » que les gens laissent ouvertes pendant des heures.
Cela arrive aussi quand deux personnes différentes touchent le même enregistrement. Pensez à une note client, un statut de commande ou une adresse. La personne 1 change le numéro de téléphone, la personne 2 change les instructions de livraison, et celui qui clique sur Enregistrer en dernier peut effacer l'autre modification si l'application envoie l'enregistrement complet au lieu d'envoyer seulement les champs modifiés.
Les réseaux lents ou peu fiables aggravent le problème. Une sauvegarde faite dans un train ou sur une connexion mobile instable peut arriver en retard. Si votre serveur l'accepte comme étant actuelle, elle peut écraser des modifications plus récentes qui ont été sauvegardées pendant que la première requête était encore en cours. Le mode hors ligne présente le même risque : vous modifiez localement, revenez en ligne et poussez des changements qui sont maintenant obsolètes.
Les retries peuvent renvoyer une ancienne mise à jour aussi : un utilisateur double‑clique sur Enregistrer, un job en arrière‑plan réessaie après un timeout avec des données obsolètes, une file rejoue un message après un crash, ou une bibliothèque cliente réessaie automatiquement une requête déjà appliquée.
Ce sont précisément les cas où le verrou optimiste est rentable. Ajoutez une simple vérification de version (ou une vérification ETag), et le serveur peut détecter « vous avez modifié une copie plus ancienne » et refuser la sauvegarde au lieu d'écraser silencieusement.
Qu'est‑ce que le verrou optimiste et comment ça marche
Le verrou optimiste empêche le problème des mises à jour perdues. Au lieu de bloquer les gens pendant l'édition, il laisse tout le monde éditer librement et ne vérifie les conflits que lorsqu'on tente de sauvegarder.
C'est plus facile à comprendre en le comparant au verrou pessimiste. Le verrou pessimiste revient à mettre un panneau « ne pas toucher » sur un enregistrement pendant que vous l'éditez. Il empêche les conflits, mais crée aussi des attentes, des timeouts et des problèmes quand « quelqu'un a laissé la page ouverte ». Le verrou optimiste suppose que les conflits sont rares, évite de bloquer et n'empêche la sauvegarde que lorsque conflict il y a.
Une « version » est une petite donnée qui change à chaque modification d'une ligne ou d'un document. Dans une ligne de base de données, c'est souvent une colonne entière comme version qui commence à 1 et s'incrémente à chaque mise à jour. Dans les API, cela peut aussi être un ETag, qui est une empreinte de l'état actuel.
Le flux de base est :
- Quand vous chargez un enregistrement, vous lisez aussi sa version actuelle.
- Quand vous enregistrez, vous renvoyez la version que vous aviez vue.
- La mise à jour réussit seulement si la version stockée correspond encore.
- Si elle correspond, l'enregistrement est mis à jour et la version est incrémentée.
- Si elle ne correspond pas, la sauvegarde est rejetée comme conflit.
Cette différence est tout l'enjeu. Le système vous dit : « Quelqu'un (ou un autre onglet) a modifié ceci après que vous l'ayez chargé. » L'application peut alors afficher un message clair et proposer une étape sûre suivante, comme recharger, comparer les différences ou copier vos modifications avant de réessayer. L'écrasement ne se produit plus silencieusement.
Cela convient à la plupart des applications CRUD parce que la plupart des utilisateurs n'éditent pas exactement le même enregistrement en même temps. Vous gardez l'interface réactive (pas de verrou tenu pendant que quelqu'un réfléchit) et vous protégez quand même les données.
Un petit exemple mental : vous ouvrez un formulaire de profil dans deux onglets. L'onglet A enregistre en premier, faisant passer la version de 3 à 4. L'onglet B essaie d'enregistrer avec la version 3. La base de données (ou l'API) refuse, et l'onglet B doit actualiser ou fusionner. Cette petite vérification de version transforme une perte de données cachée en un conflit visible et réparable.
Pas à pas : approche avec une colonne de version
Une colonne de version est la façon la plus simple d'empêcher les écrasements silencieux dans une application CRUD. Stockez un nombre sur chaque ligne, envoyez‑le au client quand il lit l'enregistrement, et exigez que le client le renvoie lors de la sauvegarde. Si le nombre a changé depuis le chargement de la page, rejetez la mise à jour.
1) Ajouter un champ version à la table
Ajoutez une colonne entière, souvent appelée version, qui démarre à 1. (Utiliser updated_at peut fonctionner comme solution de secours, mais les timestamps peuvent devenir délicats avec les fuseaux horaires et les modifications très rapides.)
ALTER TABLE documents ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
2) Faire circuler la version via votre API
Faites voyager la version avec l'enregistrement de bout en bout. À la lecture, incluez version dans la réponse API pour que l'interface puisse la stocker. Lors de l'édition, conservez cette version dans l'état du formulaire (même si elle est cachée). À la mise à jour, exigez que le client renvoie la dernière version vue (le corps de la requête est le plus simple).
Désormais, le serveur peut dire si le client enregistre une copie ancienne.
3) Mettre à jour seulement si la version correspond encore
C'est le cœur du verrou optimiste : mettez à jour la ligne seulement lorsque id et version correspondent, puis incrémentez la version.
Un pattern courant est une requête atomique :
UPDATE documents
SET title = ?, body = ?, version = version + 1
WHERE id = ? AND version = ?;
Si la requête met à jour 0 lignes, la version ne correspondait pas. Renvoyez un conflit (souvent HTTP 409) et incluez l'enregistrement le plus récent (et sa nouvelle version) pour que l'UI puisse afficher ce qui a changé.
4) Incrémenter en cas de succès, rejeter en cas d'écart
Quand la sauvegarde réussit, la réponse doit inclure la nouvelle version pour que le client soit prêt pour la prochaine édition. Lorsqu'elle échoue, ne relancez pas automatiquement. Les relances aveugles peuvent transformer un signal clair « vous avez édité une copie ancienne » en confusion, et elles peuvent encore mener à des écrasements.
Pas à pas : utiliser les ETag avec l'en‑tête If-Match
Un ETag est une courte empreinte qui représente l'état actuel d'une ressource. Si la ressource change, l'empreinte change aussi. C'est donc un bon choix pour le verrou optimiste quand vous ne voulez pas ajouter une colonne de version, ou quand vous voulez que le serveur décide de ce que signifie « même état ».
1) Retourner un ETag à la lecture (GET)
Quand un client charge un enregistrement pour l'éditer, votre API doit renvoyer l'enregistrement plus un ETag qui correspond à cet état exact. Vous pouvez calculer l'ETag à partir d'une version de ligne, d'un updated_at, ou d'un hash du JSON renvoyé.
Un flux simple :
- Le client envoie
GET /items/123 - Le serveur répond avec le corps JSON et un en‑tête
ETag: "abc123" - Le client stocke cet ETag à côté des données du formulaire qu'il édite
2) Exiger If-Match à l'écriture (PUT/PATCH)
Quand l'utilisateur sauvegarde, le client inclut l'ETag qu'il avait vu initialement. Cela dit au serveur : « N'applique ma mise à jour que si la ressource est toujours dans l'état que j'ai édité. »
- Le client envoie
PATCH /items/123avec l'en‑têteIf-Match: "abc123" - Le serveur compare
If-Matchavec l'ETag courant de l'item 123 - S'ils correspondent, appliquez le changement et retournez la ressource mise à jour avec un nouvel ETag
S'ils ne correspondent pas, vous avez un conflit. N'acceptez pas l'écriture silencieusement.
3) Renvoyer la bonne réponse en cas de conflit
La plupart des API renvoient soit 412 Precondition Failed quand If-Match ne correspond pas (le plus précis), soit 409 Conflict si vous préférez une réponse plus générale.
Incluez assez de détails pour que le client puisse récupérer : un court code d'erreur/message, et souvent la dernière version de la ressource (plus son nouvel ETag) pour que l'UI puisse montrer ce qui a changé.
Quand les ETag conviennent mieux qu'une colonne de version
Les ETag sont pratiques quand vous ne pouvez pas facilement changer le schéma de la base, quand plusieurs backends peuvent mettre à jour la même ressource, ou quand vous utilisez déjà des mécanismes de cache. Ils fonctionnent aussi bien quand l'« état de la ressource » n'est pas une seule ligne, comme un document assemblé depuis plusieurs tables.
Exemple : deux onglets modifient le même profil. L'onglet A charge la page et reçoit l'ETag "v1". L'onglet B sauvegarde un changement d'abord, ce qui fait passer l'ETag du profil à "v2". Quand l'onglet A essaie de sauvegarder avec If-Match: "v1", le serveur renvoie 412. L'UI peut alors demander à l'utilisateur de recharger, ou afficher un petit écran de fusion au lieu d'écraser l'autre onglet.
Comment gérer les conflits sans frustrer les utilisateurs
Un conflit n'est pas une erreur aux yeux de l'utilisateur. C'est une surprise. Votre travail est d'expliquer ce qui s'est passé et d'aider la personne à récupérer son travail.
Utilisez un message simple qui nomme le problème et son impact : « Cet enregistrement a été modifié ailleurs pendant que vous l'éditiez. Vos modifications n'ont pas encore été sauvegardées. » Évitez un texte vague comme « 409 Conflict » ou « Mise à jour échouée ». Les gens doivent savoir que ce n'était pas de leur faute.
Donnez des choix simples (et rendez l'option sûre la plus facile)
La plupart des applications ont besoin des mêmes trois options. Gardez‑les simples et faites de l'option la plus sûre l'action primaire.
- Recharger la version la plus récente : récupérer la dernière version et l'afficher.
- Conserver mes modifications : garder les saisies non sauvegardées dans le formulaire pour les réappliquer.
- Écraser quand même : ne l'autoriser que si l'utilisateur confirme clairement qu'il veut remplacer la modification d'un autre.
Faites de « Recharger la version la plus récente » l'action principale. « Écraser quand même » doit être secondaire et explicite, avec une confirmation indiquant ce qui sera écrasé.
Conserver les saisies non sauvegardées de l'utilisateur
La façon la plus rapide de perdre la confiance est d'effacer un formulaire après un conflit. Conservez ce que l'utilisateur a tapé, même si vous rechargez l'enregistrement.
Un pattern pratique : stocker les modifications en cours séparément (état local ou brouillon), recharger les données serveur les plus récentes, puis remplir à nouveau le formulaire en réutilisant les valeurs du brouillon là où elles restent pertinentes. Si possible, mettez en évidence les champs qui diffèrent de la version serveur pour que l'utilisateur repère vite ce qui a changé.
Quand un simple rechargement suffit vs quand il faut une UI de fusion
Un rechargement simple suffit quand le formulaire est court, que les changements sont généralement petits et que le coût de retaper est faible.
Vous aurez probablement besoin d'une UI de fusion quand les utilisateurs éditent de longs textes (descriptions, notes, politiques), quand de nombreux champs peuvent changer en même temps (tableaux de prix, configurations multi‑étapes), ou quand les conflits arrivent souvent (équipes actives, écrans d'admin partagés).
Un flux réaliste : deux personnes modifient le même enregistrement client. L'une met à jour le numéro de téléphone et sauvegarde. L'autre change l'adresse et clique sur sauvegarder un peu plus tard. Avec une bonne gestion des conflits, la seconde personne voit : « Le numéro de téléphone a été changé par quelqu'un d'autre. Votre modification d'adresse est toujours là. » Elle peut recharger et sauvegarder sans tout retaper.
Erreurs courantes et pièges à éviter
Le verrou optimiste est simple en théorie, mais quelques erreurs peuvent annuler tout l'intérêt. La plupart apparaissent seulement après que de vrais utilisateurs travaillent dans plusieurs onglets, ou quand un nouveau chemin de code est ajouté lors d'une release précipitée.
Pièges qui causent des écrasements silencieux
- Utiliser
updated_atcomme « version » quand les timestamps ne sont pas assez précis. Si deux mises à jour arrivent dans la même seconde (ou si votre base arrondit), les deux peuvent sembler valides et l'une peut écraser l'autre. - Ajouter une vérification de version/ETag sur un endpoint mais l'oublier sur un autre. Par exemple, l'écran d'édition principal utilise la vérification, mais un basculement rapide, un autosave, un panneau d'administration ou un job en arrière‑plan met à jour le même enregistrement sans la vérifier.
- Changer la version à la lecture, ou lors d'écritures qui n'altèrent pas des champs gérés par l'utilisateur. Si vous incrémentez la version quand quelqu'un se contente de voir la page, vous créez des conflits qui paraissent aléatoires et injustes.
- Attraper le conflit et relancer automatiquement sans intervention utilisateur. Les réessais aveugles peuvent transformer un signal clair « vous avez édité une copie ancienne » en une boucle confuse.
- Faire des mises à jour en masse qui ignorent la vérification de concurrence. Une seule instruction SQL ou un outil de batch qui met à jour de nombreuses lignes peut passer outre la condition de version et effacer des modifications récentes.
Petites habitudes qui évitent de gros bugs
Soyez cohérent : si un enregistrement est éditable, chaque chemin d'écriture doit soit (1) exiger la version/ETag, soit (2) être conçu explicitement comme un override forcé et logué comme tel.
Gardez la logique de versionnement stable. La version ne doit avancer que lorsque vous acceptez une mise à jour basée sur la dernière version connue. Si votre application a des écritures d'effets secondaires (recalcul de compteurs, synchronisation de métadonnées, mise à jour de last_seen), envisagez de les déplacer dans une table séparée pour qu'elles ne créent pas de conflits d'édition inutiles.
Un contrôle rapide : ouvrez le même formulaire d'édition dans deux onglets, sauvegardez dans l'onglet A puis dans l'onglet B. Si l'onglet B réussit sans conflit clair, vous avez encore une voie de mise à jour perdue quelque part.
Vérifications rapides avant livraison
Traitez le verrou optimiste comme une fonctionnalité que l'on peut casser volontairement. Si deux sauvegardes s'affrontent, vous devriez obtenir un conflit clair au lieu d'un écrasement silencieux.
Commencez par le test le plus réaliste : ouvrez le même enregistrement dans deux onglets du navigateur. Changez des champs différents dans chaque onglet, puis sauvegardez l'onglet A puis l'onglet B. L'onglet B ne doit pas « gagner » silencieusement. Il doit recevoir une réponse de conflit (souvent HTTP 409) et un message indiquant ce qui s'est passé.
Les réseaux lents sont où ces bugs se cachent. Utilisez le throttling réseau du navigateur (ou ajoutez un délai artificiel sur le serveur) pour qu'une sauvegarde prenne quelques secondes. Pendant qu'elle est en vol, sauvegardez depuis un autre onglet. Quand la requête retardée revient, elle doit échouer en sécurité.
Une checklist pré‑livraison rapide :
- Deux onglets : éditez le même enregistrement, sauvegardez dans les deux onglets, confirmez que la seconde sauvegarde reçoit un conflit.
- Sauvegarde lente : retardez une requête, faites une autre sauvegarde, confirmez que la requête retardée est rejetée.
- Reprise mobile : éditez, mettez l'app en arrière‑plan, revenez ensuite et sauvegardez, confirmez que la version/ETag est toujours vérifiée.
- Récupération hors ligne : perdez la connexion en cours d'édition, reconnectez, puis sauvegardez, confirmez que vous gérez les conflits au lieu d'écraser.
- Sécurité des brouillons : après un conflit, confirmez que l'UI conserve le texte non sauvegardé de l'utilisateur.
Vérifiez aussi les détails qui comptent pour les utilisateurs. Quand un conflit arrive, la réponse doit être suffisamment précise pour que votre client réagisse : un statut clair, un message lisible par un humain, et idéalement la dernière version serveur pour que l'UI puisse montrer « vos modifications » vs « version actuelle ».
Un exemple réaliste : deux personnes modifiant les mêmes données
Deux coéquipiers, Maya et Jordan, mettent à jour la même règle de tarification dans un panneau d'administration. La règle dit : « 10% de réduction quand le total du panier est supérieur à 100$ ». Maya veut le passer à 120$. Jordan veut augmenter la remise à 15%.
Ils ouvrent tous les deux la page d'édition à 10:00. Chaque onglet charge la règle courante. À ce moment, l'enregistrement a version = 7 (ou une valeur équivalente).
Ce qui se passe sans verrou
Maya enregistre en premier à 10:02. Le serveur écrit « threshold = 120 » dans la base.
Jordan enregistre à 10:03. Son navigateur a encore les anciennes valeurs du formulaire, donc sa mise à jour écrit « discount = 15% » et envoie aussi l'ancienne valeur threshold depuis le chargement initial. Le résultat est un écrasement silencieux : le changement de seuil de Maya a disparu, et personne n'a reçu d'avertissement. L'interface affiche souvent « Enregistré » les deux fois, donc l'équipe fait confiance à des données erronées.
Ce qui se passe avec une colonne de version
Avec le verrou optimiste, les deux mises à jour incluent la version depuis laquelle elles ont commencé.
- La requête de Maya dit : « update this rule where id=123 and version=7 »
- Le serveur met à jour la ligne et l'incrémente à version 8
- La requête de Jordan dit aussi : « where version=7 »
- La base de données ne trouve pas de correspondance (parce que l'enregistrement est maintenant en version 8)
- Le serveur renvoie une réponse de conflit au lieu d'écraser
Ce que voit l'utilisateur : Jordan reçoit un message clair comme « Cette règle de tarification a été modifiée par quelqu'un d'autre. Vérifiez la dernière version avant d'enregistrer. » La page recharge les données les plus récentes (threshold 120, version 8). Les modifications non sauvegardées de Jordan peuvent être conservées localement pour qu'il puisse réappliquer « 15% » et sauvegarder à nouveau.
Ce qui est préservé : l'enregistrement le plus récent reste intact, et la modification voulue par Jordan n'est pas perdue — elle est simplement retardée jusqu'à ce qu'il la confirme par rapport à la version la plus récente.
Étapes suivantes : déploiement progressif et aide possible
Commencez par choisir l'approche qui s'adapte à la façon dont votre appli fonctionne déjà. Une colonne de version est souvent la plus simple quand vous contrôlez la base de données et l'ORM et que la plupart des mises à jour passent par votre serveur. Les ETag avec If-Match conviennent bien quand vous avez une API REST propre, plusieurs clients ou des besoins de cache importants.
Déployez de façon progressive. Choisissez un flux d'édition à forte valeur (profils, commandes, paramètres) et ajoutez le verrou optimiste de bout en bout : lecture, édition, mise à jour et message de conflit clair. Une fois ce chemin stabilisé, répétez pour la ressource suivante.
Une checklist de déploiement sécurisée :
- Ajouter la version (ou l'ETag) à chaque réponse de lecture et à chaque requête de mise à jour.
- Renvoyer une réponse de conflit claire quand les versions ne correspondent pas (pas d'écrasements silencieux).
- Afficher une UI simple : recharger, conserver vos modifications ou réessayer après revue.
- Logger les conflits avec le type de ressource et la fréquence pour repérer les points chauds.
- Ajouter un ou deux tests qui simulent deux onglets mettant à jour le même enregistrement.
Ne vous arrêtez pas à l'API. Si l'UI se contente d'afficher « Échec de la sauvegarde », les gens vont réessayer et peuvent encore écraser les modifications d'autrui. Donnez du contexte : ce qui a changé et ce qu'ils peuvent faire ensuite. Pour les formulaires simples, un bon comportement par défaut est de recharger les données les plus récentes et de conserver le brouillon de l'utilisateur pour qu'il puisse réappliquer ses modifications.
Si vous avez hérité d'une application CRUD générée par IA, il vaut la peine de vérifier que chaque chemin d'update suit la même règle de concurrence. Des équipes comme FixMyMess (fixmymess.ai) se spécialisent à transformer des prototypes générés par IA en logiciels prêts pour la production, et un audit rapide trouve souvent des vérifications de version ou d'ETag manquantes avant qu'elles ne causent de véritables pertes de données.