Guide

Come strutturare un’applicazione ASP.NET Core in crescita: dal monolite a strati ai vertical slice

Dario Fadda Aprile 29, 2026

Quando un’applicazione ASP.NET Core è piccola, quasi qualsiasi struttura di cartelle funziona. Controller in una cartella, servizi in un’altra, repository da qualche altra parte. Per un po’ va bene. Poi l’applicazione cresce, le funzionalità si moltiplicano e le regole di business si diffondono per tutta la codebase. Ogni modifica tocca cinque o sei file in posti diversi. I nuovi sviluppatori hanno bisogno di una guida turistica solo per capire dove si trova qualcosa.

Quel momento è il punto in cui la struttura smette di essere una scelta cosmetica e diventa un problema di manutenibilità. In questo articolo esaminiamo le opzioni più comuni — Feature Folders, architettura a strati, Clean Architecture, Vertical Slices e Modular Monolith — con un’ottica pratica su quando e perché usarle.

L’obiettivo reale non è l'”architettura perfetta”

Prima di confrontare i pattern, è utile chiarire l’obiettivo. Non si tratta di rendere il progetto architetturalmente impressionante. Si tratta di rendere più facile:

  • capire dove appartiene il codice
  • modificare una funzionalità senza rompere funzionalità non correlate
  • inserire nuovi sviluppatori più velocemente
  • testare il comportamento importante con meno attrito
  • far evolvere la struttura man mano che il sistema cresce

La risposta giusta è solitamente quella che crea meno confusione per i prossimi 12-24 mesi di sviluppo, non quella che vince i dibattiti architetturali.

La testabilità è una questione di architettura

Uno dei controlli pratici più importanti è questo: possiamo verificare il comportamento di business importante con unit test veloci, senza avviare l’intera applicazione o un database reale? Se la risposta è no, l’attrito architetturale si manifesta come feedback lento, modifiche fragili e paura di fare refactoring.

Una buona struttura migliora la testabilità rendendo esplicite le dipendenze e mantenendo le regole di business lontane dai dettagli del framework e dell’infrastruttura — cose come accesso al database, gestione HTTP, file system e chiamate a servizi esterni. Una regola pratica:

  • Unit test per le decisioni di business e gli invarianti del dominio
  • Integration test per database, messaging e wiring HTTP
  • End-to-end test per i percorsi utente critici

Feature Folders

I Feature Folders organizzano il codice per capacità di business invece che per tipo tecnico. Invece della struttura classica orizzontale:

Controllers/
Services/
Repositories/
Models/


si passa a una struttura verticale per funzionalità:

Features/
  Orders/
    Create/
      CreateOrderEndpoint.cs
      CreateOrderRequest.cs
      CreateOrderHandler.cs
    GetById/
      GetOrderByIdEndpoint.cs
      GetOrderByIdHandler.cs
  Products/
    List/
      ListProductsEndpoint.cs
      ListProductsHandler.cs


Il principio guida è semplice: se devi modificare la funzionalità “Orders”, la maggior parte del codice che ti serve dovrebbe trovarsi da qualche parte sotto la cartella Orders. Questo riduce drasticamente il tempo di ricerca e la probabilità di modifiche accidentali a funzionalità non correlate.

Adatto quando: l’applicazione sta crescendo oltre il CRUD basilare, il team vuole una chiara ownership per funzionalità, gli sviluppatori sono stanchi di saltare tra strati orizzontali per fare una piccola modifica.

Attenzione: se applicati in modo disordinato, i Feature Folders possono diventare inconsistenti e trasformarsi in “un’altra convenzione di cartelle”.

Architettura a strati (Layered Architecture)

L’architettura a strati è la classica separazione in UI, logica di business e accesso ai dati:

Web/
Application/
Domain/
Infrastructure/


Esiste da decenni proprio perché è facile da spiegare, facile da insegnare e fornisce una separazione delle responsabilità immediata. Per i team che vengono da tutorial e applicazioni di esempio, è spesso il punto di partenza più familiare.

Un dettaglio pratico per .NET moderno: non è sempre necessario un layer repository separato, soprattutto se Entity Framework Core fornisce già l’astrazione necessaria per l’accesso ai dati semplice. Creare repository per “rispettare la struttura” piuttosto che per risolvere un problema reale è una delle trappole comuni.

Adatto quando: il team è relativamente piccolo, l’applicazione non è ancora molto complessa, gli sviluppatori traggono vantaggio da una struttura familiare, la codebase è principalmente transazionale e CRUD-oriented.

Attenzione: una modifica a una funzionalità richiede spesso modifiche su più strati. La logica di business può frammentarsi. Gli sviluppatori iniziano a creare astrazioni perché la struttura le richiede, non perché il problema ne ha bisogno.

Clean Architecture

Clean Architecture pone forte enfasi sui confini tra logica di dominio e dettagli dell’infrastruttura. Il principio centrale è valido: le regole di business non dovrebbero essere strettamente accoppiate a database, web framework, message broker o SDK esterni.

