Scopri come le garanzie ACID influenzano il design del database e il comportamento delle app. Esplora atomicità, consistenza, isolamento, durabilità, compromessi ed esempi pratici.

Quando paghi la spesa, prenoti un volo o trasferisci denaro tra conti, ti aspetti un risultato chiaro: o è riuscito, o non lo è. I database mirano a offrire la stessa certezza—anche quando molti utenti usano il sistema contemporaneamente, i server crashano o la rete balbetta.
Una transazione è un'unità singola di lavoro che il database tratta come un unico “pacchetto.” Può includere più passaggi—sottrarre inventario, creare un record d'ordine, addebitare una carta e scrivere una ricevuta—but è pensata per comportarsi come una singola azione coerente.
Se uno qualsiasi dei passaggi fallisce, il sistema dovrebbe riavvolgere a un punto sicuro invece di lasciare una situazione a metà.
Gli aggiornamenti parziali non sono solo guasti tecnici; diventano ticket di assistenza e rischi finanziari. Per esempio:
Questi errori sono difficili da debuggare perché tutto sembra “per lo più corretto,” ma i numeri non tornano.
ACID è un'abbreviazione per quattro garanzie che molti database possono fornire per le transazioni:
Non è un marchio di database o una singola opzione da attivare; è una promessa sul comportamento.
Garanzie più forti di solito significano che il database deve fare più lavoro: coordinazione extra, attese per lock, tracciamento delle versioni e scrittura su log. Questo può ridurre la throughput o aumentare la latenza sotto carico. L'obiettivo non è “massimo ACID in ogni momento,” ma scegliere le garanzie che corrispondono ai rischi reali del tuo business.
L'atomicità significa che una transazione è trattata come un'unità singola di lavoro: o termina completamente o non ha alcun effetto. Non ti ritrovi mai con un “aggiornamento a metà” visibile nel database.
Immagina di trasferire $50 da Alice a Bob. Sotto il cofano, questo tipicamente comporta almeno due cambiamenti:
Con l'atomicità, quei due cambiamenti hanno successo insieme o falliscono insieme. Se il sistema non può eseguire entrambi in sicurezza, non deve eseguire nessuno dei due. Questo evita l'incubo in cui Alice viene addebitata ma Bob non riceve i soldi (o Bob li riceve senza che Alice sia stata addebitata).
I database offrono due uscite per le transazioni:
Un modello mentale utile è “bozza vs. pubblica.” Mentre la transazione è in esecuzione, le modifiche sono provvisorie. Solo un commit le pubblica.
L'atomicità è importante perché i fallimenti sono normali:
Se uno di questi succede prima che il commit sia completato, l'atomicità assicura che il database possa effettuare il rollback in modo che il lavoro parziale non entri nei saldi reali.
L'atomicità protegge lo stato del database, ma la tua applicazione deve ancora gestire l'incertezza—specialmente quando una perdita di rete rende poco chiaro se un commit sia avvenuto.
Due complementi pratici:
Insieme, transazioni atomiche e retry idempotenti aiutano a evitare sia aggiornamenti parziali sia addebiti doppi accidentali.
La consistenza in ACID non significa “i dati sembrano ragionevoli” o “tutte le repliche corrispondono.” Significa che ogni transazione deve portare il database da uno stato valido a un altro stato valido—secondo le regole che definisci.
Un database può mantenere la consistenza solo rispetto a vincoli, trigger e invarianti espliciti che descrivono cosa significa “valido” per il tuo sistema. ACID non inventa queste regole; le applica durante le transazioni.
Esempi comuni includono:
order.customer_id deve puntare a un cliente esistente.Se queste regole sono attive, il database respingerà qualsiasi transazione che le violi—così non ti ritrovi con dati “mezzo validi”.
La validazione a livello di app è importante, ma non sufficiente da sola.
Un fallimento classico è verificare qualcosa nell'app (“email disponibile”) e poi inserire la riga. In concorrenza, due richieste possono superare la verifica simultaneamente. Un vincolo di unicità nel database è ciò che garantisce che solo un inserimento abbia successo.
Se codifichi “saldo non negativo” come vincolo (o lo fai rispettare affidabilmente in una singola transazione), allora qualsiasi trasferimento che sconvolgerebbe il conto deve fallire per intero. Se non codifichi quella regola da nessuna parte, ACID non può proteggerla—perché non c'è nulla da far rispettare.
La consistenza riguarda in definitiva l'essere espliciti: definisci le regole, poi lascia che le transazioni assicurino che non vengano mai violate.
L'isolamento assicura che le transazioni non si pestino i piedi a vicenda. Mentre una transazione è in corso, le altre non dovrebbero vedere lavori a metà o sovrascriverli per errore. L'obiettivo è semplice: ogni transazione dovrebbe comportarsi come se girasse da sola, anche quando molti utenti sono attivi contemporaneamente.
I sistemi reali sono trafficati: clienti che fanno ordini, agenti di supporto che aggiornano profili, job in background che riconciliano pagamenti—tutto insieme. Queste azioni si sovrappongono nel tempo e spesso toccano le stesse righe (un saldo conto, il conteggio dell'inventario, o uno slot di prenotazione).
Senza isolamento, il timing diventa parte della logica di business. Un aggiornamento “sottrai stock” potrebbe gareggiare con un altro checkout, o un report potrebbe leggere dati a metà cambiamento e mostrare numeri che non sono mai esistiti in uno stato stabile.
Il pieno isolamento “comportati come se fossi solo” può essere costoso. Può ridurre la throughput, aumentare le attese (lock) o causare retry di transazioni. Nel frattempo, molti flussi di lavoro non necessitano della protezione più severa—per esempio, leggere le analytics di ieri può tollerare piccole incoerenze.
Per questo i database offrono livelli di isolamento configurabili: scegli quanto rischio di concorrenza accettare in cambio di prestazioni migliori e meno conflitti.
Quando l'isolamento è troppo debole per il tuo carico, incontrerai anomalie classiche:
Capire questi casi d'errore rende più facile scegliere un livello di isolamento che corrisponda alle promesse del tuo prodotto.
L'isolamento determina cosa puoi “vedere” delle altre transazioni mentre la tua è in esecuzione. Quando l'isolamento è troppo debole, ottieni anomalie—comportamenti possibili ma sorprendenti per gli utenti.
Dirty read avviene quando leggi dati scritti da un'altra transazione ma non ancora commessi.
Scenario: Alex trasferisce $500 da un conto, il saldo diventa temporaneamente $200, e tu leggi quel $200 prima che il trasferimento di Alex fallisca e venga rollbackato.
Esito utente: un cliente vede un saldo errato basso, una regola antifrode si attiva per sbaglio, o un operatore di supporto fornisce una risposta sbagliata.
Non-repeatable read significa che leggi la stessa riga due volte e ottieni valori diversi perché un'altra transazione ha commitato nel frattempo.
Scenario: Carichi il totale di un ordine ($49,00), poi aggiorni i dettagli e vedi $54,00 perché una riga sconto è stata rimossa.
Esito utente: “Il mio totale è cambiato mentre procedeva il checkout,” causando sfiducia o abbandono del carrello.
Phantom read è simile alla non-repeatable read, ma riguarda un insieme di righe: una seconda query restituisce righe in più (o in meno) perché un'altra transazione ha inserito/eliminato record corrispondenti.
Scenario: Una ricerca di hotel mostra “3 camere disponibili,” poi durante la prenotazione il sistema ricontrolla e non ne trova più perché nuove prenotazioni sono state aggiunte.
Esito utente: tentativi di doppia prenotazione, schermate di disponibilità incoerenti o overselling.
Lost update si verifica quando due transazioni leggono lo stesso valore e entrambe scrivono aggiornamenti, con la scrittura successiva che sovrascrive la precedente.
Scenario: Due admin modificano lo stesso prezzo di prodotto. Partono entrambi da $10; uno salva $12, l'altro salva $11 per ultimo.
Esito utente: la modifica di qualcuno scompare; totali e report sono sbagliati.
Write skew succede quando due transazioni ciascuna applica una modifica che è valida singolarmente, ma insieme violano una regola.
Scenario: Regola: “Almeno un medico on-call deve essere programmato.” Due medici si tolgono entrambi dalla reperibilità dopo aver verificato che l'altro fosse ancora on-call.
Esito utente: rimani senza copertura, nonostante ogni transazione fosse “valida”.
Un isolamento più forte riduce le anomalie ma può aumentare attese, retry e costi sotto alta concorrenza. Molti sistemi scelgono un isolamento più debole per analytics in lettura, mentre usano impostazioni più severe per movimenti di denaro, prenotazioni e altri flussi critici per la correttezza.
L'isolamento riguarda ciò che la tua transazione può “vedere” mentre altre sono in corso. I database lo espongono come livelli di isolamento: livelli più alti riducono i comportamenti sorprendenti, ma possono costare in termini di throughput o aumentare le attese.
I team spesso scelgono Read Committed come default per app user-facing: buone prestazioni e l'assenza di dirty reads corrisponde alle aspettative. Usa Repeatable Read quando ti servono risultati stabili dentro una transazione (es. generare una fattura) e puoi tollerare overhead. Usa Serializable quando la correttezza è più importante della concorrenza (es. far rispettare invarianti complesse come “mai oversellare l'inventario”), o quando non riesci a ragionare facilmente sulle condizioni di gara nel codice applicativo.
Read Uncommitted è raro nei sistemi OLTP; a volte è usato per monitoraggio o reportistica approssimativa dove letture occasionalmente sbagliate sono accettabili.
I nomi sono standardizzati, ma le garanzie esatte differiscono per motore database (e talvolta per configurazione). Conferma con la documentazione del tuo database e testa le anomalie che contano per il tuo business.
La durabilità significa che una volta che una transazione è commessa, i suoi risultati devono sopravvivere a un crash—perdita di corrente, riavvio del processo o reset improvviso della macchina. Se la tua app dice al cliente “pagamento riuscito,” la durabilità è la promessa che il database non “dimenticherà” quel fatto dopo il prossimo guasto.
La maggior parte dei database relazionali raggiunge la durabilità con il write-ahead logging (WAL). A grandi linee, il database scrive una “ricevuta” sequenziale delle modifiche su un log su disco prima di considerare la transazione commessa. Se il database crasha, può riprodurre il log all'avvio per ripristinare le modifiche commesse.
Per mantenere i tempi di recupero ragionevoli, i database creano anche checkpoint. Un checkpoint è un momento in cui il database assicura che abbastanza delle modifiche recenti siano state scritte nei file di dati principali, così il recovery non deve riprodurre una quantità illimitata di log.
La durabilità non è un interruttore on/off; dipende da quanto aggressivamente il database forza i dati su storage stabile.
fsync a livello OS) prima di confermare il commit. Questo è più sicuro, ma può aggiungere latenza.L'hardware sottostante conta anche: SSD, controller RAID con cache di scrittura e volumi cloud possono comportarsi diversamente in caso di guasto.
Backup e replica aiutano a recuperare o ridurre i tempi di inattività, ma non sono la stessa cosa della durabilità. Una transazione può essere durabile sul primario anche se non è ancora arrivata su una replica, e i backup sono tipicamente snapshot a punti nel tempo piuttosto che garanzie commit-per-commit.
Quando esegui BEGIN e poi COMMIT, il database coordina molte parti in movimento: chi può leggere quali righe, chi può aggiornarle, e cosa succede se due persone tentano di cambiare lo stesso record simultaneamente.
Una scelta chiave “sotto il cofano” è come gestire i conflitti:
Molti sistemi mischiano le due idee in base al carico e al livello di isolamento.
I database moderni spesso usano MVCC (Multi-Version Concurrency Control): invece di tenere una sola copia di una riga, il database mantiene più versioni.
Questo è un motivo importante per cui alcuni database gestiscono molte letture e scritture contemporanee con meno blocchi—anche se i conflitti write/write devono ancora essere risolti.
I lock possono portare a deadlock: la Transazione A aspetta un lock tenuto da B, mentre B aspetta un lock tenuto da A.
I database tipicamente risolvono questo rilevando il ciclo e abbandonando una transazione (la “vittima del deadlock”), restituendo un errore così l'app può ritentare.
Se l'applicazione soffre per l'applicazione di ACID, vedrai spesso:
Questi sintomi spesso significano che è ora di rivedere la dimensione delle transazioni, gli indici o quale strategia di isolamento/locking si adatta al carico.
Le garanzie ACID non sono solo teoria del database—influenzano come progetti API, job in background e persino i flussi UI. L'idea centrale è semplice: decidi quali passi devono riuscire insieme, poi racchiudi solo quei passi in una transazione.
Una buona API transazionale di solito mappa a una singola azione di business, anche se tocca più tabelle. Per esempio, un'operazione /checkout potrebbe: creare un ordine, riservare inventario e registrare un intento di pagamento. Quelle scritture dovrebbero tipicamente vivere in una transazione in modo che commitino insieme (o rollbackino insieme) se una validazione fallisce.
Un pattern comune è:
Questo mantiene atomicità e consistenza evitando transazioni lente e fragili.
Dove posizioni i confini delle transazioni dipende da cosa significa “un'unità di lavoro”:
ACID aiuta, ma l'applicazione deve ancora gestire correttamente i fallimenti:
Evita transazioni lunghe, chiamare API esterne dentro una transazione, e tempo di pensiero dell'utente dentro una transazione (per esempio, “blocca la riga del carrello, chiedi conferma all'utente”). Queste pratiche aumentano la contesa e rendono i conflitti di isolamento molto più probabili.
Se stai costruendo un sistema transazionale velocemente, il rischio maggiore raramente è “non conoscere ACID”—è disperdere un'azione di business su più endpoint, job o tabelle senza un confine transazionale chiaro.
Piattaforme come Koder.ai possono aiutarti a muoverti più velocemente mantenendo il design attorno ad ACID: puoi descrivere un workflow (per esempio, “checkout con riserva inventario e intento di pagamento”) in una chat orientata alla pianificazione, generare una UI React più un backend Go + PostgreSQL, e iterare con snapshot/rollback se uno schema o il confine della transazione deve cambiare. Il database continua a far rispettare le garanzie; il valore è accelerare il percorso da un design corretto a un'implementazione funzionante.
Un singolo database può solitamente offrire garanzie ACID all'interno di un confine transazionale. Quando distribuisci il lavoro su più servizi (e spesso più database), quelle stesse garanzie diventano più difficili da mantenere—e più costose quando ci provi.
La coerenza stretta significa che ogni lettura vede la “verità commessa più recente.” L'alta disponibilità significa che il sistema continua a rispondere anche quando parti sono lente o irraggiungibili.
In un setup multi-servizio, un problema di rete temporaneo può forzare una scelta: bloccare o fallire le richieste finché ogni partecipante non è d'accordo (più consistente, meno disponibile), o accettare che i servizi possano essere brevemente fuori sincronia (più disponibile, meno consistente). Nessuna scelta è sempre giusta—dipende dagli errori che il tuo business può tollerare.
Le transazioni distribuite richiedono coordinazione attraverso confini che non controlli completamente: ritardi di rete, retry, timeout, crash di servizio e fallimenti parziali.
Anche se ogni servizio è corretto, la rete può creare ambiguità: il servizio di pagamento ha committato ma il servizio ordine non ha mai ricevuto la conferma? Per risolvere questo in modo sicuro, i sistemi usano protocolli di coordinamento (come two-phase commit), che possono essere lenti, ridurre la disponibilità in caso di failure e aggiungere complessità operativa.
Sagas spezzano un workflow in passi, ciascuno commitato localmente. Se un passo successivo fallisce, i passi precedenti vengono “annullati” con azioni compensative (es. rimborsare un addebito).
Outbox/inbox rende affidabile la pubblicazione e il consumo di eventi. Un servizio scrive i dati di business e un record “da pubblicare” nello stesso database locale (outbox). I consumer registrano gli ID dei messaggi processati (inbox) per gestire retry senza duplicare effetti.
Eventual consistency accetta finestre temporanee in cui i dati differiscono tra servizi, con un piano chiaro di riconciliazione.
Rilassa le garanzie quando:
Controlla il rischio definendo invarianti (cosa non deve mai essere violato), progettando operazioni idempotenti, usando timeout e retry con backoff, e monitorando la deriva (sagas bloccate, compensazioni ripetute, tabelle outbox in crescita). Per invarianti davvero critiche (es. “mai spendere oltre il saldo”), mantienile all'interno di un singolo servizio e di un singolo database transazionale quando possibile.
Una transazione può essere “corretta” in un test unitario e comunque fallire sotto traffico reale, riavvii e concorrenza. Usa questa checklist per mantenere le garanzie ACID allineate con il comportamento in produzione.
Inizia scrivendo cosa deve sempre essere vero (le tue invarianti di dati). Esempi: “saldo account mai negativo,” “totale ordine uguale alla somma delle righe,” “inventario non può scendere sotto zero,” “un pagamento è legato esattamente a un ordine.” Tratta queste regole come regole di prodotto, non trivia del DB.
Poi decidi cosa deve stare dentro una transazione e cosa può essere rimandato.
Mantieni le transazioni piccole: tocca meno righe, fai meno lavoro (nessuna chiamata API esterna) e committa rapidamente.
Rendi la concorrenza una dimensione di test di prima classe.
Se supporti i retry, aggiungi una chiave di idempotenza esplicita e testa “richiesta ripetuta dopo successo.”
Monitora indicatori che le garanzie stanno diventando costose o fragili:
Allerta sulle tendenze, non solo sui picchi, e collega le metriche agli endpoint o job che le causano.
Usa il livello di isolamento più debole che protegga le tue invarianti; non "massimizzarlo" di default. Quando hai bisogno di correttezza stretta per una piccola sezione critica (movimenti di denaro, decremento inventario), restringi la transazione a quella sezione e tieni tutto il resto fuori.
ACID è un insieme di garanzie transazionali che aiutano i database a comportarsi in modo prevedibile in caso di guasti e concorrenza:
Una transazione è una singola “unità di lavoro” che il database tratta come un pacchetto. Anche se esegue più istruzioni SQL (es. creare un ordine, decrementare l'inventario, registrare un intento di pagamento), ha solo due esiti:
Perché gli aggiornamenti parziali creano contraddizioni reali difficili e costose da correggere, ad esempio:
ACID (soprattutto atomicità + consistenza) impedisce che questi stati “metà finiti” diventino verità visibili.
L'atomicità garantisce che il database non esponga mai una transazione “a metà completamento”. Se qualcosa fallisce prima del commit—crash dell'app, perdita di rete, riavvio del DB—la transazione viene rollbackata così che i passaggi precedenti non fuoriescano nello stato persistente.
In pratica, l'atomicità rende sicuri cambiamenti multi-step (ad es. un trasferimento che aggiorna due saldi).
Non sempre sai se un commit è avvenuto quando il client perde la risposta (es. timeout di rete subito dopo il commit). Combina ACID con:
Questo evita sia aggiornamenti parziali sia addebiti/doppi invii accidentali.
In ACID, “consistenza” significa che il database passa da uno stato valido a un altro stato valido in base alle regole che definisci—vincoli, foreign key, unicità e check.
Se non codifichi una regola (es. “il saldo non può essere negativo”), ACID non può proteggerla automaticamente. Il database ha bisogno di invarianti esplicite per far rispettare la consistenza.
La validazione nell'app migliora l'UX e può applicare regole complesse, ma può fallire sotto concorrenza (due richieste che passano la stessa verifica simultaneamente).
I vincoli nel database sono il guardiano finale:
Usa entrambi: valida presto nell'app, fai rispettare definitivamente nel database.
L'isolamento controlla cosa la tua transazione può osservare mentre altre sono in esecuzione. Un'isolamento debole può produrre anomalie come:
I livelli di isolamento ti permettono di scegliere il compromesso fra prestazioni e protezione contro queste anomalie.
Un buon punto di partenza pratico è Read Committed per molte applicazioni OLTP: impedisce i dirty reads e offre buone prestazioni. Salire quando necessario:
Conferma sempre il comportamento sul tuo specifico motore database perché i dettagli variano.
La durabilità garantisce che una volta che il database conferma un commit, la modifica sopravviverà ai crash. Tipicamente ciò si ottiene con il write-ahead logging (WAL) e i checkpoint.
Fai attenzione alle configurazioni:
Backup e replica aiutano nel recupero/disponibilità, ma non sono la stessa cosa della durabilità.