Skip to content

Backend Architecture ​

Vulcan's backend consists of independent .NET 10 microservices using Vertical Slice Architecture with CQRS pattern.

Technology Stack ​

TechnologyPurpose
.NET 10Runtime
Minimal APIsHTTP endpoints
Mediator 3.0.1CQRS dispatcher
Entity Framework Core 10ORM
PostgreSQLDatabase
FluentValidationRequest validation
Swagger/OpenAPIAPI documentation
SerilogLogging
xUnitTesting

Capability Structure ​

Each capability (microservice) follows the same structure:

vulcan-be-leads/
├── src/
│   └── Vulcan.Leads/
│       ├── Abstractions/       # Base classes (MasterEntity)
│       ├── Database/           # AppDbContext, interceptors
│       ├── Entities/           # Domain entities
│       ├── Extensions/         # Endpoint mappings, DI
│       ├── Features/           # Vertical slices
│       │   └── Leads/
│       │       ├── GetAllLeads/
│       │       ├── GetLead/
│       │       ├── CreateLead/
│       │       ├── UpdateLead/
│       │       └── DeleteLead/
│       ├── HealthChecks/       # Custom health checks
│       ├── Middleware/         # Custom middleware
│       ├── Services/           # Application services
│       └── Validators/         # Shared validators
├── tests/
│   └── Vulcan.Leads.Tests/
├── Dockerfile
└── .gitlab-ci.yml

Vertical Slice Pattern ​

Each feature contains all its CRUD operations as self-contained slices.

Query Example (Read) ​

csharp
// Features/Leads/GetLead/GetLeadQuery.cs
public record GetLeadQuery(Guid Id) : IRequest<LeadResponse?>;

// Features/Leads/GetLead/GetLeadHandler.cs
public class GetLeadHandler : IRequestHandler<GetLeadQuery, LeadResponse?>
{
    private readonly AppDbContext _context;

    public GetLeadHandler(AppDbContext context)
    {
        _context = context;
    }

    public async ValueTask<LeadResponse?> Handle(
        GetLeadQuery request,
        CancellationToken cancellationToken)
    {
        var lead = await _context.Leads
            .FirstOrDefaultAsync(x => x.Id == request.Id, cancellationToken);

        return lead?.ToResponse();
    }
}

// Features/Leads/GetLead/LeadResponse.cs
public record LeadResponse(
    Guid Id,
    string Name,
    string Email,
    LeadStatus Status,
    DateTime CreatedAt
);

Command Example (Write) ​

csharp
// Features/Leads/CreateLead/CreateLeadCommand.cs
public record CreateLeadCommand(
    string Name,
    string Email,
    string? Phone
) : IRequest<CreateLeadResponse>;

// Features/Leads/CreateLead/CreateLeadHandler.cs
public class CreateLeadHandler : IRequestHandler<CreateLeadCommand, CreateLeadResponse>
{
    private readonly AppDbContext _context;

    public CreateLeadHandler(AppDbContext context)
    {
        _context = context;
    }

    public async ValueTask<CreateLeadResponse> Handle(
        CreateLeadCommand request,
        CancellationToken cancellationToken)
    {
        var lead = new Lead
        {
            Name = request.Name,
            Email = request.Email,
            Phone = request.Phone,
            Status = LeadStatus.New
        };

        _context.Leads.Add(lead);
        await _context.SaveChangesAsync(cancellationToken);

        return new CreateLeadResponse(lead.Id);
    }
}

// Features/Leads/CreateLead/CreateLeadValidator.cs
public class CreateLeadValidator : AbstractValidator<CreateLeadCommand>
{
    public CreateLeadValidator()
    {
        RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
        RuleFor(x => x.Email).NotEmpty().EmailAddress();
        RuleFor(x => x.Phone).MaximumLength(20);
    }
}

Minimal API Endpoints ​

csharp
// Extensions/LeadEndpoints.cs
public static class LeadEndpoints
{
    public static void MapLeadEndpoints(this IEndpointRouteBuilder app)
    {
        var group = app.MapGroup("/api/leads")
            .WithTags("Leads")
            .RequireAuthorization();

        group.MapGet("/", async (IMediator mediator) =>
            await mediator.Send(new GetAllLeadsQuery()));

        group.MapGet("/{id:guid}", async (Guid id, IMediator mediator) =>
            await mediator.Send(new GetLeadQuery(id)));

        group.MapPost("/", async (CreateLeadCommand cmd, IMediator mediator) =>
            await mediator.Send(cmd));

        group.MapPut("/{id:guid}", async (Guid id, UpdateLeadCommand cmd, IMediator mediator) =>
            await mediator.Send(cmd with { Id = id }));

        group.MapDelete("/{id:guid}", async (Guid id, IMediator mediator) =>
            await mediator.Send(new DeleteLeadCommand(id)));
    }
}

Program.cs Setup ​

csharp
var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddMediator();
builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure pipeline
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();

// Map endpoints
app.MapLeadEndpoints();
app.MapHealthChecks("/health");

app.Run();

Naming Conventions ​

TypePatternExample
Commands<Action><Feature>CommandCreateLeadCommand
Queries<Action><Feature>QueryGetLeadQuery
Handlers<Action><Feature>HandlerCreateLeadHandler
Responses<Feature>ResponseLeadResponse
Validators<Action><Feature>ValidatorCreateLeadValidator

Database Conventions ​

TypeConventionExample
Tablessnake_case pluralleads, work_packages
Columnssnake_casefirst_name, created_at
Primary Keysidid
Foreign Keys[entity]_idcustomer_id
Indexesix_[table]_[columns]ix_leads_email

Commands ​

bash
dotnet run                           # Start service
dotnet build                         # Build
dotnet test                          # Run tests
dotnet ef migrations add <Name>      # Create migration
dotnet ef database update            # Apply migrations

Gotchas ​

Important

  • Use ValueTask<T> not Task<T> for Mediator handlers
  • All endpoints require authorization unless [AllowAnonymous]
  • One database per capability - never share databases
  • Validators are colocated with Commands in feature folders

Built with VitePress | v1.2.0 | 🚀 Week One Sprint