Numerazione fatture sicura in concorrenza per evitare duplicati e buchi
Scopri pattern pratici per una numerazione fatture sicura in concorrenza, così più utenti possono creare fatture o ticket senza duplicati o buchi inattesi.

Cosa succede quando due persone creano record contemporaneamente
Immagina un ufficio affollato alle 16:55. Due persone completano una fattura e premono Salva nello stesso secondo. Entrambi gli schermi mostrano brevemente “Fattura #1042”. Un record vince, l'altro fallisce, o peggio, entrambi vengono salvati con lo stesso numero. Questo è il sintomo più comune nella pratica: numeri duplicati che emergono solo sotto carico.
I ticket si comportano allo stesso modo. Due operatori creano un nuovo ticket per lo stesso cliente nello stesso momento, e il tuo sistema prova a “prendere il prossimo numero” guardando l'ultimo record. Se entrambe le richieste leggono lo stesso valore “ultimo” prima che uno dei due scriva, possono scegliere lo stesso numero successivo.
Il secondo sintomo è più sottile: numeri saltati. Potresti vedere #1042, poi #1044, con #1043 mancante. Questo succede spesso dopo un errore o un retry. Una richiesta riserva un numero, poi il salvataggio fallisce per un errore di validazione, un timeout o un utente che chiude la scheda. Oppure un job in background fa retry dopo un problema di rete e prende un nuovo numero anche se il primo tentativo aveva già consumato uno.
Per le fatture questo conta perché la numerazione fa parte della tua audit trail. I contabili si aspettano che ogni fattura sia identificata in modo univoco, e i clienti possono fare riferimento ai numeri di fattura per pagamenti o email di supporto. Per i ticket, il numero è l'identificativo usato nelle conversazioni, nei report e nelle esportazioni. I duplicati creano confusione. I numeri mancanti possono sollevare dubbi durante le revisioni, anche se non è successo nulla di fraudolento.
Ecco l'aspettativa chiave da fissare subito: non tutti i metodi di numerazione possono essere contemporaneamente sicuri rispetto alla concorrenza e senza buchi. La numerazione sicura in concorrenza (nessun duplicato, anche con molti utenti) è raggiungibile e dovrebbe essere non negoziabile. La numerazione senza buchi è possibile, ma richiede regole aggiuntive e spesso cambia il modo in cui gestisci draft, fallimenti e cancellazioni.
Un buon modo per inquadrare il problema è chiedersi cosa devono garantire i numeri:
- Non devono mai ripetersi (sempre unici)
- Dovrebbero essere per lo più crescenti (caratteristica gradita)
- Non devono mai saltare (solo se lo progetti esplicitamente)
Una volta scelta la regola, la soluzione tecnica diventa molto più facile da scegliere.
Perché avvengono duplicati e buchi
La maggior parte delle app segue un pattern semplice: l'utente clicca Salva, l'app chiede il prossimo numero di fattura o ticket, poi inserisce il nuovo record con quel numero. Sembra sicuro perché funziona perfettamente quando c'è un solo utente.
Il problema nasce quando due salvataggi avvengono quasi simultaneamente. Entrambe le richieste possono raggiungere il passo “ottieni prossimo numero” prima che una completi l'inserimento. Se entrambe le letture vedono lo stesso valore “next”, provano entrambe a scrivere lo stesso numero. Questa è una race condition: il risultato dipende dal timing, non dalla logica.
Una timeline tipica è questa:
- Richiesta A legge prossimo numero: 1042
- Richiesta B legge prossimo numero: 1042
- Richiesta A inserisce fattura 1042
- Richiesta B inserisce fattura 1042 (o fallisce se una regola di unicità lo blocca)
I duplicati avvengono quando nulla nel database impedisce il secondo insert. Se controlli solo “questo numero è già preso?” nel codice dell'app, puoi comunque perdere la race tra il controllo e l'inserimento.
I buchi sono un problema diverso. Succedono quando il sistema “riserva” un numero, ma il record non diventa mai una fattura o un ticket reale e committato. Cause comuni sono pagamenti falliti, errori di validazione trovati tardi, timeout o un utente che chiude la scheda dopo che il numero è stato assegnato. Anche se l'insert fallisce e nulla viene salvato, il numero può essere già consumato.
La concorrenza nascosta peggiora la situazione perché raramente è solo “due umani che cliccano Salva.” Potresti anche avere:
- client API che creano record in parallelo
- importazioni che girano a batch
- job in background che generano fatture durante la notte
- retry da app mobili con connessioni instabili
Quindi le cause radice sono: (1) conflitti di timing quando più richieste leggono lo stesso valore del contatore, e (2) numeri allocati prima di essere sicuri che la transazione andrà a buon fine. Qualsiasi piano per una numerazione sicura in concorrenza deve decidere quale risultato tollerare: nessun duplicato, nessun buco, o entrambi, e in quali eventi (draft, retry, cancellazioni).
Decidi la regola di numerazione prima di scegliere la soluzione
Prima di progettare la numerazione sicura in concorrenza, scrivi cosa deve significare il numero nel tuo business. L'errore più comune è scegliere prima un metodo tecnico, poi scoprire che le regole contabili o legali si aspettano qualcosa di diverso.
Inizia separando due obiettivi che spesso si confondono:
- Unico: nessuna fattura o ticket condivide mai lo stesso numero.
- Senza buchi: i numeri sono unici e anche strettamente consecutivi (nessun numero mancante).
Molti sistemi reali puntano al solo unico e accettano i buchi. I buchi possono succedere per ragioni normali: un utente apre un draft e lo abbandona, un pagamento fallisce dopo che il numero è stato riservato, o un record viene creato e poi annullato. Per i ticket i buchi di solito non contano. Anche per le fatture, molte squadre accettano buchi se possono giustificarli con una audit trail (annullata, cancellata, test, ecc.). La numerazione senza buchi è possibile, ma impone regole aggiuntive e spesso aumenta l'attrito.
Poi decidi l'ambito del contatore. Piccole differenze di formulazione cambiano molto il design:
- Una sequenza globale per tutto, o sequenze separate per azienda/tenant?
- Reset ogni anno (2026-000123) o mai reset?
- Serie diverse per fatture vs note di credito vs ticket?
- Serve un formato user-friendly (prefissi, separatori), o basta un numero interno?
Un esempio concreto: un SaaS con più aziende clienti potrebbe richiedere numeri di fattura unici per azienda e che resetteranno per anno civile, mentre i ticket sono unici globalmente e non si resettano mai. Sono due contatori diversi con regole diverse, anche se in UI sembrano simili.
Se davvero hai bisogno di senza buchi, sii esplicito su quali eventi sono permessi dopo che un numero è stato assegnato. Per esempio, una fattura può essere cancellata oppure solo annullata? Gli utenti possono salvare draft senza numero e assegnare il numero solo alla finalizzazione? Queste scelte spesso contano più della tecnica di database.
Scrivi la regola in una breve specifica prima di costruire:
- Quali tipi di record usano la sequenza?
- Quando un numero è “utilizzato” (draft, inviato, pagato)?
- Qual è lo scope (globale, per azienda, per anno, per serie)?
- Come gestisci annullamenti e correzioni?
In AppMaster, questo tipo di regola appartiene vicino al modello dati e al processo di business, così il team implementa lo stesso comportamento ovunque (API, UI web e mobile) senza sorprese.
Approcci comuni e cosa garantisce ciascuno
Quando si parla di “numerazione fatture”, spesso si mischiano due obiettivi diversi: (1) non generare mai lo stesso numero due volte, e (2) non avere mai buchi. La maggior parte dei sistemi può facilmente garantire il primo. Il secondo è molto più difficile, perché i buchi possono apparire ogni volta che una transazione fallisce, un draft viene abbandonato o un record viene annullato.
Approccio 1: Sequence del database (unicità veloce)
Una sequence PostgreSQL è il modo più semplice per ottenere numeri unici e crescenti sotto carico. Scala bene perché il database è progettato per distribuire valori di sequence rapidamente, anche con molti utenti che creano record contemporaneamente.
Cosa ottieni: unicità e ordine (per lo più crescente). Cosa non ottieni: assenza di buchi. Se un insert fallisce dopo che un numero è stato assegnato, quel numero viene “bruciato” e vedrai un buco.
Approccio 2: Vincolo UNIQUE più retry (lascia decidere al database)
Qui generi un numero candidato (dalla logica dell'app), lo salvi e fai affidamento su un vincolo UNIQUE per rifiutare i duplicati. Se c'è un conflitto, fai retry con un nuovo numero.
Può funzionare, ma sotto alta concorrenza tende a diventare rumoroso. Puoi ritrovarti con più retry, più transazioni fallite e picchi difficili da debuggare. Non garantisce nemmeno l'assenza di buchi a meno di combinarlo con regole di prenotazione severe, che aggiungono complessità.
Approccio 3: Riga contatore con locking (puntare al senza buchi)
Se hai davvero bisogno di numeri senza buchi, il pattern usuale è una tabella contatore dedicata (una riga per scope di numerazione, ad esempio per anno o per azienda). Blocchi quella riga in una transazione, la incrementi e usi il nuovo valore.
Questo è il più vicino al senza buchi nel design standard del database, ma ha un costo: crea un “hot spot” singolo su cui tutti gli scrittori devono attendere. Aumentano anche i rischi operativi (transazioni lunghe, timeout, deadlock).
Approccio 4: Servizio di prenotazione separato (solo per casi speciali)
Un servizio standalone di numerazione può centralizzare le regole tra più app o database. Vale la pena solo quando hai diversi sistemi che emettono numeri e non puoi consolidare le scritture.
Lo scambio è rischio operativo: hai aggiunto un servizio che deve essere corretto, altamente disponibile e consistente.
Ecco un modo pratico per pensare alle garanzie:
- Sequence: unico, veloce, accetta buchi
- Unique + retry: unico, semplice a basso carico, può generare thrashing ad alto carico
- Riga contatore bloccata: può essere senza buchi, più lenta sotto alta concorrenza
- Servizio separato: flessibile tra sistemi, massima complessità e modalità di failure
Se costruisci questo in uno strumento no-code come AppMaster, valgono le stesse scelte: il database è dove risiede la correttezza. La logica dell'app può aiutare con retry e messaggi chiari, ma la garanzia finale dovrebbe venire da vincoli e transazioni.
Passo dopo passo: prevenire duplicati con sequence e vincoli unici
Se il tuo obiettivo principale è evitare duplicati (non garantire l'assenza di buchi), il pattern più semplice e affidabile è: lascia che il database generi un ID interno e applica l'unicità sul numero rivolto al cliente.
Inizia separando i due concetti. Usa un valore generato dal database (identity/sequence) come chiave primaria per join, modifiche ed esportazioni. Tieni invoice_no o ticket_no come colonna separata mostrata agli utenti.
Una configurazione pratica in PostgreSQL
Ecco un approccio comune in PostgreSQL che mantiene la logica del “prossimo numero” dentro il database, dove la concorrenza è gestita correttamente.
-- Internal, never-shown primary key
create table invoices (
id bigint generated always as identity primary key,
invoice_no text not null,
created_at timestamptz not null default now()
);
-- Business-facing uniqueness guarantee
create unique index invoices_invoice_no_uniq on invoices (invoice_no);
-- Sequence for the visible number
create sequence invoice_no_seq;
Ora genera il numero visualizzato al momento dell'insert (non facendo "select max(invoice_no) + 1"). Uno schema semplice è formattare il valore di sequence dentro l'INSERT:
insert into invoices (invoice_no)
values (
'INV-' || lpad(nextval('invoice_no_seq')::text, 8, '0')
)
returning id, invoice_no;
Anche se 50 utenti cliccano “Create invoice” nello stesso momento, ogni insert ottiene un valore di sequence diverso e l'indice unico blocca eventuali duplicati accidentali.
Cosa fare in caso di collisione
Con una sequence semplice le collisioni sono rare. Di solito succedono quando aggiungi regole extra come “reset per anno”, “per tenant” o numeri modificabili dall'utente. Per questo il vincolo UNIQUE è ancora importante.
A livello applicativo, gestisci un errore di violazione di unicità con un piccolo loop di retry:
- Prova l'insert
- Se ottieni un errore di vincolo unico su invoice_no, riprova
- Fermati dopo un numero limitato di tentativi e mostra un errore chiaro
Funziona bene perché i retry si attivano solo quando succede qualcosa di insolito, come due percorsi di codice che producono lo stesso formato di numero.
Mantieni la finestra di race piccola
Non calcolare il numero nell'UI, e non “prenotare” numeri leggendo prima e inserendo dopo. Generalo il più vicino possibile alla scrittura sul database.
Se usi AppMaster con PostgreSQL, puoi modellare l'id come identity primary key nel Data Designer, aggiungere un vincolo unico per invoice_no e generare invoice_no durante il flusso di create in modo che avvenga assieme all'insert. Così il database resta la fonte di verità e i problemi di concorrenza restano dove PostgreSQL è più forte.
Passo dopo passo: costruire un contatore senza buchi con locking di riga
Se hai davvero bisogno di numeri senza buchi (nessun numero di fattura o ticket mancante), puoi usare una tabella contatori transazionale e locking di riga. L'idea è semplice: solo una transazione alla volta può prendere il prossimo numero per uno scope, quindi i numeri vengono consegnati in ordine.
Per prima cosa, decidi lo scope. Molte squadre hanno bisogno di sequenze separate per azienda, per anno o per serie (come INV vs CRN). La tabella contatore memorizza l'ultimo numero usato per ciascuno scope.
Ecco un pattern pratico per una numerazione sicura in concorrenza usando i lock di riga in PostgreSQL:
- Crea una tabella
number_counterscon colonne comecompany_id,year,series,last_numbere una chiave unica su(company_id, year, series). - Avvia una transazione di database.
- Blocca la riga del contatore per il tuo scope usando
SELECT last_number FROM number_counters WHERE ... FOR UPDATE. - Calcola
next_number = last_number + 1, aggiorna la riga del contatore alast_number = next_number. - Inserisci la riga della fattura o del ticket usando
next_number, poi fai commit.
La chiave è FOR UPDATE. Sotto carico non ottieni duplicati. Non ottieni nemmeno buchi dovuti a “due utenti hanno ricevuto lo stesso numero”, perché la seconda transazione non può leggere e incrementare la stessa riga del contatore finché la prima non committa (o rollback). Invece, la seconda richiesta aspetta brevemente. Quella attesa è il prezzo da pagare per essere senza buchi.
Inizializzare un nuovo scope
Serve anche un piano per la prima volta che appare uno scope (nuova azienda, nuovo anno, nuova serie). Due opzioni comuni:
- Pre-creare le righe di contatore in anticipo (per esempio, creare le righe del prossimo anno a dicembre).
- Creare on demand: prova a inserire la riga del contatore con
last_number = 0, e se esiste già, ripiega sul normale flusso di lock-and-increment.
Se costruisci questo in uno strumento no-code come AppMaster, tieni l'intera sequenza “lock, increment, insert” dentro una singola transazione nella logica di business, così avviene tutta insieme o non avviene nulla.
Casi limite: draft, salvataggi falliti, cancellazioni e modifiche
La maggior parte dei bug di numerazione emerge nelle parti disordinate: draft che non vengono mai pubblicati, salvataggi falliti, fatture annullate e record modificati dopo che qualcuno ha già visto il numero. Se vuoi numerazione sicura in concorrenza, ti serve una regola chiara su quando il numero diventa “reale”.
La decisione più importante è il timing. Se assegni un numero nel momento in cui qualcuno clicca “Nuova fattura”, otterrai buchi dovuti ai draft abbandonati. Se assegni solo quando una fattura è finalizzata (posted, issued, sent, o qualunque cosa significhi “finale” nel tuo business), puoi mantenere i numeri più stretti e facili da spiegare.
I salvataggi falliti e i rollback sono dove le aspettative spesso si scontrano con il comportamento del database. Con una sequence tipica, una volta che un numero è preso, è preso, anche se la transazione poi fallisce. Questo è normale e sicuro, ma può creare buchi. Se la tua policy richiede numeri senza buchi, il numero deve essere assegnato solo nell'ultimo step e solo se la transazione committa. Di solito questo significa bloccare una singola riga contatore, scrivere il numero finale e committare tutto come un'unità. Se qualcosa fallisce, nulla viene assegnato.
Cancellazioni e annullamenti non dovrebbero quasi mai “riusare” un numero. Mantieni il numero e cambia lo stato. Auditor e clienti si aspettano che la storia resti consistente, anche quando un documento viene corretto.
Le modifiche sono più semplici: una volta che un numero è visibile all'esterno, trattalo come permanente. Non rinumerare una fattura o un ticket dopo che è stato condiviso, esportato o stampato. Se serve una correzione, crea un nuovo documento che faccia riferimento al vecchio (per esempio una nota di credito o un ticket sostitutivo), ma non riscrivere la storia.
Una regola pratica che molte squadre adottano:
- I draft non hanno un numero finale (usa un ID interno o “DRAFT”).
- Assegna il numero solo su “Post/Issue”, dentro la stessa transazione del cambio di stato.
- Annullamenti e cancellazioni mantengono il numero, ma ricevono uno stato e una motivazione chiari.
- Numeri stampati/emailed non cambiano mai.
- Le importazioni preservano i numeri originali e aggiornano il contatore per partire dopo il massimo valore importato.
Migrazioni e importazioni meritano cura speciale. Se passi da un altro sistema, porta i numeri di fattura esistenti così come sono e imposta il tuo contatore per partire dopo il valore massimo importato. Decidi anche cosa fare con formati in conflitto (per esempio prefissi diversi per anno). Di solito è meglio memorizzare il “display number” esattamente com'era e mantenere una chiave primaria interna separata.
Esempio: un helpdesk crea ticket rapidamente, ma molti sono draft. Assegna il numero del ticket solo quando l'agente clicca “Invia al cliente”. Questo evita di sprecare numeri su draft abbandonati e mantiene la sequenza visibile allineata con le comunicazioni reali al cliente. In uno strumento no-code come AppMaster, vale la stessa idea: conserva i draft come record senza numero pubblico e genera il numero finale durante lo step di “submit” del Business Process che committa con successo.
Errori comuni che causano duplicati o buchi inaspettati
La maggior parte dei problemi di numerazione nasce da un'idea semplice: trattare un numero come un valore di visualizzazione invece che come uno stato condiviso. Quando più persone salvano insieme, il sistema ha bisogno di un posto unico e chiaro per decidere il prossimo numero, e di una regola chiara su cosa accade in caso di fallimento.
Un errore classico è usare SELECT MAX(number) + 1 nel codice dell'app. Sembra ok in test a utente singolo, ma due richieste possono leggere lo stesso MAX prima che una faccia commit. Entrambe generano lo stesso next value e ottieni un duplicato. Anche se aggiungi un “check then retry”, puoi creare carico extra e picchi strani sotto traffico intenso.
Un'altra fonte comune di duplicati è generare il numero sul client (browser o mobile) prima di salvare. Il client non sa cosa fanno gli altri utenti e non può riservare in modo sicuro un numero se il salvataggio fallisce. I numeri generati dal client vanno bene per etichette temporanee come “Draft 12”, ma non per ID ufficiali di fatture o ticket.
I buchi sorprendono i team che presumono che le sequence siano senza buchi. In PostgreSQL, le sequence sono pensate per unicità, non per continuità perfetta. I numeri possono essere saltati quando una transazione fa rollback, quando si prefetchano ID o quando il database si riavvia. Questo è comportamento normale. Se il tuo requisito reale è “nessun duplicato”, una sequence più vincolo UNIQUE è di solito la soluzione giusta. Se il requisito è davvero “numeri senza buchi”, ti serve un pattern diverso (di solito locking di riga) e devi accettare alcuni compromessi in throughput.
Il locking può anche ritorcersi contro quando è troppo ampio. Un singolo lock globale per tutta la numerazione forza ogni azione di creazione a mettersi in fila, anche se potresti partizionare i contatori per azienda, luogo o tipo di documento. Questo può rallentare il sistema e far sembrare agli utenti che il salvataggio sia “bloccato a caso”.
Ecco gli errori da verificare quando implementi la numerazione sicura in concorrenza:
- Usare
MAX + 1(o “trova ultimo numero”) senza un vincolo di database UNIQUE. - Generare numeri finali sul client e poi provare a “risolvere i conflitti dopo”.
- Aspettarsi che le sequence PostgreSQL siano senza buchi e poi trattare i buchi come errori.
- Bloccare un contatore condiviso per tutto invece di partizionare i contatori quando ha senso.
- Testare solo con un utente, così le race condition non emergono fino al lancio.
Suggerimento pratico di test: esegui un semplice test di concorrenza che crea da 100 a 1.000 record in parallelo e poi controlla duplicati e buchi inaspettati. Se costruisci in uno strumento no-code come AppMaster, vale la stessa regola: assicurati che il numero finale venga assegnato dentro una singola transazione server-side, non nel flusso UI.
Controlli rapidi prima del rilascio
Prima di distribuire la numerazione di fatture o ticket, fai un rapido controllo sulle parti che solitamente falliscono sotto traffico reale. L'obiettivo è semplice: ogni record ottiene esattamente un numero business e le tue regole restano valide anche quando 50 persone cliccano "Create" contemporaneamente.
Ecco una checklist pratica pre-ship per la numerazione sicura in concorrenza:
- Conferma che il campo del numero business abbia un vincolo UNIQUE nel database (non solo un controllo in UI). Questa è la tua ultima linea di difesa se due richieste collidono.
- Assicurati che il numero venga assegnato dentro la stessa transazione di database che salva il record. Se l'assegnazione del numero e il salvataggio sono separati in richieste diverse, vedrai duplicati prima o poi.
- Se richiedi numeri senza buchi, assegna il numero solo quando il record è finalizzato (per esempio quando una fattura è emessa, non quando è creata come draft). Draft, form abbandonati e pagamenti falliti sono la fonte più comune di buchi.
- Aggiungi una strategia di retry per conflitti rari. Anche con locking di riga o sequence puoi incontrare errori di serializzazione, deadlock o violazioni di unicità in casi limite. Un retry semplice con breve backoff spesso basta.
- Stress test con 20–100 creazioni simultanee su tutti i punti di ingresso: UI, API pubblica e importazioni bulk. Testa mix realistici come picchi, reti lente e double submit.
Un modo rapido per validare il setup è simulare un momento di helpdesk affollato: due agenti aprono il form “Nuovo ticket”, uno invia dalla web app mentre un job di import inserisce ticket dall'email nello stesso istante. Dopo l'esecuzione, controlla che tutti i numeri siano unici, nel formato corretto e che i fallimenti non lascino record mezzi salvati.
Se costruisci il workflow in AppMaster, valgono gli stessi principi: tieni l'assegnazione del numero nella transazione del database, affida i vincoli a PostgreSQL e testa sia le azioni UI sia gli endpoint API che creano la stessa entità. Qui molte squadre si sentono sicure nei test manuali, ma vengono sorprese il primo giorno di utilizzo reale.
Esempio: ticket di helpdesk affollato e cosa fare dopo
Immagina un support desk dove gli agenti creano ticket tutto il giorno nella web app, mentre un'integrazione crea ticket da chat e email. Tutti si aspettano numeri come T-2026-000123 e che ogni numero identifichi esattamente un ticket.
Un approccio ingenuo è: leggi “ultimo numero ticket”, aggiungi 1, salva il nuovo ticket. Sotto carico, due richieste possono leggere lo stesso “ultimo numero” prima che una salvi. Entrambe calcolano lo stesso next number e otteniamo duplicati. Se provi a “fixare” facendo retry dopo un fallimento, spesso crei buchi senza volerlo.
Il database può fermare i duplicati anche se il tuo codice è ingenuo. Aggiungi un vincolo UNIQUE sulla colonna ticket_number. Poi, quando due richieste tentano lo stesso numero, un insert fallisce e puoi ritentare pulitamente. Questo è il nucleo della numerazione sicura anche per le fatture: lascia che il database imponga l'unicità, non l'UI.
La numerazione senza buchi cambia il workflow. Se richiedi nessun buco, di solito non puoi assegnare il numero finale quando il ticket è creato (draft). Invece, crea il ticket con uno stato Draft e senza ticket_number. Assegna il numero solo quando il ticket è finalizzato, così salvataggi falliti e draft abbandonati non “bruciano” numeri.
Un semplice design di tabella potrebbe essere:
- tickets: id, created_at, status (Draft, Open, Closed), ticket_number (nullable), finalized_at
- ticket_counters: key (per esempio "tickets_2026"), next_number
In AppMaster puoi modellare questo nel Data Designer con tipi PostgreSQL e poi costruire la logica nel Business Process Editor:
- Create Ticket: inserisci ticket con status=Draft e senza ticket_number
- Finalize Ticket: avvia una transazione, blocca la riga contatore, assegna ticket_number, incrementa next_number, commit
- Test: esegui due azioni “Finalize” contemporanee e conferma che non si ottengono duplicati
Cosa fare dopo: parte da una regola chiara (solo unico vs veramente senza buchi). Se puoi accettare buchi, una sequence del database più vincolo UNIQUE è di solito sufficiente e mantiene il flusso semplice. Se devi essere senza buchi, sposta la numerazione allo step di finalizzazione e tratta il “draft” come uno stato di prima classe. Poi fai load-test con più agenti che cliccano contemporaneamente e con integrazioni API che generano burst, così vedi il comportamento prima degli utenti reali.


