La guida riluttante alle migrazioni Shopify

Atto IV: L'incastro

Costruire su misura (quando devi)

La documentazione di Shopify è, sinceramente, una delle migliori documentazioni di piattaforma del settore. È completa, è aggiornata, è ricercabile, ed è gratuita. Non abbiamo alcuna intenzione di dirti di smettere di leggerla. Stiamo per dirti qualcosa di più scomodo, e cioè che la documentazione non è scritta per te—o meglio, è scritta per te e anche per altre quattro persone, tutte sulla stessa pagina, senza nessun cartello appeso a dire quale paragrafo appartiene a chi.

Immagina la singola pagina web che spiega come costruire un’app Shopify. La legge lo sviluppatore indie—quello che spedisce un calcolatore di mance a undicimila negozi, che ha bisogno che la cosa costi poco da far girare, sia banale da deployare, e dimenticabile nell’istante in cui funziona. La legge il systems integrator Plus—quello che cabla un singolo negozio dentro un ERP che era già vecchio quando lo sviluppatore era a scuola. La legge l’hobbista, una domenica, per vedere se è divertente. E la leggi tu: il lead di ingegneria di un brand che fattura nove cifre, che sta per costruire una applicazione che dovrà sopravvivere a cinque anni, a una riorganizzazione, a due migrazioni di piattaforma, e all’uscita verso un competitor proprio della persona che l’ha scritta. La pagina serve tutti voi insieme. I default a cui ricorre—lo starter template, il tunnel, la sottoscrizione webhook, il comando di deploy—sono corretti. Sono corretti per chiunque la documentazione avesse in mente quando vi ha fatto ricorso. Il guaio è che la documentazione non dice chi fosse, e la persona che aveva in mente era, il più delle volte, lo sviluppatore indie. Il percorso di default si rivela essere stato progettato per qualcun altro.

Illustrazione di un singolo rubinetto dell'acqua dotato di quattro beccucci divergenti, ognuno rivolto in una direzione diversa. Solo uno ha una tazza posizionata per raccogliere l'acqua; gli altri la versano liberamente su un pavimento che non si vede.
Una pagina. Quattro lettori. Uno di loro sei tu.

Tutto questo capitolo parla di quali default sovrascrivere, e perché, e quanto ti costa quando non lo fai. Nessuno degli override è esotico. La maggior parte sono il genere di cosa a cui un senior engineer arriva da solo, prima o poi, verso le due del mattino, dopo che la cosa che il default gli aveva detto di fare è crollata in produzione. Noi vorremmo semplicemente che ci arrivassi a un’ora più ragionevole.


Il primo punto in cui i default ti presentano il conto è quello che scegli prima ancora di aver scritto una riga: lo stack. Su Shopify, un serio build custom è raramente un pezzo solo. È un’app, più qualche Shopify Functions per la logica di carrello e checkout che l’app non può toccare, più magari una UI Extension o due per le superfici che i clienti vedono davvero. Queste non sono tre decisioni indipendenti. Interagiscono, e nell’interazione si accumula il rimpianto.

La risposta seducente è un linguaggio per tutto. Scrivi l’app in JavaScript, scrivi le Functions in JavaScript, condividi i tipi attraverso il confine, assumi per un solo set di competenze. È un argomento sinceramente valido e l’abbiamo sostenuto noi stessi, all’inizio dei progetti, con convinzione. Il conto arriva dopo. Node non ha un Rails. Lo intendiamo quasi letteralmente: non esiste un singolo framework, noioso e assestato, nell’ecosistema Node che ti consegni code, background job e routing delle richieste già risolti, nel modo in cui Rails ti consegna Active Job e Sidekiq e un router fin dal primo giorno. Così li assembli. Scegli una libreria per le code, scegli un job runner, cabli il routing, e prendi queste decisioni infrastrutturali nel mezzo del progetto, mentre dovresti costruire feature, perché i default della piattaforma ti hanno fatto entrare in un linguaggio il cui ecosistema ti costringe a portarti dietro le tue tubature. Un linguaggio per tutto è allettante fino al momento esatto in cui stai facendo debug di un incidente in produzione a mezzanotte e vorresti avere Sidekiq. (Chiedici come lo sappiamo. Il Sidekiq che avremmo voluto, quella notte, era molto specifico.)

