Il tuo ordine è andato a buon fine, ma il pagamento è fallito. Oppure il pagamento è riuscito e il magazzino non ha aggiornato le scorte. Nei microservizi, una transazione che attraversa più servizi è come una staffetta: se un corridore cade, la squadra deve sapere come recuperare. Senza un piano, il tuo sistema resta con dati incoerenti e clienti arrabbiati.
Noi, di Meteora Web, lavoriamo con architetture distribuite da anni. Sappiamo che la teoria delle transazioni ACID non si applica ai microservizi. Qui serve un altro approccio: il saga pattern. In questa guida vediamo come funziona, quando usarlo e come implementarlo senza fare errori costosi.
Perché le transazioni ACID non funzionano nei microservizi?
In un database monolitico, una transazione è atomica: o tutto riesce o tutto viene annullato. Nei microservizi, ogni servizio ha il suo database. Non puoi fare un ROLLBACK su un servizio che non conosce gli altri. Il concetto di transazione distribuita con 2PC (two-phase commit) esiste, ma è fragile: se un servizio cade durante il commit, tutto si blocca. In un sistema distribuito, la latenza e i guasti sono la norma, non l'eccezione.
Il saga pattern spezza una transazione lunga in una sequenza di transazioni locali. Ogni transazione locale aggiorna il database del servizio e pubblica un evento. Il servizio successivo ascolta l'evento e prosegue. Se un passo fallisce, si eseguono le compensating transactions per annullare i passi precedenti.
Un esempio concreto: l'ordine e-commerce
Immagina un ordine che coinvolge tre servizi: Ordini, Pagamenti, Magazzino.
- Ordini crea l'ordine con stato "in attesa".
- Pagamenti addebita la carta.
- Magazzino decrementa le scorte.
Se il magazzino fallisce perché il prodotto non è disponibile, devi annullare il pagamento. La compensating transaction per il passo 2 è un rimborso. Per il passo 1, cancellare l'ordine. Questo è il saga pattern in azione.
Sponsored Protocol
Come funziona il saga pattern in pratica?
Ci sono due modi per orchestrare una saga: coreografia e orchestrazione. La scelta dipende dalla complessità del flusso e dalla necessità di controllo.
Coreografia: eventi che si incatenano
Ogni servizio ascolta gli eventi degli altri e decide se agire. Nessun coordinatore centrale. È semplice da implementare, ma il flusso è implicito: se un evento non arriva, è difficile capire dove si è bloccato.
// Servizio Ordini: pubblica evento dopo la creazione
const event = { type: 'ORDER_CREATED', orderId: 123, amount: 100 };
await eventBus.publish('orders', event);
// Servizio Pagamenti: ascolta e addebita
await eventBus.subscribe('orders', async (event) => {
if (event.type === 'ORDER_CREATED') {
await chargeCreditCard(event.orderId, event.amount);
await eventBus.publish('payments', { type: 'PAYMENT_SUCCESS', orderId: event.orderId });
}
});
// Servizio Magazzino: ascolta e aggiorna
await eventBus.subscribe('payments', async (event) => {
if (event.type === 'PAYMENT_SUCCESS') {
await updateStock(event.orderId);
}
});Con la coreografia, ogni servizio è autonomo. Ma se il flusso cresce, diventa un groviglio difficile da debuggare.
Orchestrazione: un coordinatore che decide
Un orchestratore (un servizio dedicato) guida la saga. Conosce tutti i passi e le relative compensazioni. È più centralizzato, ma più facile da gestire e monitorare.
// Orchestratore: definisce i passi e le compensazioni
const saga = {
steps: [
{ name: 'createOrder', compensate: 'cancelOrder' },
{ name: 'chargePayment', compensate: 'refundPayment' },
{ name: 'updateStock', compensate: 'restoreStock' }
]
};
async function runSaga(order) {
const executed = [];
for (const step of saga.steps) {
try {
await executeStep(step.name, order);
executed.push(step);
} catch (err) {
console.error(`Step ${step.name} failed:`, err);
for (const executedStep of executed.reverse()) {
await compensateStep(executedStep.compensate, order);
}
throw err;
}
}
}Noi preferiamo l'orchestrazione per flussi complessi. Ti dà visibilità e controllo. La coreografia va bene per flussi semplici e indipendenti.
Sponsored Protocol
Quali sono le compensating transactions e come si progettano?
Una compensating transaction è un'azione che annulla gli effetti di una transazione precedente. Non è un rollback: è una nuova transazione che ripristina lo stato. Deve essere idempotente, cioè eseguirla più volte deve produrre lo stesso risultato.
Esempi comuni:
- Pagamento addebitato → rimborso.
- Scorte decrementate → incremento.
- Email inviata → non puoi annullarla, ma puoi inviare una email di correzione.
Non tutte le operazioni sono compensabili. Se hai inviato una notifica push, non puoi "annullarla". In quel caso, devi accettare l'incoerenza temporanea e gestirla a livello applicativo.
Come rendere idempotenti le compensazioni
Usa un ID di transazione univoco. Ogni compensazione deve controllare se è già stata eseguita per quell'ID. In PostgreSQL, puoi usare una tabella dedicata per tracciare le operazioni.
CREATE TABLE transaction_log (
transaction_id UUID PRIMARY KEY,
step_name TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
-- Prima di eseguire una compensazione, controlla se esiste già
INSERT INTO transaction_log (transaction_id, step_name, status)
VALUES ('uuid-123', 'refund_payment', 'started')
ON CONFLICT (transaction_id) DO NOTHING;
-- Se la riga esiste, la compensazione è già stata eseguitaQuesta tabella ti protegge da doppie esecuzioni, soprattutto se il servizio crasha e riparte.
Sponsored Protocol
Come gestire i fallimenti e il retry in una saga?
I fallimenti sono inevitabili. Il tuo sistema deve decidere se riprovare o compensare. La regola generale: se l'errore è temporaneo (timeout, rete), riprova. Se è permanente (dati non validi), compensa.
Implementa un retry con backoff esponenziale per gli errori temporanei. Se dopo N tentativi fallisce ancora, avvia la compensazione.
async function executeWithRetry(fn, maxRetries = 3) {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await fn();
} catch (err) {
if (err.retryable && attempt < maxRetries - 1) {
const delay = Math.pow(2, attempt) * 1000;
await sleep(delay);
attempt++;
} else {
throw err;
}
}
}
}Nel caso di orchestrazione, l'orchestratore deve persistere lo stato della saga su un database. Così, se l'orchestratore crasha, può riprendere da dove si era fermato. Usa un pattern come outbox per pubblicare eventi in modo affidabile.
Il pattern Outbox per la consistenza
Quando un servizio aggiorna il database e pubblica un evento, devi garantire che entrambe le operazioni siano atomiche. Il pattern outbox risolve questo: salvi l'evento in una tabella outbox nella stessa transazione del database. Un processo separato legge l'outbox e pubblica gli eventi sul message broker.
BEGIN;
-- Aggiorna lo stato dell'ordine
UPDATE orders SET status = 'confirmed' WHERE id = 123;
-- Inserisci l'evento nell'outbox
INSERT INTO outbox (event_id, payload) VALUES ('uuid-456', '{"type":"ORDER_CONFIRMED","orderId":123}');
COMMIT;Con l'outbox, non perdi eventi. Se il servizio crasha dopo il commit, il processo di pubblicazione li invierà comunque.
Sponsored Protocol
Quando usare il saga pattern e quando evitarlo?
Il saga pattern non è la soluzione a tutto. Usalo quando hai una transazione che attraversa più servizi e non puoi usare ACID. Evitalo se puoi ridisegnare il sistema per avere meno dipendenze tra servizi.
Alcuni scenari dove il saga è necessario:
- Ordini e-commerce con pagamento e magazzino.
- Prenotazioni di viaggio: volo, hotel, auto.
- Trasferimenti di denaro tra conti in servizi diversi.
Scenari dove puoi evitarlo:
- Se i servizi condividono lo stesso database, usa una transazione ACID.
- Se l'operazione è asincrona e non richiede consistenza immediata, puoi accettare un eventual consistency senza compensazione.
Noi, di Meteora Web, abbiamo visto progetti dove il saga era overkill. Un semplice messaggio asincrono bastava. La chiave è capire i requisiti di consistenza del tuo business.
Quali strumenti usare per implementare una saga?
Non devi costruire tutto da zero. Esistono framework e librerie che facilitano l'implementazione.
Framework Java: Axon e Eventuate
Axon Framework supporta il saga pattern con event sourcing. Eventuate Tram Sagas di Chris Richardson (l'autore del pattern) è un'altra opzione solida.
Librerie per Node.js e Python
In Node.js, puoi usare saga-orchestrator (npm). In Python, saga-pattern (PyPI). Sono librerie semplici che implementano l'orchestrazione.
Message broker come infrastruttura
Kafka o RabbitMQ sono la spina dorsale. Gestiscono gli eventi e garantiscono la consegna. Noi usiamo spesso Kafka per la sua alta affidabilità e scalabilità.
Per la documentazione ufficiale, guarda il saga pattern su microservices.io e la guida di Confluent su Kafka.
Sponsored Protocol
Errori comuni da evitare nelle transazioni distribuite
Anche i migliori sviluppatori sbagliano. Ecco gli errori che vediamo più spesso nei progetti che ci arrivano.
Non rendere idempotenti le operazioni
Se una compensazione viene eseguita due volte, può corrompere i dati. Usa un ID di transazione e controlla sempre lo stato.
Ignorare il fallimento dell'orchestratore
Se l'orchestratore crasha, la saga si blocca. Persisti lo stato e implementa un meccanismo di ripristino.
Mischiare sincrono e asincrono senza criterio
Le chiamate sincrone tra servizi aumentano l'accoppiamento. Preferisci eventi asincroni. Ma se devi restituire una risposta al cliente, usa un pattern come saga con risposta asincrona.
Cosa fare adesso
Ecco le azioni immediate per implementare il saga pattern nel tuo sistema:
- Analizza i tuoi flussi: identifica le transazioni che attraversano più servizi e dove serve consistenza.
- Scegli il modello: coreografia per flussi semplici, orchestrazione per quelli complessi.
- Progetta le compensazioni: per ogni passo, definisci l'azione di annullamento e rendila idempotente.
- Implementa il retry: con backoff esponenziale per errori temporanei.
- Persisti lo stato della saga: usa una tabella o un database per riprendere dopo un crash.
Se vuoi approfondire l'architettura dei microservizi, leggi la nostra guida pillar sui microservizi. E se il tuo sistema ha già problemi di consistenza, parlane con noi. Sappiamo come risolverli.
Le transazioni distribuite non sono un problema da sottovalutare. Con il saga pattern, però, puoi gestirle in modo robusto. Il tuo sistema resterà coerente anche quando le cose vanno male. E questo, nel nostro lavoro, fa la differenza tra un cliente che torna e uno che se ne va.