Locking ottimista per strumenti admin: prevenire le sovrascritture silenziose
Impara l'optimistic locking per strumenti admin usando colonne version e controlli su updated_at, più pattern UI semplici per gestire i conflitti di modifica senza sovrascritture silenziose.

Il problema: sovrascritture silenziose quando molte persone modificano
Una “sovrascrittura silenziosa” accade quando due persone aprono lo stesso record, entrambi fanno modifiche e l'ultima persona che clicca Salva vince. Le modifiche della prima persona spariscono senza avvisi e spesso senza modo semplice di recuperarle.
In un pannello di amministrazione affollato, questo può succedere tutto il giorno senza che nessuno se ne accorga. Le persone tengono più schede aperte, saltano tra ticket e tornano a un form rimasto aperto per 20 minuti. Quando finalmente salvano, non stanno aggiornando l'ultima versione del record. Lo stanno sovrascrivendo.
Questo problema si vede più spesso negli strumenti back-office che nelle app pubbliche perché il lavoro è collaborativo e basato sui record. I team interni modificano gli stessi clienti, ordini, prodotti e richieste ripetutamente, spesso in rapidi interventi. Le app pubbliche tendono a essere “un utente modifica le proprie cose”, mentre gli strumenti admin sono “molti utenti modificano oggetti condivisi”.
Il danno raramente è drammatico nel momento, ma si accumula in fretta:
- Un prezzo di prodotto viene riportato a un valore vecchio subito dopo un aggiornamento promozionale.
- La nota interna di un agente di supporto scompare, così il prossimo agente ripete lo stesso troubleshooting.
- Lo stato di un ordine viene invertito (per esempio, "Shipped" torna a "Packed"), innescando follow-up sbagliati.
- Il numero di telefono o l'indirizzo di un cliente viene sostituito con informazioni non aggiornate.
Le sovrascritture silenziose sono dolorose perché tutti pensano che il sistema abbia salvato correttamente. Non c'è un chiaro momento di “qualcosa è andato storto”, solo confusione dopo quando i report non tornano o un collega chiede: “Chi ha cambiato questo?”
I conflitti così sono normali. Sono un segno che lo strumento è condiviso e utile, non che il tuo team stia sbagliando. L'obiettivo non è impedire a due persone di modificare. È rilevare quando il record è cambiato mentre qualcuno lo stava modificando e gestire quel momento in modo sicuro.
Se stai costruendo uno strumento interno su una piattaforma no-code come AppMaster, vale la pena pianificare questo fin da subito. Gli strumenti admin tendono a crescere rapidamente e, una volta che i team ci fanno affidamento, perdere dati “ogni tanto” diventa una fonte costante di sfiducia.
Optimistic locking in parole semplici
Quando due persone aprono lo stesso record e entrambe premono Salva, si crea concorrenza. Ognuno è partito da uno snapshot più vecchio, ma solo uno può essere “il più recente” quando i salvataggi avvengono.
Senza protezione, l'ultimo salvataggio vince. Così ottieni sovrascritture silenziose: il secondo salvataggio sostituisce quello del primo senza rumore.
L'optimistic locking è una regola semplice: “Salverò le mie modifiche solo se il record è nello stesso stato di quando ho iniziato a modificarlo.” Se il record è cambiato nel frattempo, il salvataggio viene rifiutato e l'utente vede un conflitto.
Questo è diverso dal locking pessimista, che è più simile a “Sto modificando, quindi nessun altro può farlo”. Il locking pessimista di solito comporta blocchi, timeout e utenti bloccati. Può essere utile in casi rari (per esempio, trasferimenti di denaro), ma spesso è frustrante negli strumenti admin dove molte piccole modifiche avvengono tutto il giorno.
L'optimistic locking è generalmente il default migliore perché mantiene il lavoro fluido. Le persone possono modificare in parallelo e il sistema interviene solo quando c'è una collisione reale.
Si adatta meglio quando:
- I conflitti sono possibili ma non costanti.
- Le modifiche sono rapide (pochi campi, form breve).
- Bloccare gli altri rallenterebbe il team.
- Puoi mostrare un messaggio chiaro “qualcuno ha aggiornato questo”.
- La tua API può verificare una versione (o un timestamp) ad ogni update.
Ciò che previene è il problema della “sovrascrittura silenziosa”. Invece di perdere dati, ottieni una fermata pulita: “Questo record è cambiato da quando lo hai aperto.”
Quello che non può fare è importante: non impedirà a due persone di prendere decisioni diverse (ma valide) basate sulle stesse informazioni vecchie, e non fonde automaticamente le modifiche per te. E se salti il controllo sul lato server, non hai risolto nulla.
Limiti comuni da tenere a mente:
- Non risolve i conflitti automaticamente (serve comunque una scelta dell'utente).
- Non aiuta se gli utenti lavorano offline e poi sincronizzano senza controlli.
- Non corregge permessi errati (qualcuno può ancora modificare ciò che non dovrebbe).
- Non cattura conflitti se il controllo avviene solo sul client.
Praticamente, l'optimistic locking è solo un valore in più inviato con la modifica, più una regola lato server “aggiorna solo se corrisponde”. Se stai costruendo un pannello admin in AppMaster, quel controllo di solito vive nella Business Logic proprio dove si eseguono gli update.
Due approcci comuni: colonna version vs updated_at
Per rilevare che un record è cambiato mentre qualcuno lo stava modificando, di solito si sceglie uno di due segnali: un numero di versione o un timestamp updated_at.
Approccio 1: colonna version (intero incrementale)
Aggiungi un campo version (di solito un intero). Quando carichi il form di modifica, carichi anche la version corrente. Al salvataggio invii lo stesso valore.
L'update ha successo solo se la versione memorizzata corrisponde ancora a quella con cui l'utente ha iniziato. Se corrisponde, aggiorni il record e incrementi version di 1. Se non corrisponde, restituisci un conflitto invece di sovrascrivere.
Questo è facile da capire: la versione 12 significa “questa è la dodicesima modifica”. Evita anche gli angoli legati al tempo.
Approccio 2: updated_at (confronto di timestamp)
Molte tabelle hanno già un campo updated_at. L'idea è la stessa: leggi updated_at quando apri il form, poi includilo al salvataggio. Il server aggiorna solo se updated_at è invariato.
Può funzionare bene, ma i timestamp hanno insidie. I diversi database memorizzano precisioni diverse. Alcuni arrotondano ai secondi, il che può perdere modifiche rapide. Se più sistemi scrivono lo stesso database, deriva degli orologi e gestione dei fusi orari possono creare casi confusi.
Un confronto semplice:
- Colonna version: comportamento più chiaro, portabile tra DB, nessun problema di orologio.
updated_at: spesso “gratuito” perché esiste già, ma precisione e gestione degli orari possono tradire.
Per la maggior parte dei team, una colonna version è il segnale primario migliore. È esplicita, prevedibile e facile da citare nei log e nei ticket di supporto.
Se usi AppMaster, tipicamente significa aggiungere un campo intero version nel Data Designer e assicurarti che la logica di update lo verifichi prima di salvare. Puoi comunque mantenere updated_at per audit, ma lascia che sia il numero di versione a decidere se un edit è sicuro da applicare.
Cosa memorizzare e cosa inviare con ogni modifica
L'optimistic locking funziona solo se ogni modifica porta con sé un marcatore “ultima versione vista” del momento in cui l'utente ha aperto il form. Quel marcatore può essere un numero version o un timestamp updated_at. Senza di esso, il server non può sapere se il record è cambiato mentre l'utente scriveva.
Sul record conserva i campi di business, più un campo di concorrenza controllato dal server. Il set minimo è:
id(identificatore stabile)- campi aziendali (nome, status, prezzo, note, ecc.)
version(intero che incrementa a ogni update riuscito) oupdated_at(timestamp scritto dal server)
Quando la schermata di modifica si apre, il form deve memorizzare il valore “last-seen” di quel campo di concorrenza. L'utente non deve poterlo modificare, quindi mantienilo come campo nascosto o nello stato del form. Esempio: l'API ritorna version: 12 e il form conserva 12 fino al Salva.
Quando l'utente clicca Salva, invia due cose: le modifiche e il marcatore last-seen. La forma più semplice è includerlo nel body della richiesta di update, tipo id, campi modificati e expected_version (o expected_updated_at). Se stai costruendo l'interfaccia in AppMaster, tratta questo valore come qualsiasi altro bound value: caricalo con il record, non cambiarlo e rimandalo con l'update.
Sul server l'update deve essere condizionale. Aggiorni solo se il marcatore atteso corrisponde a quello attualmente nel DB. Non fare “merge” in silenzio.
Una risposta di conflitto dovrebbe essere chiara e facile da gestire nell'UI. Una risposta pratica include:
- HTTP status
409 Conflict - un messaggio breve tipo “This record was updated by someone else.”
- il valore corrente del server (
current_versionocurrent_updated_at) - opzionalmente, il record corrente (così l'UI può mostrare cosa è cambiato)
Esempio: Sam apre un record Customer a version 12. Priya salva una modifica, portandolo a version 13. Sam poi preme Salva con expected_version: 12. Il server ritorna 409 con il record corrente a version 13. Ora l'UI può chiedere a Sam di rivedere i valori più recenti invece di sovrascrivere l'edit di Priya.
Passo dopo passo: implementare l'optimistic locking end-to-end
L'optimistic locking si riduce a una regola: ogni modifica deve dimostrare che si basa sull'ultima versione salvata del record.
1) Aggiungi un campo di concorrenza
Scegli un campo che cambi a ogni scrittura.
Un version intero dedicato è più semplice da capire. Parti da 1 e incrementalo a ogni update. Se hai già un updated_at affidabile che cambia sempre, puoi usarlo, ma assicurati che si aggiorni a ogni write (inclusi i job background).
2) Invia quel valore al client alla lettura
Quando l'UI apre una schermata di modifica, includi la version (o updated_at) corrente nella risposta. Salvala nello stato del form come valore nascosto.
Pensalo come una ricevuta che dice: “Sto modificando ciò che ho letto per ultimo.”
3) Richiedi il valore al momento dell'update
Al salvataggio il client rimanda i campi modificati più il valore di concorrenza last-seen.
Sul server rendi l'update condizionale. In termini SQL:
UPDATE tickets
SET status = $1,
version = version + 1
WHERE id = $2
AND version = $3;
Se l'update interessa 1 riga, il salvataggio è riuscito. Se interessa 0 righe, qualcuno ha già cambiato il record.
4) Ritorna il nuovo valore dopo il successo
Dopo un salvataggio riuscito, ritorna il record aggiornato con la nuova version (o il nuovo updated_at). Il client dovrebbe sostituire lo stato del form con quanto ritorna il server. Questo evita “doppi salvataggi” usando una versione vecchia.
5) Tratta i conflitti come un esito normale
Quando l'update condizionale fallisce, restituisci una risposta di conflitto chiara (spesso HTTP 409) che includa:
- il record corrente così com'è ora
- le modifiche tentate dal client (o abbastanza informazioni per ricostruirle)
- quali campi differiscono (se puoi calcolarlo)
In AppMaster, questo si mappa bene a un campo modello PostgreSQL nel Data Designer, a un endpoint di lettura che ritorna la version e a un Business Process che esegue l'update condizionale e si dirama in successo o gestione conflitto.
Pattern UI che gestiscono i conflitti senza infastidire gli utenti
L'optimistic locking è solo metà del lavoro. L'altra metà è ciò che l'utente vede quando il suo salvataggio viene rifiutato perché qualcuno ha cambiato il record.
Una buona UI per i conflitti ha due obiettivi: fermare le sovrascritture silenziose e aiutare l'utente a completare rapidamente il suo compito. Se fatto bene, sembra una guida utile, non un ostacolo.
Pattern 1: Dialogo bloccante semplice (più veloce)
Usalo quando le modifiche sono piccole e gli utenti possono riapplicare facilmente le loro modifiche dopo il reload.
Mantieni il messaggio breve e specifico: “This record changed while you were editing. Reload to see the latest version.” Poi dai due punti chiari:
- Reload and continue (primario)
- Copy my changes (opzionale ma utile)
“Copy my changes” può mettere i valori non salvati negli appunti o mantenerli nel form dopo il reload, così le persone non devono ricordare cosa hanno scritto.
Questo funziona bene per aggiornamenti a campo singolo, toggle, cambi di stato o note brevi. È anche il più semplice da implementare in molti builder, incluso AppMaster.
Pattern 2: “Review changes” (migliore per record ad alto valore)
Usalo quando il record è importante (prezzi, permessi, pagamenti) o il form è lungo. Invece di un errore a capolinea, porta l'utente a una schermata di conflitto che confronta:
- “Your edits” (quello che ha provato a salvare)
- “Current values” (l'ultimo dal DB)
- “What changed since you opened it” (i campi in conflitto)
Mantieni il confronto focalizzato. Non mostrare ogni campo se solo tre sono in conflitto.
Per ogni campo in conflitto, offri scelte semplici:
- Keep mine
- Take theirs
- Merge (solo quando ha senso, per esempio tag o note)
Dopo che l'utente ha risolto i conflitti, salva di nuovo con il valore di versione più recente. Se supporti testo ricco o note lunghe, mostra un piccolo diff (aggiunto/rimosso) così gli utenti decidono rapidamente.
Quando permettere un sovrascrivi forzato (e chi può farlo)
A volte serve un overwrite forzato, ma dovrebbe essere raro e controllato. Se lo aggiungi, fallo deliberato: richiedi una breve motivazione, registra chi l'ha fatto e limita l'opzione a ruoli come admin o supervisor.
Per gli utenti normali, default a “Review changes”. Il forzare il salvataggio è più difendibile quando l'utente è il proprietario del record, il record è a basso rischio o il sistema sta correggendo dati errati sotto supervisione.
Scenario d'esempio: due colleghi modificano lo stesso record
Due agenti di supporto, Maya e Jordan, lavorano nello stesso strumento admin. Aprono entrambi il profilo di un cliente per aggiornare lo stato e aggiungere note dopo chiamate diverse.
Timeline (con optimistic locking abilitato usando version o updated_at):
- 10:02 - Maya apre il Customer #4821. Il form carica Status = "Needs follow-up", Notes = "Called yesterday" e Version = 7.
- 10:03 - Jordan apre lo stesso cliente. Vede gli stessi dati, sempre Version = 7.
- 10:05 - Maya cambia Status in "Resolved" e aggiunge la nota: "Issue fixed, confirmed by customer." Clicca Salva.
- 10:05 - Il server aggiorna il record, incrementa Version a 8 (o aggiorna
updated_at) e registra un audit: chi ha cambiato cosa e quando. - 10:09 - Jordan scrive una nota diversa: "Customer asked for a receipt" e clicca Salva.
Senza controllo di concorrenza, il salvataggio di Jordan potrebbe sovrascrivere silenziosamente lo stato e la nota di Maya, a seconda di come è costruito l'update. Con l'optimistic locking, il server rifiuta l'update di Jordan perché lui stava cercando di salvare con Version = 7 mentre il record è già a Version = 8.
Jordan vede un messaggio di conflitto chiaro. L'UI mostra cosa è successo e gli dà un passo successivo sicuro:
- Reload the latest record (scarta le mie modifiche)
- Apply my changes on top of the latest record (raccomandato quando possibile)
- Review differences (mostra "Mine" vs "Latest") e scegli cosa mantenere
Una schermata semplice può mostrare:
- “This customer was updated by Maya at 10:05”
- I campi che sono cambiati (Status e Notes)
- Un'anteprima della nota non salvata di Jordan, così può copiarla o riapplicarla
Jordan sceglie “Review differences”, mantiene lo Status di Maya = "Resolved" e aggiunge la sua nota a quelle esistenti. Salva di nuovo, questa volta usando Version = 8, e l'update riesce (ora Version = 9).
Stato finale: nessuna perdita di dati, nessun dubbio su chi ha sovrascritto chi e una traccia di audit pulita che mostra la modifica di Maya e entrambe le note come edit separati e tracciabili. In uno strumento costruito con AppMaster, questo si mappa a un singolo controllo nell'update più un piccolo dialogo di risoluzione conflitti nell'UI.
Errori comuni che ancora causano perdita di dati
La maggior parte dei bug di “optimistic locking” non riguarda l'idea, ma il passaggio tra UI, API e DB. Se uno strato dimentica le regole, puoi comunque avere sovrascritture silenziose.
Un errore classico è leggere la versione quando si apre il form, ma non rimandarla al salvataggio. Succede spesso quando un form viene riutilizzato tra pagine e il campo nascosto viene perso, o quando un client API invia solo i campi “cambiati”.
Un'altra trappola comune è fare i controlli di conflitto solo nel browser. L'utente può vedere un avviso, ma se il server accetta comunque l'update, un altro client (o un retry) può sovrascrivere i dati. Il server deve essere il guardiano finale.
Pattern che causano più perdita di dati:
- Token di concorrenza mancante nella richiesta di salvataggio (
version,updated_ato ETag), così il server non ha nulla da confrontare. - Accettare aggiornamenti senza una condizione atomica, per esempio aggiornando solo per
idinvece che per “id + version”. - Usare
updated_atcon bassa precisione (per esempio secondi). Due edit nello stesso secondo possono sembrare identici. - Sostituire campi grandi (note, descrizioni) o interi array (tag, line items) senza mostrare cosa è cambiato.
- Trattare qualsiasi conflitto come “semplice retry”, che può riapplicare valori obsoleti sopra dati più recenti.
Un esempio concreto: due lead di supporto aprono lo stesso record cliente. Uno aggiunge una nota lunga, l'altro cambia lo stato e salva. Se il tuo salvataggio sovrascrive l'intero payload, il cambio di stato può cancellare accidentalmente la nota.
Quando succede un conflitto, i team perdono ancora dati se la risposta API è troppo scarna. Non limitarti a restituire “409 Conflict.” Ritorna abbastanza informazioni per consentire a un umano di risolvere:
- La versione server corrente (o
updated_at) - I valori più recenti del server per i campi interessati
- Una lista chiara dei campi che differiscono (anche solo i nomi)
- Chi lo ha cambiato e quando (se lo tracci)
Se lo implementi in AppMaster, applica la stessa disciplina: mantieni la version nello stato UI, inviala con l'update ed esegui il controllo nella logica backend prima di scrivere su PostgreSQL.
Controlli rapidi prima del rilascio
Prima di mettere in produzione, fai un rapido controllo sui failure mode che creano “ha salvato ma ha sovrascritto qualcun altro”.
Controlli su dati e API
Assicurati che il record porti un token di concorrenza end-to-end. Quel token può essere version o updated_at, ma deve essere trattato come parte del record, non come metadato opzionale.
- Le letture includono il token (e l'UI lo archivia nello stato del form, non solo a schermo).
- Ogni update rimanda il token last-seen e il server lo verifica prima di scrivere.
- Al successo, il server restituisce il nuovo token così l'UI resta in sync.
- Edits bulk e inline seguono la stessa regola, senza scorciatoie.
- I job background che modificano le stesse righe controllano anch'essi il token (altrimenti creeranno conflitti casuali).
Se costruisci in AppMaster, ricontrolla che il campo nel Data Designer esista (version o updated_at) e che il Business Process di update lo confronti prima di eseguire la scrittura.
Controlli UI
Un conflitto è “sicuro” solo se il passo successivo è ovvio.
Quando il server rifiuta un update, mostra un messaggio chiaro: “This record changed since you opened it.” Poi offri una prima azione sicura: ricaricare i dati più recenti. Se possibile, aggiungi un percorso “reload and reapply” che mantiene gli input non salvati dell'utente e li riapplica sul record aggiornato, così una piccola modifica non diventa una riscrittura.
Se il tuo team ne ha davvero bisogno, aggiungi un'opzione di “force save” controllata. Limitala per ruolo, richiedi conferma e registra chi ha forzato e cosa è cambiato. Così le emergenze restano possibili senza rendere la perdita di dati la regola.
Prossimi passi: aggiungi il locking a un workflow e poi estendi
Inizia in piccolo. Scegli una schermata admin dove le persone spesso si pestano i piedi e aggiungi l'optimistic locking lì prima di estendere. Le aree ad alto collisione sono di solito ticket, ordini, prezzi e inventario. Se rendi i conflitti sicuri su una schermata trafficata, vedrai rapidamente il pattern da ripetere altrove.
Scegli il comportamento di conflitto di default prima, perché influenza sia la logica backend sia l'UI:
- Block-and-reload: blocca il salvataggio, ricarica l'ultimo record e chiedi all'utente di riapplicare la modifica.
- Review-and-merge: mostra “le tue modifiche” vs “le modifiche più recenti” e lascia decidere all'utente cosa mantenere.
Block-and-reload è più veloce da costruire e funziona bene quando le modifiche sono brevi (cambi di stato, assegnazioni, note piccole). Review-and-merge vale la pena quando i record sono lunghi o ad alto rischio (tabelle prezzi, modifiche multi-campo agli ordini).
Poi implementa e testa un flusso completo prima di estendere:
- Scegli una schermata e lista i campi che gli utenti modificano più spesso.
- Aggiungi una version (o
updated_at) al payload del form e rendila obbligatoria al salvataggio. - Rendi l'update condizionale nella scrittura al database (aggiorna solo se la version corrisponde).
- Progetta il messaggio di conflitto e l'azione successiva (reload, copy my text, compare view).
- Testa con due browser: salva nella tab A, poi prova a salvare dati obsoleti nella tab B.
Aggiungi logging leggero per i conflitti. Anche un semplice evento “conflict happened” con tipo record, nome schermata e ruolo utente ti aiuta a individuare gli hotspot.
Se costruisci strumenti admin con AppMaster (appmaster.io), i pezzi principali si incastrano bene: modella un campo version nel Data Designer, applica update condizionali nei Business Processes e aggiungi un piccolo dialogo di conflitto nel UI builder. Una volta che il primo workflow è stabile, ripeti lo stesso pattern schermata per schermata e mantieni l'UI di risoluzione conflitti coerente così gli utenti la imparano una volta e si fidano ovunque.
FAQ
Un "silent overwrite" avviene quando due persone modificano lo stesso record da schede o sessioni diverse e l'ultimo salvataggio sostituisce le modifiche precedenti senza alcun avviso. Il problema è che entrambi gli utenti vedono un "salvataggio effettuato", quindi le modifiche mancanti vengono notate solo più tardi.
L'optimistic locking significa che l'app salva le tue modifiche solo se il record non è cambiato da quando lo hai aperto. Se qualcun altro ha già salvato, il tuo salvataggio viene rifiutato con un conflitto in modo che tu possa rivedere i dati più recenti invece di sovrascriverli.
Il locking pessimista blocca gli altri dall'editare mentre lavori, il che spesso genera attese, timeout e il classico “chi ha bloccato questo?”. L'optimistic locking si adatta meglio alle interfacce admin perché permette il lavoro parallelo e gestisce i conflitti solo quando effettivamente avvengono.
Una colonna di version è solitamente l'opzione più semplice e prevedibile perché evita problemi di precisione e fusi orari dei timestamp. Un controllo su updated_at può funzionare, ma rischia di perdere modifiche rapide se il timestamp è memorizzato con bassa precisione o gestito in modo incoerente tra sistemi.
Serve un token di concorrenza controllato dal server sul record, tipicamente version (intero) o updated_at (timestamp). Il client deve leggerlo quando apre il form, tenerlo invariato mentre l'utente modifica e rimandarlo al salvataggio come valore “expected”.
Perché il client non è una fonte affidabile per proteggere dati condivisi. Il server deve applicare un aggiornamento condizionale tipo “update where id = X and version = Y”, altrimenti un altro client, un retry o un job in background possono comunque sovrascrivere i dati.
Un buon default è un messaggio bloccante che dice che il record è cambiato e offre un'azione sicura: ricaricare la versione più recente. Se l'utente ha digitato molto, conserva il suo input non salvato così può riapplicarlo dopo il reload invece di riscrivere tutto.
Restituire una risposta di conflitto chiara (spesso 409) insieme a contesto sufficiente per recuperare: la versione corrente del server e i valori più recenti. Se possibile, includi chi ha aggiornato e quando, così l'utente capisce perché il suo salvataggio è stato rifiutato.
Attenzione a: token mancanti al salvataggio, aggiornamenti che filtrano solo per id invece che per id + version, e controlli su timestamp con bassa precisione. Un altro problema comune è sostituire campi grandi (note, array) invece di aggiornare solo i campi intenzionati, aumentando il rischio di cancellare il lavoro di qualcun altro.
In AppMaster, aggiungi un campo version nel Data Designer e includilo nel record che l'UI legge nello stato del form. Poi applica un aggiornamento condizionale nel Business Process così che la scrittura abbia successo solo quando la versione attesa corrisponde, e gestisci il ramo di conflitto nell'interfaccia con un flusso di reload/review.


