Inside every BitzOrcas.Modern module, Vertical Slice Architecture (VSA) determines the shape. A feature — creating a note, fetching a greeting, sending a chat message — lives in one feature folder: the endpoint, the handler, and the validation rule together. No layered round-trip. No Domain → Application → Infrastructure → API hops. One feature, one folder, one PR.
The slice shape
A slice is split across the module’s projects. The CreateNote sandbox feature looks like this:
BitzOrcas.Application/Sandbox/Notes/├── CreateNoteCommand.cs ← ICommand<Note> (record)├── CreateNoteCommandHandler.cs ← ICommandHandler<CreateNoteCommand, Note>└── GetNoteByIdQuery.cs ← IQuery<Note?> (record)
BitzOrcas.Application/Sandbox/Notes/ (handlers inline or separate)├── CreateNoteCommandHandler.cs ← handler implementation└── GetNoteByIdQueryHandler.cs ← handler implementation
BitzOrcas.Api/Endpoints/Sandbox/├── NoteEndpoints.cs ← Minimal API endpoint mappingA complete slice — CreateNote
The cleanest way to learn VSA is to read one slice end-to-end. Here’s CreateNote from the sandbox module:
public sealed record CreateNoteCommand(string Title, string Content) : ICommand<Result<Note>>;public sealed class CreateNoteCommandHandler : ICommandHandler<CreateNoteCommand, Result<Note>>{ private readonly ISandboxDataPort _dataPort;
public CreateNoteCommandHandler(ISandboxDataPort dataPort) => _dataPort = dataPort;
public async ValueTask<Result<Note>> Handle( CreateNoteCommand command, CancellationToken cancellationToken) { var note = Note.Create(command.Title, command.Content); await _dataPort.AddAsync(note, cancellationToken).ConfigureAwait(false); return Result<Note>.Success(note); }}public static class NoteEndpoints{ internal static RouteHandlerBuilder MapNoteEndpoints(this IEndpointRouteBuilder endpoints) { var group = endpoints.MapGroup("/api/v1/sandbox/notes");
group.MapPost("/", async ( CreateNoteCommand command, IMediator mediator, CancellationToken cancellationToken) => { var result = await mediator.Send(command, cancellationToken); return result.IsFailure ? TypedResults.BadRequest(result.Error) : TypedResults.Created($"/api/v1/sandbox/notes/{result.Value.Id}", result.Value); }) .WithName("CreateNote") .WithSummary("Create a sandbox note");
// GET by ID, GET all, etc.
return group; }}That’s the entire feature. Small files. One slice. One PR.
Result<T> — never throw for business failures
BitzOrcas enforces the Result<T> pattern from BitzOrcas.Domain. Business failures return Result.Failure(Error), never throw exceptions:
// Success pathreturn Result<Note>.Success(note);
// Business failure pathreturn Result<Note>.Failure(new Error( ErrorType.Validation, "Note title cannot be empty"));
// Not-found pathreturn Result<Note>.Failure(Error.NotFound("Note", noteId));The Error type uses {Module}.{Scenario}.{Reason} codes and carries structured details. The endpoint translates Result<T> into appropriate HTTP responses.
Validation — IRequestRule
BitzOrcas uses IRequestRule for validation (not FluentValidation). Rules are attached to commands:
public sealed class CreateGreetingCommandRule : IRequestRule<CreateGreetingCommand>{ public ValueTask<Error?> ValidateAsync( CreateGreetingCommand request, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(request.Message)) return ValueTask.FromResult<Error?>( new Error(ErrorType.Validation, "Message is required."));
if (request.Message.Length > 500) return ValueTask.FromResult<Error?>( new Error(ErrorType.Validation, "Message must not exceed 500 characters."));
return ValueTask.FromResult<Error?>((Error?)null); }}The ValidationPipelineBehavior runs all registered IRequestRule<T> implementations before the handler. Rules are registered in DI alongside the handler.
The Mediator pipeline
Every request passes through the pipeline before reaching the handler:
LoggingPipelineBehavior → timing + exception catch-all → AuthorizationPipelineBehavior → fail-closed authorization → ValidationPipelineBehavior → IRequestRule validation → IdempotencyPipelineBehavior → dedup for IIdempotentRequest → TransactionPipelineBehavior → UoW commit/rollback (Command only) → ActivityAuditPipelineBehavior → activity audit → DomainEventDispatchPipelineBehavior → dispatch domain events → [Handler]See the Mediator Pipeline diagram for a visual flow.
Conventions
- Handlers are
public sealed. Mediator’s source generator requires concrete types. - Handlers return
ValueTask<T>. Allocation-friendly for the synchronous fast path. - Every
awaituses.ConfigureAwait(false). This is library code; never capture the synchronization context. - Commands and queries are
records. Preferred for immutability and equality. - Endpoints are static extension methods on
IEndpointRouteBuilder. No controllers. Just minimal APIs. - One endpoint per slice. A slice is one feature; one feature has one HTTP entrypoint.
- Rules live next to handlers. Auto-registered in DI.
- Permissions live in Contracts. Endpoints reference them via
.RequirePermission(...).
CQRS without ceremony
VSA pairs naturally with CQRS. The distinction is two marker interfaces:
public sealed record CreateNoteCommand(string Title, string Content) : ICommand<Result<Note>>;public sealed record GetNoteByIdQuery(Guid Id) : IQuery<Result<Note>>;The TransactionPipelineBehavior only runs for ICommand messages (not IQuery), so query handlers skip the UoW commit. The IdempotencyPipelineBehavior only processes messages implementing IIdempotentRequest.
What VSA doesn’t do
- It doesn’t replace your domain model. Aggregates still enforce invariants. VSA is about how features are organized.
- It doesn’t enforce database isolation. Each module’s adapters share the same database, with tenant isolation via
TenantEntityBase. - It doesn’t make architecture tests optional. ArchUnitNET enforces namespace boundaries — slices spread the logic, arch tests prevent drift.
Related
- Modular monolith — the outer structure that contains the slices
- Module Overview — every module is structured this way
- Dependency Injection — how the composition root wires slice handlers
- Architecture Diagrams — visual pipeline and layer diagrams