Lunedì 6 luglio 2026, 9:12. Apro un nuovo file: spec.md. Riga 1, intestazione: # Refactor global scope multi-tenant: Application Service. Riga 2, vuota. Riga 3, primo paragrafo. Niente codice aperto in editor, nessun branch nuovo creato sul repo, nessun task aperto sull’agente. Solo un file markdown e una giornata davanti. Quattro settimane dopo, venerdì 31 luglio, lo stesso file è ancora spec.md, è cresciuto a otto pagine, ed è il documento più importante che abbia scritto in tutto il 2026.
In mezzo, la prima implementazione che l’agente ha prodotto, l’ho buttata via il pomeriggio del primo giorno. Non per sfizio: per un vincolo di consistency sul cluster MariaDB con replica circolare che il modello non aveva colto, e che peraltro non gli avevo neppure spiegato bene. Quel taglio è il momento in cui ho capito una cosa che adesso provo a mettere giù in ordine: la specifica è il vero artefatto, il codice è solo il prodotto della specifica fatta passare attraverso il modello del momento.
Il problema: rifare il global scope multi-tenant senza rompere la replica
Il modulo da rifare è il cuore del nostro Application Service in Laravel. Più di trenta model che vivono dentro un boundary tenant-aware: ogni record appartiene a un tenant, ogni query deve essere automaticamente scoped al tenant del request corrente, ogni job nelle code Horizon deve riportarsi dietro il TenantContext, ogni cache key deve avere il prefix coerente. Il pattern è classico Laravel: un Global Scope che inietta il filtro tenant_id = ? su tutti i model, un middleware HTTP che imposta il contesto, un payload di queue arricchito con il contesto, un trait che si occupa del resto.
Il vecchio scope funzionava da due anni e mezzo, da quando avevamo estratto la piattaforma a partire dal partner storico. Funzionava su una decina di tenant, sotto carico moderato, con un cluster MariaDB single-master più replica di lettura. Non era pensato per quello che è il nostro stack di oggi: replica circolare a due nodi, codename interni james e jason, davanti a MaxScale come router con session-level read-write split, MariaDB Galera escluso per scelta a favore della replica asincrona classica, e un vincolo che è il punto centrale di tutta questa storia.
Il vincolo è questo, lo riformulo in termini astratti perché coincide con la proprietà tecnica e non con i FQDN: il sistema deve garantire che qualsiasi entità tenant-aware scritta sul nodo primario sia visibile alle letture immediatamente successive sullo stesso percorso utente, anche se il router decide di servire la lettura dal nodo secondario per bilanciare il carico. È il classico problema “read your writes” sotto eventual consistency: la replica fra i due nodi non è istantanea (parliamo di 15-80 millisecondi sotto carico tipico, fino a centinaia di millisecondi se il primario è sotto pressione), e una sessione utente che scrive un record e poi naviga subito su una pagina che lo legge non può accettare di vederlo “sparire” perché è stata servita dal secondario non ancora sincronizzato.
A questo si somma la regola di scrittura: per evitare conflitti di replica circolare, le scritture per uno stesso tenant devono andare sempre sullo stesso nodo finché la sessione è viva. È un’affinità di sessione applicata al livello di tenant, non al livello di utente.
Il vecchio scope non gestiva nessuno di questi due punti. Gestiva il tenant_id come filtro di query, e basta. La pipeline di MaxScale era stata configurata in modo conservativo (tutte le scritture sul primario, tutte le letture sul primario, secondario solo come fallback) e quel setup era diventato un collo di bottiglia adesso che i tenant sono cresciuti. Volevamo aprire il read-write split, ma per farlo ci serviva uno scope che capisse il vincolo.
Il primo prototipo dell’agente: 850 righe in quattro ore
Lunedì pomeriggio. Apro Claude Code, gli passo il file spec.md in versione embrionale (due pagine), gli dico: “implementa il nuovo global scope multi-tenant rispettando questi requisiti”. L’agente lavora quattro ore. Mi consegna una pull request da 850 righe: nuovo TenantScope, nuovo middleware, nuovo trait BelongsToTenant aggiornato, una pipeline interessante per il routing delle query verso il nodo giusto via un connection resolver custom, una manciata di test.
Lo leggo. È intelligente, ben scritto, segue le convenzioni Laravel, i nomi sono buoni. Ma non funziona. Più precisamente: passa i test che ha scritto, ma viola il vincolo di consistency che gli avevo enunciato male nella specifica.
Il punto preciso del difetto: l’agente aveva implementato l’affinità di sessione lato applicazione, mantenendo in cache (Redis) la mappatura tenant_id → nodo per la durata della sessione utente. Sembra ragionevole. Ma sotto carico reale, con sessioni utente che durano 8 ore e con job di queue che scrivono per conto del tenant in modo asincrono, la cache si sarebbe disallineata: un job Horizon partito sul nodo jason per un tenant la cui sessione web era pinnata a james avrebbe scritto sul nodo “sbagliato” rispetto alla mappatura, e la successiva lettura della sessione web avrebbe potuto non vedere il record per la finestra di replica.
Il modello non aveva colto che l’affinità doveva essere applicata sia al traffico HTTP sia ai job di queue, e che la sorgente di verità della mappatura non poteva essere una cache lato applicazione (che vive per sessione) ma doveva essere una proprietà del tenant stesso, persistita.
Buttare via 850 righe scritte da un altro è facile. Buttare via 850 righe scritte da te in quattro ore è meno facile, dal punto di vista emotivo. Buttare via 850 righe scritte dall’agente è una decisione tecnica pulita: nessun ego in mezzo, solo la domanda “questa cosa rispetta gli invariants?”. La risposta era no. Cancellata, branch chiuso, ripreso il file spec.md.
La svolta: smettere di chiedere codice, iniziare a chiedere domande
Martedì 7 luglio, 9:00. Cambio metodo. L’agente non scrive più codice. L’agente diventa un revisore di specifica: per ogni sezione del documento, gli chiedo di smontarla, di trovarmi i casi d’angolo che ho ignorato, di simulare scenari di failure, di proporre invarianti che ho dimenticato di scrivere.
Il dialogo nei primi giorni è stato qualcosa del genere. Io: “il tenant è pinnato a un nodo per tutta la sessione”. L’agente: “cosa succede se il nodo pinnato cade? quanto tempo aspetti prima di failover? il failover deve cambiare l’affinità o resta? cosa succede ai job di queue già in volo per quel tenant?”. Io aggiungo le risposte alla specifica. L’agente: “ok, e cosa succede se due richieste HTTP per lo stesso tenant arrivano in parallelo subito dopo il failover? c’è una corsa fra le due, e su quale nodo arrivano le scritture in quella finestra?”. Io aggiungo. E così via.
Quattro settimane di questo dialogo. Non lineari: due ore al giorno alcuni giorni, otto altri giorni, qualche giornata zero perché ero su altri temi. Ma sempre tornando al file. Ogni sezione che chiudevo, l’agente la riapriva con una domanda nuova che non avevo previsto. Ogni risposta diventava una riga in più nella specifica.
Ho dovuto disciplinarmi: non chiudevo l’agente quando non sapevo rispondere a una sua domanda. Mi fermavo, andavo a guardare la documentazione di MariaDB sulla replica asincrona circolare, leggevo il codice di MaxScale per capire il comportamento esatto sotto failover, parlavo con il dev senior che si occupa di infrastruttura per capire come avevamo configurato i timeout. Le risposte tornavano nella specifica. Non era l’agente a “sapere” la risposta, ero io che sapevo cosa cercare grazie alla domanda dell’agente.
Cosa è entrato nella specifica
Il documento finale, alla fine di luglio, è di otto pagine markdown. La struttura, in ordine:
Glossario. Tre pagine di termini definiti in modo univoco: tenant, scope, affinità di sessione, finestra di replica, connection resolver, sessione di scrittura, lettura “read-your-writes”, lettura “consistente al meglio”, e così via. Sembra banale ma è la sezione più importante: due settimane fa “sessione” significava cose diverse a seconda di chi parlava (sessione HTTP, sessione DB, sessione utente). Adesso ha un significato unico nel documento.
Invarianti. Otto invarianti numerate, ognuna con esempio positivo e negativo. Esempio: “una scrittura tenant-aware completata con commit atomico al tempo T sul nodo primario di affinità deve essere visibile a tutte le letture tenant-aware con tenant_id corrispondente effettuate al tempo T+epsilon, dove epsilon non eccede la finestra massima di failover”. Esempio positivo, esempio negativo, ragionamento sul perché l’invariante non è negoziabile.
Casi d’angolo enumerati. Quattordici casi, ognuno con descrizione, comportamento atteso, e test fixture suggerita. I primi quattro sono ovvi (failover singolo, doppio failover, partizione di rete temporanea, scrittura simultanea da HTTP e queue). Gli altri dieci sono quelli che l’agente ha tirato fuori chiedendo, e che da solo non avrei messo: scrittura in transazione che si committa nel mezzo di un failover, job di queue che inizia su un nodo e finisce su un altro, sessione utente in-flight durante un cambio di affinità tenant, e così via.
Contratto API interno. Definizione delle interfacce esposte: TenantContext, TenantAffinityResolver, ConsistentReadGuard, TenantScope. Per ognuna, signature, contratto, errori possibili, esempio di uso.
Risposte attese a errori. Non solo happy path. Cosa fa il sistema quando il nodo pinnato non risponde, quando la cache di affinità è disallineata rispetto alla persistenza, quando un job di queue trova il tenant rimosso, quando il TenantContext non è popolato in un job. Ogni errore ha una risposta deterministica e una metrica di telemetria associata.
Telemetria minima. Sei metriche da esporre in Grafana per monitorare il comportamento dello scope in produzione: latenza di affinità, frequenza di failover, drift di replica per tenant, hit rate della cache di affinità, errori di consistency rilevati, profondità delle code di Horizon per tenant.
Piano di rollout. Prima fase a feature flag con un solo tenant interno, seconda fase su tre tenant a basso traffico con monitoring intensivo, terza fase su tutto il fleet con lookback di una settimana.
Otto pagine. Nessuna riga di codice nel file.
L’implementazione finale: 1.300 righe in tre giorni
Lunedì 27 luglio, ore 9:00. La specifica è stabile da quattro giorni, l’agente ha chiuso le sue ultime domande senza trovare buchi. Apro un nuovo branch sul repo dell’Application Service, gli passo il file spec.md, e gli do il via libera.
Tre giorni. Mercoledì 29 luglio sera ho 1.300 righe di codice nuovo: TenantScope riscritto, TenantAffinityResolver con persistenza in tabella dedicata e cache di lettura, ConsistentReadGuard per le rotte critiche, middleware HTTP aggiornato, payload di queue arricchito, una manciata di event listener per il failover. Più 70 test, di cui 40 che coprono direttamente i casi d’angolo enumerati nella specifica.
Il mio tempo umano sopra il codice è stato di sei ore di review architetturale distribuite in tre giorni. Non ho riletto le 1.300 righe riga per riga: ho confrontato il diff aggregato con la specifica e mi sono assicurato che ogni invariante avesse il suo test, che ogni caso d’angolo avesse la sua copertura, che la telemetria fosse esposta correttamente. Ho aperto i singoli file solo per il TenantAffinityResolver (cuore del modulo) e per il middleware HTTP. Tre file, in totale.
Il rollout è iniziato venerdì 31 luglio sul tenant interno. Sta girando da poche ore mentre scrivo. La fase due la faremo a metà agosto, la fase tre a fine settembre. Aggiornerò il post se qualcosa va storto, ma ho una buona impressione: gli invariants sono coperti, gli edge case enumerati, la telemetria espone il comportamento al primo sguardo.
La regola che ho introdotto
In Romiltec adesso vale questa regola, scritta nel runbook interno e ripetuta a ogni dev senior: la specifica è il vero artefatto, il codice è il prodotto della specifica passata attraverso il modello del momento.
Detto in pratica: se un giorno cambiamo coding agent, riscriviamo il codice (l’agente nuovo lo rigenererà dalla specifica). La specifica resta, e con la specifica restano gli invariants, i casi d’angolo, i contratti, la telemetria. Il valore intellettuale che abbiamo prodotto in quattro settimane di luglio non è il codice del TenantAffinityResolver: è la conoscenza, scritta in italiano e in markdown, di come si fa lo scoping tenant-aware sopra un cluster MariaDB con replica circolare quando vuoi aprire il read-write split. Quel sapere lo proteggiamo nel file spec.md versionato sul repo runbook interno, non nel codice Laravel del modulo.
Ne segue una conseguenza che nel 2024 non avrei creduto: il diff di codice che gli agenti aprono non è più la mia preoccupazione principale come responsabile tecnico. La mia preoccupazione principale è il diff della specifica. Quando vedo aprirsi una pull request sul file spec.md la leggo riga per riga, perché ogni riga aggiunta o cambiata può alterare il significato di un invariante o introdurre un caso d’angolo. Quando vedo aprirsi una pull request sul codice generato dalla specifica, faccio review architetturale e mi fido che la specifica sia la fonte di verità che ha guidato l’agente.
Cosa porto a casa
Tre cose, in ordine.
La prima: scrivere specifiche è il mestiere del responsabile tecnico nel 2026. Lo è in modo molto più letterale che nel 2014. Nel 2014 la specifica era un design doc che leggevamo all’inizio del progetto e poi lasciavamo invecchiare; il codice diventava la fonte di verità dopo due settimane. Nel 2026 la specifica è il documento operativo che leggi tu e l’agente ogni volta che si tocca il modulo, e che riscrivi quando le condizioni cambiano. Il codice diventa derivata, non sorgente.
La seconda: i coding agent ricompensano la disciplina della specifica come nessun strumento prima. Negli anni delle review classiche, un design doc fatto bene faceva risparmiare ore di discussione. Adesso fa risparmiare giorni di codice rifatto. Il rapporto valore/effort sulla specifica è cresciuto di un ordine di grandezza, perché l’agente non si lamenta di una specifica troppo dettagliata, anzi: più è dettagliata, meglio lavora.
La terza: la qualità della specifica è proporzionale alla qualità delle domande che riesci a farti su di essa. Nelle quattro settimane di luglio, le domande migliori non le ho fatte io, le ha fatte l’agente. Non perché lui “sapesse” la risposta giusta, ma perché era pago di smontare la mia specifica e cercare i buchi. Quel ruolo, il revisore di specifica che non ha fretta e non si stanca, è un valore enorme. È il primo uso di un coding agent che mi ha fatto sentire di avere un collaboratore tecnico in più, non solo un esecutore più veloce.
Venerdì 31 luglio 2026, 17:30. Il modulo è in produzione su un tenant interno, la specifica è chiusa nella sua versione 1.0 sul repo runbook, il branch del codice è merged. Apro un nuovo file: spec.md. Riga 1, intestazione: # Pipeline di stilometria: refactor del consumer Horizon. Riga 2, vuota. Riga 3, primo paragrafo. Si ricomincia. Il vero codice di Romiltec, oggi, si scrive così.