Quindi il nostro default—ed è solo un default, un punto da cui iniziare a discutere—va nell’altra direzione. Ruby e Rails per l’app: Sidekiq e Active Job rendono il queuing dei webhook un problema risolto invece di un progetto di ricerca, il routing è nella scatola, e c’è persino una gem Liquid così puoi testare i template che lo storefront renderizzerà davvero. Il costo è onesto e lo nominiamo: non esiste un Ruby per le Shopify Functions. Nessuno. Le Functions girano in una sandbox di calcolo vincolata, e le tue scelte sono TypeScript o Rust, quindi il sogno di un solo linguaggio non è mai stato sul tavolo per la parte del sistema che calcola il prezzo del carrello. Il che va bene, perché per le Functions in particolare propendiamo comunque verso Rust. I limiti di calcolo sono abbastanza stretti che il margine di performance di Rust smette di essere un vezzo e inizia a essere il motivo per cui la Function ci sta dentro il budget, punto; e poiché il testing end-to-end delle Functions è ancora scarno, il compilatore che ti fa il type-checking sta facendo un vero lavoro di sicurezza che non puoi facilmente ottenere in nessun altro modo. App in Ruby on Rails, Functions in TypeScript o Rust. Questo è il default. (Chiedici come lo sappiamo.)

Se quella raccomandazione fa storcere il naso al tuo team—fluente in JavaScript, indifferente a Ruby—nota che il terreno sotto la domanda si è spostato. Una volta sceglievi il linguaggio che il tuo team già conosceva, perché il costo della scarsa familiarità si misurava in settimane di brancolamenti. Il tooling AI ha silenziosamente ridotto quel divario. Un bravo engineer può ora essere produttivo in un framework non familiare-ma-convenzionale molto più in fretta di quanto potesse tre anni fa, il che significa che la vecchia domanda—quale linguaggio conosce il mio team?—ha perso quasi tutto il suo peso. La domanda che l’ha sostituita è più affilata e più difficile da schivare: quale ecosistema ti dà infrastruttura già risolta per i problemi che colpirai di sicuro? Colpirai di sicuro il queuing dei webhook. Colpirai di sicuro i background job. Il linguaggio in cui il tuo team si trova a suo agio non cambierà se quei problemi esistono; cambia soltanto se li risolvi o li ricostruisci.


Il che ci porta ai webhook, dove il default della piattaforma è al suo più affascinante e al suo più pericoloso, perché il default funziona davvero—per un po’, a una dimensione che ti lascerai alle spalle.

Il percorso da tutorial è semplice ed è quello che ti mostra la documentazione: ti sottoscrivi a un evento, Shopify fa una HTTP POST al tuo endpoint quando l’evento accade, la tua app fa qualcosa. Ordine creato, parte un webhook. Cliente aggiornato, parte un webhook. È reattivo, è real-time, è un solo diagramma che puoi disegnare su un tovagliolo, e in un negozio piccolo è corretto. A volumi enterprise ha quattro failure mode, e non si annunciano; si accumulano.

Gli eventi arrivano fuori ordine. Shopify non ti promette che cliente aggiornato atterri dopo cliente creato, e a volume a volte non lo farà, e adesso il tuo handler deve mettersi sulla difensiva rispetto a un mondo in cui gli effetti precedono le loro cause. Gli eventi si perdono—in silenzio. Se la tua app non è disponibile per i trenta secondi che hai passato a deployare, i webhook scoccati in quella finestra non vengono pazientemente riconsegnati per sempre; alcuni di loro semplicemente non ci sono più, e niente te lo dice, e lo scopri settimane dopo quando un report non quadra. Non c’è una dead-letter queue integrata, non c’è un fan-out integrato—se due parti del tuo sistema hanno entrambe bisogno di sapere di un ordine, è un problema tuo da risolvere, nel tuo codice, su ogni evento. E il più crudele: i rate limit dell’API di Shopify si applicano alle tue reazioni, quindi uno storefront trafficato può scoccare eventi più in fretta di quanto tu riesca a processarli, e un’architettura reattiva può esaurire la tua quota API solo per stare al passo con gli eventi in arrivo—bruciando l’intero budget per restare in pari, senza nulla che avanzi per il lavoro che volevi fare davvero.

