Простой вопрос: делая задачу, касающуюся API - вы чаще работаете с одним эндпоинтом, или пишите, условные, репозитории, которые используются сразу в нескольких эндпоинтах? Скорее всего, первое, тогда почему мы разбиваем проект по слоям, а не по фичам (эндпоинтам)?
Это видно в часто используемых нынче архитектурных подходах: Layered, Clean Architecture, Onion, и так далее. Не буду выделять что-то конкретное и объясню общую разницу в подходах:
Vertical Slice Architecture (VSA) строится вокруг каждого отдельного feature-слайса (эндпоинта, как самый простой пример), а не на вокруг слоев.
То есть, если код относится к конкретному эндпоинту, мы не размазываем его по всему проекту в папках Commands/Services/Repositories/DTOs и т.п., а кладем в одно место, там где и будет находиться эндпоинт. На картинке это выглядит так:

Главный принцип - уменьшаем связанность между слайсами (фичами), увеличиваем связанность внутри слайса
Зачем?
Вопрос, ответ на который я вижу довольно редко в статьях об архитектурах. Не будем плодить карго-культ "потому что так все делают" и ответим:
Главное преимущество VSA - продуктивность и минимум оверхэда, выраженного в бесконечной навигации по коду: все в одном месте, без беготни по проекту, лишних абстракций (а они вам вот все действительно нужны?), тысяч файлов, с минимальным пересечением логики.
Принцип KISS хорошо выполняется и позволяет быстро разобраться - не обязательно понимать все тонкости архитектуры проекта, прежде чем начинать писать код.
От огромных сервисов то мы ушли, только вот теперь скроллим не тысячи строк когда, а сотни папок.
При этом, основные преимущества более привычных подходов сохраняются: модульность (которая, на мой взгляд, в VSA выражена даже лучше), отсутствие огромных сервисов, легкость тестирования и т.п.
Данный подход довольно новый (опустим про хорошо забытое старое - я уверен многие сами его давно используют неосознанно), даже на хабре сложно найти авторскую статью. В 2018 году, например, о нем написал Jimmy Bogard, автор всем нам знакомого MediatR и AutoMapper, а теперь уже и тренинги по VSA проводит.
Как знать, может через несколько лет и очередной eShop попробует данную архитектуру.
Пример организации проекта
Поскольку архитектура довольно новая, устоявшихся названий и понятий еще нет, поэтому код ниже не стоит воспринимать как единственно-верный вариант
Также буду использовать Minimal API (но никто не мешает пользоваться контроллерами)
Немного о Minimal API
Кто еще не успел познакомится этим нововведением dotnet - не помешает. Это не просто желание "мы тоже хотим писать на c# API в 4 строчки кода", а действительно хороший инструмент, который призван заменить устаревшие контроллеры. На хабре мало инфы, если данная статья зайдет, напишу о том, как использовать Minimal API в реальном проекте с кучей инфраструктуры, а не просто Hello World
Структура будет выглядеть следующим образом:
Program.cs
Endpoints (или, как часто называют, Features)
| User
| GetUserInfo.cs
| UserInfoResponse.cs
GetUserInfo.cs:
public class GetUserInfo : IEndpoint
{
public void Register(IEndpointRouteBuilder endpointsBuilder)
{
// Регистрация эндпоинта. В случае контроллера это был бы стандартный метод с атрибутом [HttpGet("user/info")].
endpointsBuilder.MapGet("/user/info", HandleAsync)
.RequireAuthorization()
.WithDescription("Get user basic info")
.WithTags("User");
}
// Параметры этого метода почти аналогичны параметрам метода контроллера, DI зарезолвит все необходимые зависимости.
public static Task<UserInfoResponse> HandleAsync(ExampleDbContext db, UserContext userContext, CancellationToken ct)
{
return db.Users.Where(x => x.Id == userContext.UserId)
.Select(x => new UserInfoResponse
{
UserId = x.Id,
Username = x.Username,
})
.SingleAsync(ct);
}
}
Где в HandleAsync() - вся необходимая логика запроса. Этот метод сделан публичным для возможности тестирования, об этом чуть ниже.
Такая наглядность позволит подпускать к проекту не только сеньоров с солидным архитектурным бэкграундом, но и разработчиков с опытом поменьше. Да и для вас самих GitHub Copilot, или чем вы пользуетесь, сможет банально лучше анализировать весь нужный код т.к. будет иметь более полный контекст.
IEndpoint - просто интерфейс для удобства регистрации эндпоинта Minimal API с единственным методом Register(): в отличии от контроллеров, магической рефлексии из коробки нет. Подробнее, как я говорил, стоит разобрать в отдельной статье.
Можно заметить, что в описанной мной структуре UserInfoResponse.cs находится внутри GetUserInfo.cs: если что, да, так можно делать, просто перетащив файл в другой в IDE. В .csproj для этого появится соответствующая запись с DependentUpon. Парой абзацев ниже скриншот для примера.
Но если не нравится - можно создать папку GetUserInfo и сложить в нее все файлы, относящиеся к данному эндпоинту (и сам эндпоинт)
MediatR
Кто не увидел MediatR - не беспокойтесь - ничто не мешает использовать его с паттерном CQRS в данной схеме. Все необходимые файлы просто лягут под EndpointName.cs, а не размажутся тонким слоем по проекту.