In pratica, però, alcuni team spingono Clean Architecture così lontano che ogni caso d’uso viene sepolto sotto strati di interfacce, wrapper, handler, repository e adattatori che il sistema non ha realmente bisogno. Il takeaway più importante non è il template completo, ma il principio: tieni le regole di business lontane dall’infrastruttura tecnica.

// Esempio: un handler di dominio che NON dipende da EF Core direttamente
public class CreateOrderHandler
{
    private readonly IOrderRepository _repository;  // astrazione, non EF diretto
    private readonly IEventPublisher _events;

    public CreateOrderHandler(IOrderRepository repository, IEventPublisher events)
    {
        _repository = repository;
        _events = events;
    }

    public async Task<OrderId> Handle(CreateOrderCommand command, CancellationToken ct)
    {
        var order = Order.Create(command.CustomerId, command.Items);
        await _repository.SaveAsync(order, ct);
        await _events.PublishAsync(new OrderCreated(order.Id), ct);
        return order.Id;
    }
}


Adatto quando: il dominio ha una complessità significativa, l’applicazione ha una lunga vita prevista, più infrastrutture devono rimanere scambiabili o isolate, il team ha la disciplina per usare i confini intenzionalmente.

Attenzione: è facile over-engineerare. Troppa cerimonia rallenta il lavoro su funzionalità semplici. I team inesperti spesso copiano diagrammi invece di risolvere il vero problema di manutenibilità.

Vertical Slice Architecture

L’architettura a vertical slice organizza il codice attorno a singoli casi d’uso o richieste. Invece di pensare per layer tecnici, ogni “slice” è un percorso verticale completo dalla richiesta alla risposta:

Features/
  PlaceOrder/
    PlaceOrderCommand.cs
    PlaceOrderHandler.cs
    PlaceOrderValidator.cs
    PlaceOrderEndpoint.cs
  CancelOrder/
    CancelOrderCommand.cs
    CancelOrderHandler.cs
    CancelOrderEndpoint.cs


Ogni slice è autonoma e contiene tutto il necessario per gestire quella specifica operazione. Questo riduce l’accoppiamento tra funzionalità diverse: modificare “PlaceOrder” non richiede di toccare il codice di “CancelOrder”.

MediatR è comunemente usato con questo pattern in .NET, ma non è obbligatorio — il pattern funziona anche con endpoint minimali diretti.

Adatto quando: le funzionalità sono relativamente indipendenti tra loro, il team preferisce massimizzare la coesione per caso d’uso, si vuole limitare al minimo l’accoppiamento laterale.

Attenzione: la duplicazione del codice tra slice simili può crescere se non si definisce chiaramente cosa è condiviso e cosa non lo è.

Modular Monolith

Il modular monolith è uno step successivo rispetto ai pattern precedenti: invece di organizzare il codice per funzionalità singole, si definiscono moduli di business più ampi con confini chiari tra loro, pur rimanendo un’unica applicazione deployabile.

Modules/
  Ordering/
    Api/
    Application/
    Domain/
    Infrastructure/
  Catalog/
    Api/
    Application/
    Domain/
    Infrastructure/
  Payments/
    ...


Ogni modulo espone un’interfaccia pubblica e nasconde i propri dettagli interni. La comunicazione tra moduli avviene attraverso quella interfaccia — mai direttamente tra le implementazioni interne. Questo crea i presupposti per un eventuale passaggio a microservizi, se e quando il sistema lo richiederà, senza dover fare un refactoring massiccio.

Adatto quando: il sistema è abbastanza grande da giustificare confini chiari tra aree di business, non si vuole la complessità operativa dei microservizi, si vuole prepararsi a una futura decomposizione senza impegnarsi subito.

Quale scegliere?

Non esiste una risposta universale, ma questo schema può orientare la scelta:

  • App nuova, team piccolo, CRUD dominante → Layered o Feature Folders
  • App in crescita, molte funzionalità indipendenti → Feature Folders o Vertical Slices
  • Dominio complesso, lunga vita prevista, team disciplinato → Clean Architecture
  • Sistema grande, confini di business chiari, no microservizi ancora → Modular Monolith

Inizia dalla struttura più semplice che risolve il tuo problema attuale. Evolvi quando la complessità del sistema lo giustifica, non prima. Il momento migliore per passare a un’architettura più sofisticata è quando il dolore del non averla è reale e misurabile — non anticipatorio.


Fonte: ASP.NET: How to Structure a Growing Application So It Stays Maintainable — Chris Pietschmann, pietschsoft.com.

💬 Unisciti alla discussione!


Questo è un blog del Fediverso: puoi trovare quindi questo articolo ovunque con @blog@spcnet.it e ogni commento/risposta apparirà qui sotto.

Se vuoi commentare su Come strutturare un’applicazione ASP.NET Core in crescita: dal monolite a strati ai vertical slice, utilizza la discussione sul Forum.
Condividi la tua esperienza, confrontati con altri professionisti e approfondisci i dettagli tecnici nel nostro 👉 forum community