Learnixo
Back to blog
AI Systemsintermediate

Vertical Slice Folder Structure — Organizing by Feature

Structure a Vertical Slice Architecture project by feature: co-locating command, handler, validator, and endpoint in one folder, shared kernel placement, and how to scale the structure as features grow.

Asma Hafeez KhanMay 16, 20263 min read
Vertical SliceArchitectureFolder StructureASP.NET Core.NETOrganization
Share:𝕏

The Core Idea

Traditional layered structure (organize by technical concern):
  Controllers/
    PatientController.cs
    PrescriptionController.cs
  Services/
    PatientService.cs
    PrescriptionService.cs
  Repositories/
    PatientRepository.cs
    PrescriptionRepository.cs

Vertical Slice structure (organize by feature):
  Features/
    Patients/
      GetPatient/
        GetPatientQuery.cs
        GetPatientHandler.cs
        GetPatientValidator.cs
        GetPatientEndpoint.cs
      CreatePatient/
        CreatePatientCommand.cs
        CreatePatientHandler.cs
        CreatePatientValidator.cs
        CreatePatientEndpoint.cs
    Prescriptions/
      CreatePrescription/
        CreatePrescriptionCommand.cs
        CreatePrescriptionHandler.cs
        ...

Add a feature → add a folder. Change a feature → touch one folder.

Full Project Structure

src/
  Api/
    Features/
      Patients/
        GetPatient/
          GetPatientQuery.cs
          GetPatientHandler.cs
          GetPatientValidator.cs
          GetPatientResponse.cs
          GetPatientEndpoint.cs
        CreatePatient/
          CreatePatientCommand.cs
          CreatePatientHandler.cs
          CreatePatientValidator.cs
          CreatedPatientResponse.cs
          CreatePatientEndpoint.cs
        DischargePatient/
          DischargePatientCommand.cs
          DischargePatientHandler.cs
          DischargePatientEndpoint.cs
      Prescriptions/
        CreatePrescription/
          ...
        DispensePrescription/
          ...
        GetActivePrescriptions/
          ...
      LabResults/
        SubmitLabResult/
          ...
        GetLabHistory/
          ...
    SharedKernel/
      Behaviors/
        ValidationBehavior.cs
        LoggingBehavior.cs
        PerformanceBehavior.cs
      Exceptions/
        NotFoundException.cs
        ValidationException.cs
      Models/
        Result.cs
        PagedResult.cs
    Infrastructure/
      Persistence/
        ApplicationDbContext.cs
        Configurations/
          PatientConfiguration.cs
          PrescriptionConfiguration.cs
      Messaging/
        ServiceBusPublisher.cs
    Program.cs

Feature Folder Contents

C#
// Features/Patients/GetPatient/GetPatientQuery.cs
public sealed record GetPatientQuery(Guid PatientId) : IRequest<Result<GetPatientResponse>>;

// Features/Patients/GetPatient/GetPatientResponse.cs
public sealed record GetPatientResponse(
    Guid   PatientId,
    string Mrn,
    string FirstName,
    string LastName,
    int    ActivePrescriptionCount);

// Features/Patients/GetPatient/GetPatientValidator.cs
public sealed class GetPatientValidator : AbstractValidator<GetPatientQuery>
{
    public GetPatientValidator()
    {
        RuleFor(q => q.PatientId).NotEmpty();
    }
}

// Features/Patients/GetPatient/GetPatientHandler.cs
public sealed class GetPatientHandler : IRequestHandler<GetPatientQuery, Result<GetPatientResponse>>
{
    private readonly ApplicationDbContext _db;

    public GetPatientHandler(ApplicationDbContext db) => _db = db;

    public async Task<Result<GetPatientResponse>> Handle(
        GetPatientQuery query, CancellationToken ct)
    {
        var response = await _db.Patients
            .Where(p => p.Id == new PatientId(query.PatientId))
            .Select(p => new GetPatientResponse(
                p.Id.Value,
                p.Mrn.Value,
                p.FirstName,
                p.LastName,
                p.Prescriptions.Count(pr => pr.IsActive)))
            .FirstOrDefaultAsync(ct);

        return response is null
            ? Result.Failure<GetPatientResponse>(DomainErrors.Patient.NotFound)
            : Result.Success(response);
    }
}

// Features/Patients/GetPatient/GetPatientEndpoint.cs
public sealed class GetPatientEndpoint : IEndpoint
{
    public void MapEndpoint(IEndpointRouteBuilder app)
    {
        app.MapGet("api/patients/{patientId:guid}",
            async (Guid patientId, ISender sender, CancellationToken ct) =>
            {
                var result = await sender.Send(new GetPatientQuery(patientId), ct);
                return result.IsSuccess ? Results.Ok(result.Value) : Results.NotFound();
            })
            .WithName("GetPatient")
            .RequireAuthorization();
    }
}

Endpoint Registration Pattern

C#
// Scan and register all IEndpoint implementations automatically
public interface IEndpoint
{
    void MapEndpoint(IEndpointRouteBuilder app);
}

// Extension method for Program.cs
public static class EndpointExtensions
{
    public static IServiceCollection AddEndpoints(
        this IServiceCollection services, Assembly assembly)
    {
        services.Scan(scan =>
            scan.FromAssemblies(assembly)
                .AddClasses(c => c.AssignableTo<IEndpoint>())
                .AsImplementedInterfaces()
                .WithScopedLifetime());
        return services;
    }

    public static WebApplication MapEndpoints(this WebApplication app)
    {
        var endpoints = app.Services.GetRequiredService<IEnumerable<IEndpoint>>();
        foreach (var endpoint in endpoints)
            endpoint.MapEndpoint(app);
        return app;
    }
}

// Program.cs
builder.Services.AddEndpoints(typeof(Program).Assembly);
app.MapEndpoints();

SharedKernel: What Goes There

SharedKernel contains things used across multiple features:

Infrastructure shared by features:
  SharedKernel/Behaviors/    → MediatR pipeline behaviors (logging, validation)
  SharedKernel/Models/       → Result, PagedResult, Error type
  SharedKernel/Exceptions/   → NotFoundException, ValidationException

Domain primitives shared by features:
  SharedKernel/Domain/       → base Entity, ValueObject, DomainEvent types

NOT in SharedKernel:
  ✗ Feature-specific logic — it belongs in the feature folder
  ✗ Database context — it is infrastructure
  ✗ Any class used by only one feature — keep it co-located

Production issue I've seen: A team started with Vertical Slice but gradually moved "shared" helpers into a growing SharedKernel. After 6 months, SharedKernel had 40 classes — validators, helpers, formatters, adapters — more than many of the feature folders. It became a dumping ground for anything that felt "reusable." The real test: if a file is only used by one feature, it belongs in that feature's folder, not in SharedKernel. Resist premature extraction.


Key Takeaway

Structure projects by feature, not by technical layer. Each feature folder contains its command/query, handler, validator, response, and endpoint — all co-located. Use an IEndpoint interface scanned at startup to register Minimal API endpoints. SharedKernel contains only cross-feature infrastructure (Result type, MediatR behaviors, base types) — not feature logic. When adding a feature, touch one folder. When changing a feature, touch one folder.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.