We’ll explore this architecture through a mini ToDo List project in a two-part series.
Article 1:
This article explains what Vertical Slice Architecture and CQRS are, why they complement each other, when to use them (and when not to), includes a simple diagram and high-level request flow, explores real-world use cases, outlines pros and cons, and compares this approach to traditional layered architecture.
Article 2:
The implementation: how the ToDo API is built. You can read it here: Modern Backend Architecture in .NET – Implementing Vertical Slice + CQRS
You’ll see the actual project structure, how Commands and Queries are implemented using MediatR, how FluentValidation integrates into the pipeline, and a step-by-step walkthrough of how a request flows from endpoint → MediatR → handler → database → response.
The Project We're Building
We use one concrete example throughout both articles: a Todo API. The app lets users create, read, update, and delete todo items (each has a title, an optional description, and a completed flag). It’s a small, familiar domain so we can focus on architecture instead of business rules.
Because the whole project is built around this Todo feature, you’ll see the word Todo everywhere: Todo entity, CreateTodo command, GetTodo query, TodoEndpoints, Features/Todos/, and so on. Once you see how Vertical Slice and CQRS work with Todos, you can apply the same ideas to any other feature (e.g., Orders, Invoices, Users).
You can find project here: https://github.com/RikamPalkar/vertical-cqrs-architecture-dotnet/tree/main
1. What is Vertical Slice Architecture?
Vertical Slice Architecture means organizing code by feature (or by “use case”), not by layer.
Traditional (horizontal/layered): You have folders like
Controllers/,Services/,Repositories/,Models/One feature (e.g. “Create Todo”) is spread across many of these folders.
Vertical slice: You have one folder per feature or per action. Everything needed for that action lives in one place: the request (command/query), the handler, the validator, and the endpoint mapping.
In this project, the “Todo” feature is split into slices like:
Features/Todos/CreateTodo/— create a todoFeatures/Todos/GetTodo/— get one todoFeatures/Todos/GetTodos/— get all todosFeatures/Todos/UpdateTodo/— update a todoFeatures/Todos/DeleteTodo/— delete a todo
In one sentence: You organize by “what the system does” (features/use cases), not by “what kind of class it is” (controllers, services, repositories).
Where entities and DTOs live
In this style of project:
Entities (the classes that represent database tables and are used by EF Core) live in Core. All table definitions go there. Entities are shared across the app: handlers that write data create or update these objects and save them via the DbContext.
DTOs (Data Transfer Objects, shapes used for input or output of an operation, not the table itself) live with the feature:
Request DTOs (what the client sends in the API, e.g. the body of POST/PUT) usually sit next to the endpoints or in the same feature folder.
Result DTOs (what a command or query returns, e.g.
CreateTodoResult,GetTodoResult) live in the same slice as the command or query, often in the same file. Each slice owns its request/response shape.
2. What is CQRS?
CQRS stands for Command Query Responsibility Segregation.
Command = an intention to change state (create, update, delete). Commands usually return a small result (e.g. id, or success).
Query = an intention to read state. Queries return data and do not change it.
So you separate:
Writes: Commands + Command Handlers
Reads: Queries + Query Handlers
In this project:
Commands:
CreateTodoCommand,UpdateTodoCommand,DeleteTodoCommandQueries:
GetTodoQuery,GetTodosQuery
Each command/query has one handler that contains the logic. MediatR is the library that “sends” the command or query to the right handler.
Purpose of Command, Handler, and Validator
In each slice you typically have three kinds of pieces.
Command (or Query): Represents what the user wants to do. It is a small object that carries only the data needed for that action (e.g. "create a todo with this title and description"). It does not contain logic. Commands are for writes; queries are for reads.
Handler: Contains the business logic for that action. It receives the command (or query), talks to the database or other services, and returns a result. One handler per command/query. Handlers are where the real work happens (create entity, save, return result).
Validator: Checks that the input of a command (or request) is valid before the handler runs. It answers: "Is the title present? Is it too long?" If validation fails, the handler is never called and the API can return a 400 with error messages. Validators keep validation rules in one place and out of the handler.
3. What is MediatR?
MediatR is a .NET library that implements the mediator pattern. In plain terms:


Join the conversation! Your thoughts help the community grow.