Vertical slices
We strongly recommend organizing a Cratis application by feature, not by technical layer. Nothing in the framework enforces this — Arc discovers your commands and read models wherever they live — but we recommend it emphatically, because high cohesion and low coupling fall out of the shape almost for free. Everything for one behavior — the command that writes state, the read model and query that serve it, the React component that renders it, and the specs that prove it — lives together in a single folder. That folder is a vertical slice, and the slice folders inside a feature map directly onto the timeline an event model lays out.
This page explains why. If you want to see one built, the capstone walkthrough does exactly that.
The pain it removes
Section titled “The pain it removes”In a layer-organized codebase, one feature is smeared across the tree:
❌ Organized by layer — "register author" touches five foldersCommands/RegisterAuthor.csHandlers/RegisterAuthorHandler.csDtos/AuthorDto.csClients/AuthorsClient.tsweb/src/features/authors/AddAuthor.tsxTo change how authors register, you open five folders, hold the whole layer cake in your head, and hope you didn’t miss a place. New contributors have to learn every layer before they can touch one feature.
A vertical slice puts the whole feature in one place:
✅ Organized by feature — one folder, read top to bottomAuthors/Registration/├── Registration.cs # command + Handle() + read model + query├── AddAuthor.tsx # the React screen└── when_registering/ # the specsYou navigate by what the software does, not by what kind of file it is.
Why the frontend and backend belong together
Section titled “Why the frontend and backend belong together”This is the part that surprises people coming from a layered or separate-repo world. Cratis deliberately keeps the C# backend and the React frontend of a feature side by side. Three reasons:
-
A feature is one thing, and it spans the stack. “Register an author” is not a backend task and a frontend task — it’s one behavior with a write side and a read side. The command, the query, and the form are different facets of the same change. They are conceived together, they change together, and a bug usually lives in the handoff between them. Keeping them adjacent means one mental context and, often, one pull request.
-
Full-stack type safety binds them anyway. Arc generates the TypeScript proxies for a command or query from your C# types. The frontend literally depends on the backend’s shapes — so when you rename a property in the command, the React code stops compiling until you fix it. Co-locating makes that feedback loop tight: the thing that broke is right next to the thing that changed, and the compiler points you straight at it. Separating them across folders or repos hides that relationship without removing it.
-
High cohesion, low coupling — by construction. Everything a feature needs is in its folder (cohesion); features don’t reach into each other’s internals, they communicate through commands, queries, and shared contracts (low coupling). Layering inverts this: it scatters one feature (low cohesion) and tempts every feature to share the same fat “service” or “models” layer (high coupling). Slices keep changes local, so you can build, change, and even delete a feature without ripping through unrelated code.
None of this dictates your repository layout, though. The proxy generator writes the TypeScript wherever you point it — the CratisProxiesOutputPath MSBuild property in your backend project sets the output folder, which can just as well be a separate frontend folder or another checkout entirely. If your team keeps backend and frontend apart, Arc follows your structure and the type safety holds across the distance. We co-locate because the feedback loop in point 2 is at its tightest when the generated proxy sits next to the command it mirrors — a strong preference, not a requirement.
What lives in a slice
Section titled “What lives in a slice”A single backend .cs file can hold the backend artifacts for the behavior — the [Command] with its Handle(), validators, and the [ReadModel] with its query methods. Alongside it sit the React component(s) that consume the generated proxies, and the specs. Arc’s convention-based discovery means none of it needs registration or wiring — the slice folder is self-contained and reads top to bottom.
Slice types
Section titled “Slice types”Not every slice does the same job. There are four kinds — the same building blocks an event model lays out on its timeline:
| Type | Purpose |
|---|---|
| State change | Mutates state — a command that writes to MongoDB, EF Core, or an optional event store. |
| State view | Serves current state through a queryable read model. |
| Automation | Runs side effects after a command or, with Chronicle, reacts to stored events. |
| Translation | Adapts input from one slice/system by triggering a command in another. |
The one nuance
Section titled “The one nuance”Co-location is about a behavior, not duplication. Things genuinely shared between slices — a concept like AuthorId, a shared read model contract, or a domain fact used by more than one feature — move up to the feature root or a shared location, not copied into every slice. The rule of thumb: if it belongs to one behavior, it lives in that slice; if it’s truly shared, lift it just high enough to be shared and no higher.
See also
Section titled “See also”- Build a full-stack feature — a slice built end to end.
- Why Arc — the framework that makes slices low-ceremony.
- MediatR, MVC, and Arc — how this differs from layered controllers/handlers.