Concepts
Architecture overview
Vertical slice architecture, the .NET projects and the patterns that divide them, the React SPA, and how a feature is added across both sides.
On this page
Vertical slices, in one minute
Most codebases are organised by layer: a controllers folder, a services folder, a repositories folder, a models folder. A single feature is then smeared across all of them, so changing one behaviour means editing four places and reading code that serves a hundred unrelated features.
Vertical slice architecture turns that ninety degrees. Code is organised by feature, not by technical layer. Everything one use case needs (its request, its logic, its validation, its result) lives together in one folder. The slice you reason about as a human is the slice in the code, so a feature is something you can read, change or delete in one place.
Organised by layer
One feature cuts through every layer. To change it you touch four places.
Organised by feature
One feature is one column. Read it, change it or delete it in one place.
The benefits are practical: less coupling between unrelated features, changes that stay local, an obvious place for every piece of code, and a shape a new engineer (or an AI assistant) can learn once and apply everywhere. For the deeper rationale, Jimmy Bogard’s Vertical Slice Architecture is the canonical write-up.
Slicekit applies this shape on both sides of the wire: the .NET API is slice-per-feature, and so is the React SPA.
The API, project by project
The API is vertical slice + domain-driven design + event-driven CQRS on EF Core and PostgreSQL.
It is deliberately not a clean-architecture onion of many layered projects, and there is no
repository abstraction: the AppDbContext is the unit of work and the repository.
The whole API is two source projects, plus four test projects that guard them.
Slicekit.Api
the host: endpoints, middleware, composition root
Slicekit.Core no dependency on the web host
Features/
vertical slices: command, handler, validator, result
Domain/
aggregates that raise events, primitives
Persistence/
AppDbContext, EF configurations, migrations
Messaging
Wolverine CQRS, transactional outbox
Four test projects guard it
Unit.Tests
logic in isolation
Architecture.Tests
boundaries hold
Feature.Tests
real Postgres
Api.Tests
HTTP end to end
Slicekit.Apiis the host. It owns HTTP: endpoints, middleware, the composition root, OpenAPI. Endpoints are thin adapters that map a route, its policies (authorization, validation, rate limiting, CSRF) and its status codes.Slicekit.Coreholds everything else: the feature slices underFeatures/, the domain underDomain/, persistence underPersistence/, and the messaging configuration. It has no dependency on the web host.
That one-way dependency is the point. Because handlers are invoked over Wolverine’s message bus rather than called from controllers, the API is just one possible host. A CLI tool, a background worker or a scheduled job can dispatch the exact same commands without touching any HTTP code.
Two domain rules are worth stating up front, because they keep the model honest:
- Aggregates raise events. Business rules live in aggregates, which enforce their own invariants and raise a domain event to record what happened. Raising is a fact inside the domain; it sends nothing.
- Dispatchers publish events. After the change commits, raised events are published to interested handlers and to the transactional outbox.
How commands, queries, events and the outbox fit together is its own page: CQRS and domain events.
React SPA
TanStack · typed client
.NET 10 API
vertical slices · CQRS
PostgreSQL
EF Core
RabbitMQ
outbox · events
Grafana
traces · logs · metrics
At runtime the picture is simple: the SPA talks to the API through one typed client, and the API owns persistence, messaging and telemetry.
The frontend
The SPA mirrors the API’s slice-per-feature shape. Each feature is a folder under features/ that
owns its typed client calls, its TanStack Query hooks, its localized Zod schemas and its components.
Shared primitives (the UI kit, the API client, theme handling) live in components/ and lib/;
anything feature-specific stays inside the feature.
api/ · Features/ApiKeys/CreateApiKey/
- Command.cs the request shape
- Handler.cs domain logic
- Validator.cs input rules
- Result.cs the response shape
frontend/ · features/api-keys/
- api.ts typed client call
- hooks.ts query + mutation
- schemas.ts zod, localized
- components/ the UI
It is a Vite + React 19 app in strict TypeScript, with TanStack Router for type-safe routing, TanStack Query for server state, and shadcn/ui on Tailwind. Every request goes through one typed client that handles cookie sessions, the CSRF header and a silent refresh-and-retry on 401. See the frontend overview and the typed API client.
Adding a feature, end to end
Because both sides share the slice shape, one feature is one slice through the whole system. The path is always the same:
- Write the slice in
Slicekit.Core. A new folder underFeatures/with the command (a plain record), the handler (the logic, returning aResult<T>), validation, and the result type. The handler loads an aggregate, calls a domain method, and the aggregate raises its event. - Map a thin endpoint in
Slicekit.Api. Declare the route, its permission, rate limit, validation and CSRF as policy, then hand the command to the Wolverine bus. No logic here. - Mirror the slice on the frontend. A folder under
features/with the typedapi.tscall, thehooks.tsquery and mutation, localizedschemas.ts, and the components that render it. - Lean on the guardrails. Architecture tests fail the build if a slice reaches across a feature boundary, and feature tests run against a real PostgreSQL via Testcontainers.
The full step-by-step lives in the two recipes: adding a vertical slice on the API, and the matching frontend recipe in the docs.
Proven, not experimental
Slicekit runs on mainstream technology with years (some decades) behind it: PostgreSQL, Redis, RabbitMQ, relational data, CQRS, domain-driven design, REST and JWT, on the current stable releases of .NET and React. The patterns are battle-tested and the dependencies are ones you can hire for and reason about. There is no exotic runtime and nothing bleeding-edge that breaks on the next upgrade: modern where it helps you, boring where it protects you.
What is deliberately absent
- No clean-architecture onion. Two source projects, not a stack of layered ones.
- No repository pattern. The
AppDbContextis the unit of work and the repository. - No data-migration shims. Slicekit is a template with no existing data to bridge.
- No bespoke runtime. Everything is standard .NET, EF Core and Wolverine.