Ниже я оставлю ссылки на репозитории, делают именно так. Или же, как можно заметить в этих репозиториях, кто-то вообще создает эти классы прямо внутри файла EndpointName.cs. А почему бы и нет? Несколько классов в одном файле - это, конечно, вполне себе антиппатерн. Но как и всегда в таких случаях, это не значит что это автоматически "плохо". Это плохо только если не понимаешь, почему обычно так не делают.
Однако, я бы задумался: действительно ли нужен MediatR с таким подходом, особенно учитывая что он становится платным? Проблему сервисов на тысячи строк мы решаем и без него. Это просто еще один аргумент против его бездумного использования.
Domain-Driven Design и другие архитектуры
Я хоть и намеренно противопоставил VSA другим подходам, на практике же они совместимы во многих аспектах: хоть немного сложный проект быстро выйдет за пределы набора API эндпоинтов. В конце статьи я привел примеры, которые вполне сочетают в себе эти подходы.
Вообще, принципы DDD хорошо работают с VSA: у нас все еще остается большое количество кода, прежде всего инфраструктурного, который хорошо ложится на эту модель: разумеется, не надо тащить в каждый эндпоинт общую для проекта обработку ошибок/транзакции/валидацию и т.п.: это все так и продолжит жить в своей области.
Тестирование
Тут все просто - вызываем наш HandleAsync, передаем нужные параметры и моки сервисов.
Может, не так красиво, как с использованием DI и MediatR, но зато при изменении эндпоинта больше вероятность отловить ошибку в тесте на этапе компиляции.
Если хочется красиво и с вызовами API - можно так.
Использование за пределами API
Конечно, большинство проектов не состоит из одних только эндпоинтов: у нас есть очереди, job'ы и т.п. Но ничто не мешает использовать там такой же подход. Более того: я думаю многие заметят, что они сами, осознанно или нет, уже так делают. А если нет - время задуматься.
Ну и в целом, такой подход легко выходит не только за пределы не только бэкенда (Feature-Sliced Design - пример из мира фронтэнда), но и кода вообще: мы же, например, не разбиваем команды в проекте по принципу "эта команда пишет контроллеры, а эта - хэндлеры".
Микросервисы и монолиты
Вполне логично, что VSA может работать и так и так. И такая архитектура проще разбивается на микросервисы: ведь мы выделяем их не по слоям "это у нас микросервис Domain", а по фичам. Может, с таким подходом "у нас монолит, но мы переходим на микросервисы" в каждой второй компании было бы чуть меньше.
Границы тоже есть
На заглавной картинке видно, как слайс проходит прямо по базе данных. Кто-то может сделать вывод, что Entity/модели, представляющие таблицы в БД, тоже нужно ограничивать областью слайса.
Если у вас на проекте подход Code-first (или просто полная репрезентация БД в коде), не соглашусь: это все еще должен быть отдельный слой. Если же вы пишите DTO для данных из базы под конкретный эндпоинт - этот DTO прекрасно ляжет в слайс.
Аналогично, если используете паттерн репозитория, тут, конечно, зависит от вашей реализации: если метод репозитория используется в одном-единственном эндпоинте/группе эндпоинтов, и его можно выделить не в ущерб тестированию, то, вероятно, так и стоит сделать: общий принцип - не держать код конкретной фичи вне ее пределов.
Похожие подходы
На хабре уже была статья-перевод, вдохновленная VSA, только с группированием больше по целым модулям, а не конкретным эндпоинтам. Для наглядности, приведу пример структуры оттуда:
WebApplication
│ appsettings.json
│ Program.cs
│ WebApplication.csproj
│
├───Modules
│ └───Orders
│ │ OrdersModule.cs
│ ├───Models
│ │ Order.cs
│ └───Endpoints
│ GetOrders.cs
│ PostOrder.cs
Слайс тут - не отдельный эндпоинт, а группа. Принципиально подход не меняется. Такой подход предлагал небезызвестный Роберт Мартин и назвал это Screaming Architecture. Главная идея тут - что архитектура "кричит": по структуре проекта мы можем понять не просто, какой архитектурный подход использовался, а и то, какой функционал выполняет данный проект.
На практике, как и везде, конечно, не все гладко
Что если у двух эндпоинтов какой-то общий между собой код? Использовать сервисы/хэлперы никто не запрещает. Поместить их можно рядом с эндпоинтами: зачем выносить уникальный для них код куда-то за их пределы. Ну или, вот статья на эту тему для вдохновения.
Или, если эндпоинты почти идентичны (например, из-за версионирования), может совсем объединить их в один файл.
Однако, не забываем, что принцип DRY, конечно, важен, но 3 строчки одинакового кода - еще не повод пересмотреть архитектуру проекта - все равно она не будет идеальной. Даже если кажется что будет.
В целом, если в проекте большое количество внутренних связей, хитрых транзакций и т.п., которые тяжело разбить по фичам, такая архитектура может выглядеть уже не так красиво. Впрочем, как и любая другая архитектура в неподходящем для нее контексте.
Главное, что хочется отметить: при проектировании нужно думать не о том, как написали вон в той авторитетной статье/книге, а головой.
Напоследок, поскольку я привел только весьма упрощенный пример, а не инструкцию к действию, несколько проектов VSA на GitHub:
ContosoUniversity от Jimmy Bogard. Давненько не обновлялся, но как реф подойдет.
Еще пример VSA со своей статьей
Food Delivery Microservices - work in progress, но общие концепции можно почерпнуть.
Комментарии (9)
ritorichesky_echpochmak
08.06.2025 18:03Видимо мне наглядности не хватает, но пока выглядит как "я не смог пилить архитектуру, поэтому буду по старинке складывать всё в кучу". Для тупней типа меня - отличненько
Espleth Автор
08.06.2025 18:03=)
Все мы иногда тупни. Найти себе сплошных senior-ninja-10+yoe-developer в команду, чтобы поддерживать сложную архитектуру, и не испортить все в первый кранч/отпуск тимлида (и это еще если тимлид сам понимает, что делает) - это, конечно, здорово, но сложно и дорого.
Можно найти довольно много книг/статей, и вот прям шаг за шагом построить себе красивую архитектуру следуя всем заветам, и для этого не потребуется быть каким-то одаренным разработчиком. Однако, точно ли оно стоило потраченного времени? Точно ли бизнес выиграет от этого оверхэда? Точно ли коллега прочел ту же книгу и на одной волне с тобой, или просто делает вид?
На разных проектах свои правильные ответы на эти вопросы, но у меня есть ощущение, что к сожалению их обычно не задают себе.
S1908
08.06.2025 18:03Все становится сложным когда сущностей в бд 100+. Вы предлагаете делить одномерно по VSA или HSA. Я уже давно применяю подход двумерного деления: VSA+HSA. Если интересно могу описать.
Espleth Автор
08.06.2025 18:03Я там местами описал, в параграфах про DDD и границы подхода: далеко не весь код проекта получится (и стоит) поделить прям по слайсам: горизонтальные слои все равно останутся, и для меня суть VSA скорее в том, чтобы избавиться от горизонтальных слоев (и не размазывать код по всему проекту) там, где это возможно.
Так что, возможно, корректнее это было бы назвать VSA+HSA. Но если ваше понимание этого отличается, то интересно было бы услышать.
S1908
08.06.2025 18:03Если не применять двумерный подход то всегда случается что одна ответственность с одной точки залезает в другую так как по второй мерности они слиты воедино.
jakobz
08.06.2025 18:03Вообще, я тут на эту тему размышлял в таком стиле: если у нас в моде теперь микросервисы, а монолиты типа плохо - почему мы внутри одного монолита даже не пытались все эти годы делить код на части?
Можно же придумать и как ограничить куски БД, которым управляет каждый слайс. И сделать взаимодействие «слайсов» так, чтобы это были асинхронные вызовы с сериализуемыми параметрами. Чтобы чуть что - деплоить этот слайс отдельно, или выделить в отдельную репу.
Но там куча вопросов, начиная с того, как разложить это по проектам. Я пока ничего лучше эдакой 2х мерной архитектуры не придумал: у меня разбито на проекты как обычно - common (dto, интерфейсы), services (реализация), web (контроллеры и прочее про веб). Но в каждом проекте - все лежит по одинаково названным папочкам, типа Users, Products, и т д. И этим слайсам разрешено общаться только через их публичные интерфейсы в common. Так себе, конечно, но хоть какой-то порядочек.
Особенно этот вопрос заиграл с LLM-ками: им надо четкие правила как что класть, и где что искать, ограничить им контекст нужно.
Espleth Автор
08.06.2025 18:03Да, VSA в текущем виде - не универсальное решение, хотя у меня есть подозрение что универсального решения и не будет: слишком уж размыты могут быть границы фичи от проекта к проекту. Да и сегодня твой эндпоинт рекомендаций для юзера учитывает только его купленные товары, а завтра пол базы корми нейронке, чтобы угадывать что захочет купить юзер.
В общем, правила может и можно сформулировать, только увы, головой все равно думать придется.
Но с LLM-ками да. По моему опыту, они могут подсказывать куда чаще и качественнее с VSA структурой (да и просто если писать код не забывая о KISS), чем с огородом из слоев и абстракций, даже при одинаковом функционале проекта.
Heggi
08.06.2025 18:03Я в своих проектах заменил MediatR на аналог с кодогенерацией https://www.nuget.org/packages/Mediator.SourceGenerator
Kroen89
Тоже недавно открыл для себя VSA и пока что вполне ей доволен.
Посмотрим как она поведёт себя при усложнении проекта