6 min

Prevenire record duplicati nelle app CRUD: difese pratiche

Prevenire record duplicati nelle app CRUD richiede più livelli: vincoli unici nel database, chiavi di idempotenza e stati dell'interfaccia che impediscono invii doppi.

Prevenire record duplicati nelle app CRUD: difese pratiche

Cosa sono i record duplicati e perché succedono

Un record duplicato è quando la tua app memorizza la stessa cosa due volte. Può essere due ordini per lo stesso checkout, due ticket di supporto con gli stessi dettagli o due account creati dallo stesso flusso di registrazione. In un'app CRUD i duplicati spesso sembrano righe normali prese singolarmente, ma sono sbagliati quando guardi i dati nel loro insieme.

La maggior parte dei duplicati nasce da comportamenti normali. Qualcuno clicca Create due volte perché la pagina sembra lenta. Su mobile, un doppio tap è facile da non notare. Anche utenti attenti ritenteranno se il pulsante sembra ancora attivo e non c'è un segnale chiaro che qualcosa stia succedendo.

Poi c'è il lato più complesso: reti e server. Una richiesta può andare in timeout e venire ritentata automaticamente. Una libreria client potrebbe ripetere una POST se pensa che il primo tentativo sia fallito. La prima richiesta potrebbe riuscire, ma la risposta va persa, così l'utente riprova e crea una seconda copia.

Non puoi risolvere tutto con un solo livello perché ogni livello vede solo una parte della storia. L'interfaccia può ridurre i doppi invii accidentali, ma non può fermare i retry dovuti a connessioni inaffidabili. Il server può rilevare ripetizioni, ma ha bisogno di un modo affidabile per riconoscere “questa è la stessa creazione di prima”. Il database può applicare regole, ma solo se definisci cosa significa “la stessa cosa”.

L'obiettivo è semplice: rendere le creazioni sicure anche quando la stessa richiesta arriva due volte. Il secondo tentativo dovrebbe diventare un no-op, una risposta pulita di “già creato”, o un conflitto gestito, non una seconda riga.

Dove si infilano i duplicati (client, rete, server)

Molti team trattano i duplicati come un problema del database. In pratica, però, i duplicati nascono spesso prima, quando la stessa azione di creazione viene attivata più volte.

Lato client: l'utente invia due volte

Un utente clicca Create e sembra non succedere nulla, quindi clicca di nuovo. Oppure preme Invio e poi clicca il pulsante subito dopo. Su mobile puoi avere due tap ravvicinati, eventi touch e click sovrapposti, o un gesto che viene registrato due volte.

Rete e trasporto: la richiesta viene rinviata

Anche se l'utente invia una sola volta, la rete può comunque ripetere la richiesta. Un timeout può scatenare un retry. Un'app offline può mettere in coda un “Save” e rinviarlo quando torna online. Alcune librerie HTTP ritentano automaticamente su certi errori, e tu non lo noterai finché non vedrai righe duplicate.

Lato server: retry, webhook e worker in background

I server ripetono il lavoro intenzionalmente. Le job queue ritentano i job falliti. I provider di webhook spesso consegnano lo stesso evento più di una volta, specialmente se il tuo endpoint è lento o restituisce uno status non 2xx. Se la tua logica di creazione è innescata da questi eventi, dai per scontato che i duplicati accadranno.

Concorrenza: due richieste in race

La concorrenza crea i duplicati più subdoli. Due tab inviano lo stesso form a pochi millisecondi di distanza. Se il server fa “esiste già?” e poi inserisce, entrambe le richieste possono superare il controllo prima che avvenga l'inserimento.

Tratta client, rete e server come fonti separate di ripetizioni. Avrai bisogno di difese in tutti e tre.

Parti da vincoli unici nel database (ultima linea di difesa)

Se vuoi un posto affidabile per fermare i duplicati, metti la regola nel database. Le correzioni UI e i controlli server aiutano, ma possono fallire sotto retry, lag o due utenti che agiscono contemporaneamente. Un vincolo unico nel database è l'autorità finale.

