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.