Architettura backend · Lavoro del team · Luglio 2026, poi iterato
Trasformare un BFF monolitico in slice possedute dalle feature senza cambiare un contratto
Durante una migrazione incrementale a React, il backend-for-frontend ASP.NET Core era il punto di integrazione tra una web application multi-tenant e la sua API interna — e wiring HTTP, orchestrazione, contratti e infrastruttura di test erano concentrati in pochi grandi file condivisi che ogni sviluppatore doveva toccare. Ho guidato il refactor in endpoint, servizi, contratti e test posseduti dalle feature, protetto da snapshot Swagger a ogni checkpoint: 307 operazioni preservate, zero handler rimasti nella composition root.
- Il mio ruolo
- Architettura, sequenza di estrazione, modello di composizione, controlli di compatibilità e sicurezza del rilascio: miei. La logica delle feature spostate: dei rispettivi autori.
- Stack
- ASP.NET Core
- C#
- Endpoint routing
- Dependency injection
- Swagger / OpenAPI
- NUnit
In sintesi
- File centrale degli endpoint da 5.657 a 81 righe; da 128 registrazioni a 0 handler
- Tutte le 307 operazioni Swagger preservate al cutover su staging
- In esecuzione in testing, staging e produzione entro circa due settimane
Contesto
Il BFF sta tra una web application multi-tenant e la sua API interna ed esponeva già centinaia di operazioni. Wiring HTTP, orchestrazione applicativa, contratti e infrastruttura di test vivevano in pochi grandi file condivisi: il file principale degli endpoint aveva raggiunto 5.657 righe con 128 registrazioni HTTP (133 operazioni verbo-path su 19 tag), un servizio condiviso mescolava domini non correlati, e la registrazione delle dipendenze di produzione e test poteva divergere.
Con più sviluppatori al lavoro contemporaneamente sulle nuove aree React, tutti avevano bisogno delle stesse superfici BFF. La struttura era diventata un collo di bottiglia per merge e coordinamento più che un problema di qualità del codice — e l'obiettivo era rendere ogni feature comprensibile e modificabile in modo indipendente preservando route, payload, status code, autenticazione, comportamento tenant e compatibilità con il frontend.
Cosa ho fatto
- Ho inventariato la superficie HTTP e l'ho congelata in snapshot Swagger/OpenAPI prima di spostare qualsiasi cosa.
- Ho progettato la slice per feature: React / RTK Query → /api/bff → modulo endpoint della feature → servizio della feature → client API downstream user-scoped → contratto della feature → adapter frontend e modello UI.
- Ho introdotto un unico metodo di composizione condiviso per la registrazione delle dipendenze, usato sia dall'host di produzione sia dal test host, così che i due non potessero più divergere; validazione della sessione, contesto tenant e accesso all'API downstream sono rimasti condivisi invece di essere copiati in ogni slice.
- Ho spostato gruppi di endpoint, logica applicativa e DTO in checkpoint controllati, confrontando dopo ogni passo route, operation ID, tag e metadati dei payload con la baseline.
- Ho aggiunto guard su startup, route, operation ID, tag e risoluzione delle dipendenze, più un controllo esplicito di build e freschezza dopo aver scoperto che un assembly compilato stantio poteva far passare falsamente il confronto Swagger.
- Ho scritto materiale di onboarding in Markdown e HTML per chi lavora nella nuova struttura e, più avanti, ho ottimizzato il quality gate locale e le fixture di test condivise quando la prima versione si è rivelata troppo lenta per il lavoro quotidiano sulle pull request.
Decisioni
Estrazione, non riscrittura
La logica esistente delle feature ha mantenuto comportamento e autori. La mia responsabilità erano i confini, la sequenza e la rete di sicurezza, non reimplementare le regole di business.
Prima il contratto
Gli snapshot OpenAPI definivano cosa significasse 'invariato': route, shape dei payload, status code, autenticazione e comportamento tenant dovevano corrispondere alla baseline a ogni checkpoint prima di iniziare il successivo.
Una sola composition root per produzione e test
Lo stesso catalogo di registrazioni alimenta entrambi gli host. Le responsabilità trasversali restano condivise; una slice possiede solo ciò che è specifico della sua feature.
Prima un gate severo, poi renderlo economico
Il primo quality gate proteggeva contratti e test ma chiedeva agli sviluppatori di ricordare passaggi extra, e alcune verifiche locali erano abbastanza lente da interrompere il normale lavoro sulle PR. Conservare la protezione riducendone il costo è stata la seconda metà del lavoro.
La difficoltà concreta
Il refactor doveva procedere mentre gli stessi file venivano modificati da altri sviluppatori per nuove feature, quindi è avanzato in checkpoint abbastanza piccoli da essere mergiati e verificati — mai come un'unica branch di lunga durata.
La scoperta del falso verde è stata importante: un assembly compilato stantio faceva passare il confronto dei contratti anche se il codice era cambiato. Far sì che il guard compilasse e verificasse la freschezza per primo lo ha trasformato da conforto in vera protezione.
Risultato
- Al cutover su staging il file centrale degli endpoint è passato da 5.657 a 81 righe e da 128 registrazioni a zero handler; i file endpoint dedicati sono cresciuti da 18 a 42, le classi service del BFF da 20 a 40 e i file contratto per feature da 18 a 40, con tutte le 307 operazioni Swagger preservate.
- La struttura è arrivata in testing, staging e produzione ed è diventata il modello usato dal team. Non ho osservato regressioni causate dall'estrazione, e entro circa due settimane dall'identificazione del collo di bottiglia la struttura modulare era operativa nei vari ambienti.
- Nuove aree potevano essere sviluppate in parallelo senza concentrare lavori non correlati in un unico file di endpoint, servizio, contratto o test; i percorsi delle richieste sono diventati più facili da seguire, per le persone come per gli strumenti assistiti dall'AI.
- In uno snapshot successivo l'architettura era cresciuta a 92 moduli endpoint, 91 classi service e 93 file contratto, sempre senza handler nella composition root — una crescita che appartiene al lavoro di prodotto di tutto il team, non al solo refactor.
Numeri, con i loro limiti
- File centrale degli endpointPrima5.657 righeDopo81 righesolo quel file; logica redistribuita, non eliminata
- Handler HTTP nella composition rootPrima128 registrazioniDopo0
- File endpoint dedicatiPrima18Dopo42
- Classi service del BFFPrima20Dopo40
- File contratto per featurePrima18Dopo40
- Operazioni SwaggerPrima307Dopo307preservate al cutover su staging
Cosa questi numeri non dicono
- La riduzione delle righe riguarda il file centrale di composizione: codice e complessità totali del BFF sono stati redistribuiti in confini per feature, non tagliati nella stessa proporzione.
- Non sono state conservate metriche indipendenti su frequenza dei merge conflict, lead time, tasso di difetti o ore risparmiate. Il rollout in tutti gli ambienti e l'assenza di regressioni si basano sull'osservazione diretta durante il lavoro.
- I conteggi successivi (92 / 91 / 93) riflettono lo sviluppo continuo del team, e la velocizzazione del quality gate non è stata misurata con un benchmark.
Cosa dimostra
So prendere un collo di bottiglia condiviso in un backend in produzione e ristrutturarlo con la garanzia di preservare i contratti, in checkpoint che il team può assorbire — e poi rendere le protezioni abbastanza economiche da sopravvivere all'uso quotidiano.
Attribuzione
Estrazione, modello di composizione e guard sono lavoro mio, con analisi e documentazione assistite dall'AI sotto la mia revisione. Le feature spostate nelle nuove slice sono state scritte da colleghi e restano loro.