Inizia scegliendo una regola di unicità concreta che corrisponda a come le persone pensano al record. Esempi comuni:

  • Indirizzo email per un utente
  • Numero d'ordine per un acquisto
  • Un external_id da un provider di pagamenti

Fai attenzione a campi che sembrano unici ma non lo sono, come un nome completo.

Una volta scelta la regola, applicala con un vincolo unico (o indice unico). Questo fa sì che il database rifiuti un secondo insert che violerebbe la regola, anche se due richieste arrivano nello stesso momento.

Quando il vincolo scatta, decidi quale esperienza mostrare all'utente. Se creare un duplicato è sempre sbagliato, bloccalo con un messaggio chiaro (“Quell'email è già in uso”). Se i retry sono comuni e il record esiste già, spesso è meglio trattare il retry come un successo e restituire il record esistente (“Il tuo ordine è già stato creato”).

Usa pattern di scrittura sicuri quando corrispondono alle regole

Se la tua operazione di create è davvero “crea o riutilizza”, un upsert può essere il pattern più pulito. Esempio: “crea cliente per email” può inserire una nuova riga o restituire quella esistente. Usalo solo quando corrisponde al significato di business. Se possono arrivare payload leggermente diversi per la stessa chiave, decidi quali campi possono aggiornarsi e quali devono rimanere invariati.

I vincoli unici non sostituiscono le chiavi di idempotenza o buoni stati UI, ma ti danno uno stop duro su cui tutto il resto può appoggiarsi.

Chiavi di idempotenza: rendere sicure le creazioni ripetute

Una chiave di idempotenza è un token unico che rappresenta una singola intenzione dell'utente, per esempio “crea questo ordine una volta”. Se la stessa richiesta viene inviata di nuovo (doppio click, retry di rete, ripresa mobile), il server la tratta come retry, non come nuova creazione.

Questo è uno degli strumenti più pratici per rendere sicuri gli endpoint di create quando il client non riesce a capire se il primo tentativo è riuscito.

Gli endpoint che ne traggono più beneficio sono quelli dove un duplicato è costoso o confonde, come ordini, fatture, pagamenti, inviti, abbonamenti e form che scatenano email o webhook.

Al retry, il server dovrebbe restituire il risultato originale del primo tentativo riuscito, incluso lo stesso ID creato e lo stesso codice di stato. Per farlo, conserva un piccolo record di idempotenza indicizzato da (utente o account) + endpoint + chiave di idempotenza. Salva sia l'esito (ID record, corpo della risposta) sia uno stato “in corso” in modo che due richieste quasi simultanee non creino due righe.

Conserva i record di idempotenza abbastanza a lungo da coprire i retry reali. Una baseline comune è 24 ore. Per i pagamenti, molti team tengono 48–72 ore. Una TTL mantiene lo storage limitato e corrisponde a quanto è probabile un retry.

Se generi API con un builder guidato come Koder.ai, è comunque utile rendere l'idempotenza esplicita: accetta una chiave inviata dal client (header o campo) e applica la regola “stessa chiave, stesso risultato” sul server.

Passo dopo passo: aggiungere idempotenza a un endpoint di create

Create invoices without duplicates
Costruisci un flusso tipo fatturazione dove i retry restituiscono sempre lo stesso ID di record.

L'idempotenza rende sicura una richiesta di create da ripetere. Se il client ritenta per un timeout (o l'utente clicca due volte), il server restituisce lo stesso risultato invece di creare una seconda riga.