Illustrazione di un gomitolo di spago che corre da un singolo nodo d'ancoraggio ordinato sulla sinistra a una dozzina di fili che si sfilacciano, slegati, spargendosi liberamente nell'aria sulla destra.
Il fan-out vive nella tua infrastruttura. Non nelle impostazioni di un vendor.

La forma che sopravvive è smettere del tutto di lasciare che Shopify faccia POST direttamente dentro la tua applicazione. Mettici in mezzo un vero bus. La versione a cui facciamo ricorso: Shopify consegna gli eventi a un bus AWS EventBridge, EventBridge li instrada dentro SQS, e SQS ti dà le tre cose che i webhook HTTP grezzi non hanno mai dato—durabilità, così un evento aspetta in coda invece di evaporare mentre deployi; ordinamento, dove ti serve; e una dead-letter queue, così il messaggio che non si può processare atterra da qualche parte dove puoi vederlo invece di svanire. Ti serve un secondo consumer? Aggiungi un’altra coda SQS sottoscritta allo stesso bus senza toccare una sola cosa nella tua configurazione di Shopify—il fan-out vive nella tua infrastruttura, dove lo controlli tu, non nelle impostazioni webhook di un vendor. CloudWatch ti dà l’observability. E come bonus silenzioso che il tuo team di sicurezza amerà più di quanto ti aspetti, la tua applicazione smette del tutto di esporre qualsiasi endpoint HTTP all’internet pubblico; legge da una coda che possiede invece di aspettare che il web aperto bussi.

C’è la tentazione, dopo aver letto gli ultimi due paragrafi, di andare a costruire tutto quanto immediatamente, e vogliamo prevenirla, perché la frase più importante di questa sezione è quella che ti dice quando non farlo. Prima di costruire un’architettura webhook reattiva completa, chiediti se l’operazione abbia davvero bisogno di essere real-time. Moltissime non ce l’hanno. L’esempio a cui torniamo sempre è la compliance Omnibus sull’esposizione dei prezzi—la norma UE per cui mostri il prezzo più basso che un articolo ha avuto nei trenta giorni precedenti. Sembra un problema da webhook: il prezzo cambia, ricalcola il prezzo-precedente-più-basso, reagisci. Ma alla legge non importa se la cifra che mostri è vecchia di trenta secondi. Una bulk operation notturna che percorre il catalogo e ricalcola i numeri è legalmente sufficiente, ed è drasticamente—quasi offensivamente—più semplice di una pipeline reattiva. Niente bus, niente coda, niente ansia da ordinamento, niente matematica delle quote. Un job, una volta a notte, il genere di cosa su cui puoi ragionare completamente mentre ti fai il caffè. Metà delle architetture reattive che abbiamo visto costruire erano monumenti a un requisito real-time che nessuno aveva davvero verificato fosse reale.


Una breve digressione, perché manda fuori strada il paragrafo qui sopra e si merita un angolo tutto suo.

Il tunnel merita più di una targhetta, perché è il punto in cui un team multi-sviluppatore incontra l’assunzione single-developer frontalmente, di solito nella prima settimana, di solito nella confusione.

Ecco la scena, e al lead di ingegneria che sta per lanciare il quickstart ufficiale della Shopify-CLI con tutto il team che guarda lo schermo condiviso: questa parte è gentile, e poi è un avvertimento. La CLI è meravigliosa. Lanci un comando e tira su un’app di sviluppo, apre un tunnel verso la tua macchina locale, e inizia a consegnare webhook live al codice sul tuo laptop. La prima volta sembra magia. L’avvertimento è che la magia è costruita per una persona sola. La CLI crea un singolo tunnel webhook per app di sviluppo, e i webhook vanno a chiunque abbia avviato il proprio server più di recente. Così il tuo secondo sviluppatore lancia lo stesso comando, e il tunnel si ri-punta silenziosamente su di lui, e ora l’app locale del primo sviluppatore è diventata sorda e non lo sa—lì seduta, server in esecuzione, a non ricevere niente, a fare debug di un silenzio che non è un bug. Due engineer, una sola casella di posta, e la posta va a chiunque l’abbia toccata per ultimo. In un team di sei è un taglietto quotidiano che costa ore vere e un ricorrente “aspetta, a te arrivano i webhook? perché a me no” nel canale.

