Skip to content
Slicekit
All posts
· Slicekit Team

Vertical slices without the cargo cult

Vertical slices are a cohesion rule, not a war on Clean Architecture or DDD. The myths, the one principle that matters, and where slices actually bite.

Spend an afternoon reading about Vertical Slice Architecture and you will come away thinking it is a faction in a war: slices versus Clean Architecture, slices versus DDD, slices versus anyone who ever wrote an interface. Pick a side, delete your Services folder, and ship features that share nothing with their neighbours.

Most of that is cargo cult. The ritual (folders named after features, no shared code, a table per slice) gets copied without the judgement that made it work in the first place, and the result is a codebase that is just as hard to change as the layered one it replaced, only now the mess is distributed. Before you buy a template that organises code this way, it is worth being precise about what the idea actually claims, and what it does not.

The one principle, and it is a cohesion rule

Jimmy Bogard, who coined the term, states it in a single sentence: “minimize coupling between slices, and maximize coupling in a slice” (Vertical Slice Architecture). That is the whole thing. It is a statement about cohesion, about what belongs together, and nothing else. A slice owns its request, its handler, its validation and its result, and the code that changes together lives together.

Crucially, the principle says nothing about how a slice fulfils its request. Bogard’s own framing is that each slice is free to choose: a trivial read can be a raw query, a CRUD write can be a transaction script, and a slice with real rules can reach for the full DDD machinery. The architecture does not mandate uniformity across slices. It mandates that whatever a slice does, it does in one place.

Organised by layer

Controllers
Services
Repositories
Models

One feature cuts through every layer. To change it you touch four places.

Organised by feature

CreateApiKey
Login
ExportData

One feature is one column. Read it, change it or delete it in one place.

Slicekit organises by feature, not by layer, so the slice you reason about is the slice in the code.

This is also why the “slices versus Clean Architecture” framing is confused. The two are orthogonal. VSA is about cohesion (what lives together); Clean Architecture is about dependency direction (what is allowed to depend on what). You can have both at once, and Slicekit does (practical VSA). They answer different questions and compose cleanly.

The myths Slicekit ignores on purpose

Three claims get attached to VSA that are not in the definition, and each one, taken literally, makes a codebase worse (VSA myths).

“No shared code, zero abstractions.” The principle minimises coupling between slices; it does not ban shared infrastructure. Slicekit slices share AppDbContext, the Result<T> error taxonomy, the endpoint filters for validation and CSRF, and the whole domain model. What they do not share is a BaseService that twelve features inherit from, because that is the coupling the principle is actually warning about. Sharing stable infrastructure is fine. Sharing a god-class that every feature can change out from under every other is the trap.

“One database table per slice.” This conflates code layout with data layout, and they are unrelated. In Slicekit, User is a single aggregate spanning permissions, consents, API keys and refresh tokens, and many slices read and write that one aggregate. The slice boundary is about which code changes together, not about carving the schema into per-feature shards.

“VSA replaces Clean Architecture or DDD.” This is the semantic diffusion Oskar Dudycz writes about: a sharp idea gets stretched until it means “the way I structure code now,” and the original content leaks out (semantic diffusion). Slicekit pairs slices with a DDD core; it does not use slices as a reason to skip the domain model. The slice (command, handler, validator, result) lives in Slicekit.Core/Features/. The aggregates with the invariants live in Slicekit.Core/Domain/. The handler is thin precisely because the aggregate is not: it loads the aggregate, calls one named method, and saves. The decision of whether a change is even allowed lives inside the model, not smeared into the slice.

Mirroring the slice, without overreaching

A feature is not only its server half, so Slicekit gives the frontend its own features/ folder with the same shape: the typed client call, the TanStack Query hooks, the localized Zod schemas and the UI in one place. The symmetry is the same cohesion rule applied on both sides of the wire, not a separate doctrine.

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
Learn one slice and you can navigate every slice; the layout is the same on either side.

Notice what this is not. It is not a rule that the frontend slice may share nothing with other frontend slices, and it is not a claim that mirroring the boundary makes the two halves a single deployable unit. It is the modest, useful version: pick up a ticket, open two folders, and the feature is genuinely one thing rather than two scattered piles.

Where slices bite, and the discipline they demand

Here is the part the marketing copy skips. The honest defence of duplication is that “duplication is cheaper than the wrong abstraction”: two slices doing similar work may carry similar code on purpose, because a premature shared base class couples them and a little repetition does not. That defence is real. But it is not free, and the cost is discipline.

Without refactoring maturity on the team, slices rot in a specific way. The duplication that was supposed to be temporary calcifies. The three almost-identical handlers drift apart, each patched independently, until “the same feature” behaves three subtly different ways and nobody dares unify them. VSA does not save you from this. It hands you the rope and trusts you to extract the genuinely shared concept deliberately, into something a slice depends on, rather than inheriting it by default or copy-pasting it forever.

This bites hardest exactly where you would hope it would help: when many features share complex domain rules. If the rules are genuinely common, scattering them across slices in the name of independence is how you get divergence and drift. That is the case Slicekit answers by not making slices carry the rules at all. The complex, shared invariants live once in the aggregate; the slices stay thin and stay independent. The cohesion rule governs the application layer, and the domain model absorbs the shared complexity. That division is the judgement the cargo cult leaves out, and it is the part worth paying for.

Read the vertical slices guide for the full recipe, including how a command flows through Wolverine and how a slice leans on the domain model. For what happens once a handler raises an event, see commands, events and the outbox.