mndev
EN/UA/IT
Tutte le note
5 min di lettura

Sincronizzare 500K diamanti senza una sola doppia vendita

L'architettura di un marketplace di diamanti con 500K+ prodotti live: scansione adattiva, 200 worker paralleli, cache versionata e un sistema di prenotazione.

Una doppia vendita è il peggior modo di fallire nel commercio. Non per il rimborso, ma per ciò che dice al cliente: ha scelto una pietra, ha inserito i dati della carta e poi gli è stato comunicato che la cosa acquistata non esiste. Nessuna qualità di design recupera da lì.

Calavera vende diamanti tramite Nivoda: un catalogo di 500.000+ pietre da fornitori globali, dove ognuna è unica e qualsiasi di esse può essere venduta da qualcun altro in qualsiasi momento. La giacenza non è un numero che scende. Sono 500.000 articoli singoli la cui esistenza è controllata da terzi.

Zero doppie vendite dal lancio. Questa è l'architettura che l'ha prodotto.

Perché l'approccio ovvio non funziona

L'approccio ovvio è interrogare l'API del fornitore al caricamento della pagina. Fallisce sulla latenza: una ricerca su 28 parametri diventa una chiamata live all'infrastruttura di qualcun altro, e una vetrina di lusso non può chiedere al cliente di aspettare due secondi per applicare un filtro.

Il secondo approccio ovvio è una sincronizzazione notturna completa in un database locale. Fallisce sulla freschezza: una pietra venduta alle 9:00 resta in elenco fino al giro della notte successiva. Con un catalogo di queste dimensioni non è un caso limite raro — è lo stato normale del sistema.

La risposta che funziona non è nessuna delle due, e non è un compromesso fra loro. È una pipeline che riconcilia di continuo una copia canonica locale, più un livello di prenotazione che rende innocua la staleness residua.

Scansione adattiva invece di forza bruta

Iterare 500.000 record pagina per pagina a intervalli fissi è insieme troppo lento dove conta e sprecato dove non conta. Le giacenze di diamanti non sono distribuite in modo uniforme: le fasce di prezzo con più pietre sono anche quelle con più ricambio, mentre l'estremo raro ad alto carato cambia lentamente.

Lo scheduler divide il catalogo in fasce di prezzo e scansiona per densità — le fasce con più pietre e più ricambio vengono visitate più spesso. Il lavoro viene emesso come messaggi, una pagina per messaggio, ed elaborato da circa 200 worker concorrenti.

Due proprietà lo rendono robusto e non soltanto veloce:

  • Una pagina per messaggio. Il crash di un worker perde una pagina di lavoro, riprovata in modo indipendente. Non esiste un job lungo da perdere.
  • La concorrenza è una manopola, non una costante. È tarata sui rate limit del fornitore, e abbassarla degrada la freschezza invece di rompere la correttezza.

La pipeline in tre fasi

I dati grezzi del fornitore non vengono mai scritti direttamente nella fonte di verità della vetrina. Passano da tre fasi, ognuna con una sola responsabilità:

  1. 01Map — normalizza i campi del fornitore nello schema canonico. I fornitori non concordano su enti di certificazione, nomi delle forme e formati delle misure, e quel disaccordo viene contenuto qui.
  2. 02Price — applica regole di ricarico, conversione valuta e arrotondamento. La logica dei prezzi vive in un solo posto, ed è questo a renderla verificabile.
  3. 03Rate — assegna punteggio e ordinamento a ogni pietra per la rilevanza in ricerca, così il catalogo può essere ordinato con qualcosa di più utile del prezzo.

Separarle è ciò che mantiene il sistema diagnosticabile. Quando una pietra mostra un prezzo sbagliato, la fase è nota prima ancora di iniziare l'indagine. Una singola trasformazione monolitica trasforma ogni bug nella lettura dell'intera pipeline.

Ricerca: sotto i 50 ms su 28 faccette

Il dataset canonico è indicizzato in Typesense, che serve la ricerca su 28 parametri — carato, taglio, colore, purezza, fluorescenza, certificazione, misure — con risposte sotto i 50 ms.

La ricerca lavora interamente sull'indice locale. Non tocca mai l'API del fornitore. È la decisione che rende la vetrina istantanea, ed è difendibile solo perché la pipeline dietro tiene onesto l'indice.

Davanti c'è una cache versionata con supporto ETag: 95% di hit e 85% delle richieste condizionali chiuse con un 304. La versione fa parte della chiave, quindi un aggiornamento dei dati invalida in modo atomico invece che per scadenza.

// La versione entra nella chiave: una nuova versione del dataset
// invalida tutte le voci dipendenti insieme, senza finestre scaglionate.
const cacheKey = `search:v${datasetVersion}:${hash(filters)}`

Il sistema di prenotazione

Tutto quanto sopra riduce la finestra di staleness. Nulla la chiude. Fra l'ultima sincronizzazione di una pietra e il momento in cui il cliente clicca «acquista», quella pietra può essere venduta altrove.

Quindi l'ultimo passo non cerca di impedire la staleness: la rende innocua. Quando una pietra entra nel carrello viene prenotata, e la disponibilità viene riverificata presso il fornitore in quell'istante. Le prenotazioni si aggiornano ogni ora. Il controllo avviene una volta, sull'unico articolo che interessa davvero al cliente, nell'unico momento in cui la correttezza conta.

È tutto qui il trucco, e si generalizza ben oltre i diamanti:

La navigazione può essere eventualmente consistente. Il checkout no. Spendi il budget di consistenza nel punto dell'impegno, non a ogni visualizzazione di pagina.

Un controllo di disponibilità live su ogni risultato di ricerca sarebbe insostenibile e rallenterebbe il catalogo fino al passo d'uomo. Su una sola pietra al momento del carrello è una chiamata — ed è la chiamata che elimina la doppia vendita.

Come si tiene in esercizio

Una pipeline che gira senza sorveglianza va comunque osservata. Una dashboard di 15 pagine segue 487K diamanti su tre feed con il 99,7% di successo — throughput per fase, conteggio degli errori, versione ed età del dataset.

Il numero che conta operativamente non è il tasso di successo ma l'età dei dati: quanto tempo è passato dall'ultima riconciliazione di ogni fascia di prezzo. Il tasso di successo dice che la pipeline gira. L'età dice se la vetrina sta dicendo la verità.

Cosa si generalizza

La maggior parte dei negozi non sincronizzerà mai mezzo milione di prodotti altrui. Le decisioni strutturali restano comunque trasferibili:

  • Non lasciare mai che una richiesta rivolta al cliente dipenda da un'API di terzi. Sincronizza in qualcosa che controlli e servi da lì.
  • Dividi l'ingestione in fasi con una sola responsabilità, così i guasti restano localizzati.
  • Rendi l'invalidazione della cache funzione della versione dei dati, non del tempo trascorso.
  • Verifica nel punto dell'impegno. Accetta la consistenza eventuale ovunque altro.
  • Monitora l'età dei dati, non solo il successo dei job.

Il risultato su questo progetto: 500K+ pietre ricercabili in meno di 50 ms, precisione delle giacenze del 100% al checkout e zero doppie vendite dal lancio — da otto microservizi che girano senza nessuno a guardarli.

Scrivimi su Telegram