Il rimedio è smettere di combattere col tunnel e aggirarlo. Punta l’app di sviluppo allo stesso bus EventBridge che il resto della tua architettura già usa, e lascia che ogni sviluppatore legga il proprio stream da lì. Un engineer di MeUndies, stanco esattamente di questo, ha costruito un piccolo lettore di log EventBridge che ha dato a ogni sviluppatore uno stream di eventi privato e personale—la propria vista degli eventi, nessuna lotta per un singolo tunnel, nessun laptop sordo. Non era un grosso pezzo di software. Era il lavoro di un sabato che si è ripagato entro il martedì successivo, ed è il genere di cosa che la documentazione non ti dirà mai di costruire, perché la documentazione sta ancora, premurosamente, parlando all’unico sviluppatore che immagina tu sia. E la storia dello staging fa rima con questa: Shopify non ha alcuna nozione nativa di ambienti per le app custom—nessun interruttore “staging”, nessun menu a tendina degli ambienti—quindi te lo costruisci. Due app separate nella Dev Dashboard, una di staging e una di produzione, lo stesso codebase che deploya su ciascuna con credenziali diverse, la CI che spinge il tuo branch main verso lo staging e le tue release taggate verso la produzione. I token di automazione delle app arrivati all’inizio di quest’anno hanno reso questo pulito in un modo che francamente prima non era; quello che prima richiedeva un umano che cliccava in giro per una dashboard è ora uno step di deploy come un altro. La piattaforma non ti consegnerà gli ambienti. Ti lascerà, ora, costruirteli come si deve.


L’ultimo default è il più grosso, il più alla moda, e quello su cui abbiamo l’opinione più forte: quante cose deployi.

Il pitch costante—dai talk delle conference, dai gentili suggerimenti della console AWS, da almeno un engineer in ogni team che ha letto un buon post sul blog—è il serverless. Un Lambda per webhook. Una function per preoccupazione. Pezzi piccoli, indipendenti, scalabili all’infinito, ognuno che fa una cosa sola. Suona come la pulizia in persona, e per alcuni sistemi è la risposta giusta. Per una integrazione Shopify enterprise, posseduta da un solo team, tende a essere un errore al rallentatore, e gli errori sono specifici. La tua logica di business si frammenta su una dozzina di unità di deploy, così la regola su come si cumula uno sconto ora vive in tre Lambda e in una variabile d’ambiente, e capirla significa aprire quattro console. I cold start fanno male proprio dove te lo puoi permettere di meno—il percorso di carrello e checkout sensibile alla latenza, dove un Lambda che si sveglia aggiunge i millisecondi che un cliente sente. E tracciare un singolo fallimento attraverso diversi Lambda, un flusso multi-step che ha toccato quattro function e si è rotto alla terza, è un pomeriggio passato con un correlation ID e una preghiera, mentre in un singolo processo sarebbe stato un solo stack trace.

Quindi ricorriamo, deliberatamente e un po’ fuori moda, a quello che DHH e la gente di 37signals hanno chiamato il Majestic Monolith. Una sola applicazione ben strutturata e a lunga esecuzione. Background job messi in coda internamente invece che sparpagliati per il cloud. Un solo stream di log—quando qualcosa si rompe, c’è un solo posto dove guardare. Una sola superficie di deploy—quando rilasci, rilasci una cosa sola. È noioso da gestire, e questa non è una scusa, è l’intero argomento: noioso da gestire—che è esattamente il punto. Le ore che non passi a correlare log tra le function e a ragionare sui cold start sono ore che passi sul business. Tannico gira così in produzione—una sola app, un business del vino enterprise multi-market, un catalogo con una complessità reale, diverse integrazioni appese a esso—e il motivo per cui resta comprensibile è che ce n’è una sola. Una app, un log, un solo posto dove vive la verità.

Illustrazione di un solido edificio in mattoni con una finestra calda e illuminata, in piedi in un campo cosparso di decine di minuscole e precarie torri di fiammiferi a vari angoli, alcune visibilmente spente o fumanti.
Noioso da gestire. Questa non è una scusa. È l'intero argomento.