Una ricetta pratica in 5 passi

  1. Crea una chiave di idempotenza per ogni azione dell'utente (per esempio quando preme “Create invoice”). Generala sul client e mantienila stabile per quella singola azione.
  2. Invia la chiave con la richiesta ogni volta che tenti la creazione. Un header funziona bene (come Idempotency-Key), ma mandarla nel body JSON può andare bene ugualmente.
  3. Sul server, memorizza la chiave con un ambito chiaro: chi sta chiamando (utente o account), a quale endpoint si applica e quale creazione rappresenta. Salva anche il payload della risposta finale.
  4. Se arriva di nuovo la stessa chiave per lo stesso ambito, non creare nulla. Restituisci la risposta salvata (incluso l'ID del record originale).
  5. Testalo sotto stress: clic rapidi, retry client e timeout di rete simulati. Conferma che ottieni esattamente una nuova riga.

Come appare la logica server

Il dettaglio chiave è che "controlla + conserva" deve essere sicuro sotto concorrenza. In pratica, memorizzi il record di idempotenza con un vincolo unico su (scope, key) e tratti i conflitti come un segnale per riutilizzare il risultato.

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

Esempio: un cliente preme “Create invoice”, l'app invia la key abc123 e il server crea la fattura inv_1007. Se il telefono perde il segnale e ritenta, il server risponde con lo stesso inv_1007, non con inv_1008.

Quando testi, non fermarti al “double click”. Simula una richiesta che va in timeout sul client ma completa comunque sul server, poi ritenta con la stessa chiave.

Accorgimenti UI che fermano gli invii doppi accidentali

Le difese server-side contano, ma molti duplicati ancora cominciano con una persona che fa la cosa normale due volte. Una buona UI rende ovvia la strada sicura.

Disabilita il pulsante di invio non appena l'utente invia. Fallo al primo click, non dopo la validazione o dopo che la richiesta è partita. Se il form può essere inviato tramite più controlli (un pulsante e Invio), blocca tutto lo stato del form, non solo un pulsante.

Mostra uno stato di progresso chiaro che risponde a una domanda: sta funzionando? Un semplice “Salvataggio...” o uno spinner bastano. Mantieni il layout stabile in modo che il pulsante non salti e non invogli un secondo click.

Un piccolo insieme di regole previene la maggior parte dei doppi invii: imposta un flag isSubmitting all'inizio dell'handler di submit, ignora nuovi submit mentre è true (per click e Invio) e non resettarlo finché non hai una risposta reale.

Le risposte lente sono dove molte app falliscono. Se riattivi il pulsante su un timer fisso (per esempio dopo 2 secondi), gli utenti possono inviare di nuovo mentre la prima richiesta è ancora in volo. Riattiva solo quando il tentativo è completo.

Dopo il successo, rendi improbabile un nuovo invio. Naviga lontano (alla pagina del nuovo record o alla lista) o mostra uno stato di successo con il record creato visibile. Evita di lasciare lo stesso form compilato con il pulsante abilitato.

Casi limite da pianificare (schede, refresh, mobile)

I bug duplicati più ostinati nascono da comportamenti "strani ma comuni": due schede, un refresh o un telefono che perde il segnale.

Prima di tutto, definisci correttamente l'unicità. “Unico” raramente significa “unico nell'intero database”. Potrebbe significare uno per utente, uno per workspace o uno per tenant. Se sincronizzi con un sistema esterno, potresti aver bisogno di unicità per sorgente esterna più il suo ID esterno. Un approccio sicuro è scrivere la frase esatta che intendi (per esempio, “Un numero fattura per tenant per anno”) e poi applicarla.

Il comportamento multi-tab è una trappola classica. Gli stati di caricamento UI aiutano in una singola scheda, ma non fanno nulla tra schede diverse. Qui le difese server-side devono comunque reggere.

Il tasto Indietro e il refresh possono scatenare invii accidentali. Dopo una creazione riuscita, gli utenti spesso aggiornano per “controllare” o premono Indietro e reinviano un form che sembra ancora modificabile. Preferisci una vista del record creato invece del form originale e fai in modo che il server gestisca replay sicuri.

Il mobile aggiunge interruzioni: backgrounding, reti instabili e retry automatici. Una richiesta potrebbe riuscire, ma l'app non ricevere la risposta, quindi ritenta alla ripresa.

Errori comuni e trappole

Build retry-safe APIs fast
Crea endpoint Go + PostgreSQL che gestiscono i retry senza generare righe extra.

Il fallimento più comune è considerare la UI come l'unica rete di sicurezza. Un pulsante disabilitato e uno spinner aiutano, ma non coprono refresh, reti mobili instabili, utenti che aprono una seconda scheda o bug client. Server e database devono ancora poter dire “questa creazione è già avvenuta”.

Un'altra trappola è scegliere il campo sbagliato per l'unicità. Se imposti un vincolo unico su qualcosa che non è veramente unico (un cognome, un timestamp arrotondato, un titolo libero), bloccherai record validi. Usa un identificatore reale (come un ID esterno del provider) o una regola con ambito (unico per utente, per giorno o per record padre).

Le chiavi di idempotenza sono facili da implementare male. Se il client genera una chiave nuova a ogni retry, ottieni una nuova creazione ogni volta. Mantieni la stessa chiave per tutto l'intento utente, dal primo click fino ai retry.

Fai attenzione anche a cosa restituisci sui retry. Se la prima richiesta ha creato il record, un retry dovrebbe restituire lo stesso risultato (o almeno lo stesso ID record), non un errore vago che fa ritentare l'utente.

Se un vincolo unico blocca un duplicato, non nasconderlo dietro “Qualcosa è andato storto.” Dì cosa è successo in chiaro: “Questo numero fattura esiste già. Abbiamo mantenuto l'originale e non abbiamo creato una seconda fattura.”

Checklist rapida prima del rilascio

Prima del deploy, fai un rapido controllo specifico per i percorsi di creazione. I risultati migliori arrivano impilando difese in modo che un click mancato, un retry o una rete lenta non possano creare due righe.

Conferma tre cose:

  • Le regole del database corrispondono alla realtà (i vincoli unici esistono in produzione e sono correttamente a scopo).
  • Gli endpoint di create importanti sono sicuri da ripetere (le chiavi di idempotenza restituiscono lo stesso esito).
  • L'UI rende lo stato ovvio (submit disabilitato, progresso chiaro e una vista post-successo evidente).

Un controllo pratico: apri il form, clicca submit due volte rapidamente, poi aggiorna a metà submit e riprova. Se riesci a creare due record, anche utenti reali lo faranno.

Esempio: creare una fattura senza duplicati

Keep control of your backend
Ottieni il codice sorgente e verifica come sono implementati la logica di creazione e i vincoli.

Immagina una piccola app di fatturazione. Un utente compila una nuova fattura e tocca Create. La rete è lenta, lo schermo non cambia subito e tocca Create di nuovo.

Solo con protezione UI, potresti disabilitare il pulsante e mostrare uno spinner. Questo aiuta, ma non basta. Un doppio tap può comunque scivolare su alcuni dispositivi, può avvenire un retry dopo un timeout, o l'utente può inviare da due schede.

Solo con un vincolo unico nel database puoi fermare i duplicati esatti, ma l'esperienza può risultare dura. La prima richiesta riesce, la seconda colpisce il vincolo e l'utente vede un errore anche se la fattura era stata creata.

Il risultato pulito è idempotenza più vincolo unico:

  • Il client genera una chiave di idempotenza per l'azione di create e la riusa sui retry.
  • Il server conserva la chiave con il risultato della creazione e restituisce lo stesso ID fattura per i retry.
  • Il vincolo unico continua a proteggerti se due chiavi diverse cercano di creare lo stesso numero fattura.

Un messaggio UI semplice dopo il secondo tap: “Fattura creata – abbiamo ignorato l'invio duplicato e mantenuto la tua prima richiesta.”

Prossimi passi: mantenerlo affidabile mentre l'app cresce

Una volta che hai le basi, i successi successivi riguardano visibilità, pulizia e coerenza.

Aggiungi logging leggero attorno ai percorsi di creazione così puoi distinguere tra un'azione utente reale e un retry. Registra la chiave di idempotenza, i campi unici coinvolti e l'esito (creato vs restituito esistente vs rifiutato). Non servono tool pesanti per cominciare.

Se i duplicati esistono già, ripuliscili con una regola chiara e una traccia di audit. Per esempio, conserva il record più vecchio come “vincitore”, riattacca le righe correlate (pagamenti, line items) e marca gli altri come uniti invece di cancellarli. Questo facilita supporto e reportistica.

Scrivi le tue regole di unicità e idempotenza in un unico posto: cosa è unico e in quale ambito, quanto tempo vivono le chiavi di idempotenza, come appaiono gli errori e cosa dovrebbe fare l'UI sui retry. Questo evita che nuovi endpoint aggirino silenziosamente le protezioni.

Se stai costruendo schermate CRUD velocemente in Koder.ai (koder.ai), vale la pena includere questi comportamenti nel template di default: vincoli unici nello schema, endpoint di create idempotenti nell'API e stati di caricamento chiari nella UI. Così la velocità non compromette la qualità dei dati.

Domande frequenti

What exactly counts as a duplicate record in a CRUD app?

Un record duplicato è quando la stessa entità del mondo reale viene memorizzata due volte, per esempio due ordini per lo stesso checkout o due ticket per lo stesso problema. Di solito accade perché la stessa azione di “create” viene eseguita più volte a causa di doppi invii da parte dell'utente, retry o richieste concorrenti.

Why do duplicates happen even when users only click once?

Perché una seconda creazione può essere attivata senza che l'utente se ne renda conto: un doppio tap su mobile, premere Invio e poi cliccare il pulsante, ecc. Anche quando l'utente invia una sola volta, il client, la rete o il server possono ritentare la richiesta dopo un timeout e provocare una seconda creazione. Non si può assumere che “POST sia una sola volta”.

Is disabling the submit button enough to prevent duplicates?

Non in modo affidabile. Disabilitare il pulsante e mostrare “Salvataggio…” riduce i doppi invii accidentali, ma non impedisce i retry dovuti a reti instabili, refresh, più schede, job in background o ridelivery di webhook. Servono anche difese a livello server e database.

What should I put a database unique constraint on?

Un vincolo unico è l'ultima linea di difesa che impedisce l'inserimento di due righe anche se arrivano due richieste simultanee. Funziona meglio quando definisci una regola di unicità realistica (spesso con ambito, per esempio per tenant o workspace) e la applichi direttamente nel database.

Do I need both unique constraints and idempotency keys?

Risolvono problemi diversi. I vincoli unici bloccano duplicati basati su una regola di campo (es. numero fattura), mentre le chiavi di idempotenza rendono un tentativo di creazione specifico sicuro da ripetere (stessa chiave = stesso risultato). Usarli insieme offre sicurezza e una migliore esperienza utente sui retry.

How should the client generate and reuse an idempotency key?

Genera una chiave per ogni intento dell'utente (una pressione di “Create”), riutilizzala per tutti i retry di quell'intento e inviala con la richiesta. La chiave deve rimanere stabile attraverso timeout e riprese dell'app, ma non deve essere riutilizzata per una creazione diversa in seguito.

What should the server do when it receives the same idempotency key again?

Conserva un record di idempotenza indicizzato per ambito (es. utente o account), endpoint e la chiave di idempotenza, e salva la risposta che hai restituito per la prima richiesta andata a buon fine. Se la stessa chiave arriva di nuovo, restituisci la risposta salvata con lo stesso ID creato invece di inserire una nuova riga.

How do I make idempotency safe under concurrency and race conditions?

Usa un approccio “check + store” sicuro per la concorrenza, tipicamente imponendo un vincolo unico sul record di idempotenza (per ambito e chiave). In questo modo due richieste quasi simultanee non possono entrambe comportarsi come “la prima” e una dovrà riutilizzare il risultato salvato.

How long should I keep idempotency records?

Conservale abbastanza a lungo da coprire retry realistici; un valore comune è 24 ore, più lungo per flussi come pagamenti dove i retry possono avvenire più tardi. Usa una TTL in modo che lo storage non cresca all'infinito e che la durata corrisponda al periodo in cui un client potrebbe ragionevolmente ritentare.

What’s the best user-facing behavior when a duplicate create is detected?

Tratta una creazione duplicata come un retry riuscito quando è chiaramente lo stesso intento, restituendo il record originale (stesso ID) invece di un errore vago. Se invece l'utente sta cercando di creare qualcosa che deve essere unico (per esempio un'email), mostra un messaggio di conflitto chiaro che spiega cosa esiste già e cosa è stato fatto.

Related posts