Questo è lo stesso istinto del test delle primitive del capitolo precedente, puntato verso l’interno. Lì, la domanda era se un’app si costruisca sui concetti nativi di Shopify o contrabbandi dentro uno strato parallelo; qui, la domanda è se il tuo stesso sistema costruisca una cosa coerente o ne contrabbandi dentro una dozzina parallele. Stesso temperamento, stesso guadagno: meno mondi separati devi riconciliare ai confini, più a lungo il tuo sistema resta qualcosa che un umano può tenere in testa.

E quindi la riga che vogliamo ti porti fuori da questo capitolo, quella che vale la pena attaccare a un monitor dove il prossimo architecture-astronaut del tuo team possa vederla: parti da un monolite. Stacca un servizio solo quando hai un problema di scaling concreto e dimostrato. Non uno previsto. Non uno da lavagna. Uno reale, con un grafico dietro, che puoi indicare. Spaccare presto significa pagare l’intera tassa operativa della distribuzione per una scala che non hai e che forse non raggiungerai mai. Puoi sempre ritagliare un servizio da un monolite ben strutturato quando il grafico finalmente lo richiede. Non puoi facilmente riassemblare dodici Lambda in qualcosa su cui puoi ragionare.


C’è una versione più quieta della stessa decisione che siede uno strato più sotto, in come la tua unica app tiene i suoi dati. Prima o poi sceglierai tra il rispecchiare i dati di Shopify dentro il tuo database—sincronizzarli, tenere una copia locale aggiornata con webhook e riconciliazione—e il restare API-first, interrogando Shopify a domanda e tenendo il meno possibile. Il mirroring ti compra query locali veloci, resilienza quando l’API di Shopify ha un brutto pomeriggio, e la capacità di fare analytics vere su dati modellati come ti servono; ti costa una sync da costruire, mantenere e riconciliare, più il rischio permanente che la tua copia e la verità divergano in silenzio. L’API-first ti compra semplicità e dati che sono sempre aggiornati per definizione; ti costa latenza, e lega il tuo destino ai rate limit e all’uptime di qualcun altro. La risposta onesta è di solito un ibrido—rispecchia i dati di riferimento che interroghi di continuo e che cambiano raramente, resta API-first per i dati operativi che devono essere aggiornati al secondo—ma il meta-punto è quello da tenere stretto: lascia che sia il requisito di business a guidare l’architettura, non il contrario. Decidi tu a cosa servono i dati, e la forma segue. Decidi prima la forma e passerai un anno a spiegare al business perché il sistema non può fare la cosa ovvia.


Niente di tutto questo è una stoccata a Shopify, e vogliamo essere chiari su questo, perché un capitolo che passa così tanto tempo a elencare default da sovrascrivere può leggersi come un capitolo che pensa che la piattaforma abbia sbagliato. Non l’ha fatto. I default sono buone risposte. Sono semplicemente risposte a domande poste da persone che non stanno costruendo quello che stai costruendo tu, alla scala a cui lo stai costruendo, con la durata di vita di cui hai bisogno. I team che riescono nello sviluppo custom su Shopify non sono quelli con l’architettura più ingegnosa o lo stack più esotico. Sono quelli che hanno capito quali default sovrascrivere, avevano ragioni specifiche per ogni override, e hanno costruito l’infrastruttura che quelle ragioni richiedevano—non di più, non di meno.

Quindi quando apri quella pagina di documentazione domani—e dovresti, è davvero eccellente—leggila come un traduttore legge una lettera indirizzata a qualcun altro. La maggior parte è per te. Una parte è per lo sviluppatore indie che spedisce a undicimila negozi, e quella parte sembrerà esattamente come la parte che è per te, e l’unico modo per distinguerle è continuare a porsi la domanda che questo capitolo si è posto fin dall’inizio: per chi era progettato questo default, e sono io? Di solito non lo sei. Sovrascrivilo apposta, scrivi perché, e vai a letto a un’ora ragionevole. La versione di te di mezzanotte ti sarà grata, e così l’engineer che eredita questo fra tre anni e trova, contro ogni previsione, un sistema che può davvero capire.