Когда меня впервые познакомили с UseCase, я их отрицал.
Но после того как разобрался глубже, я больше не представляю своё приложение без этого подхода. И теперь хочу поделиться им с вами.
Примеры в статье будут на Laravel (PHP 8.3), однако сам подход не зависит от конкретного фреймворка или языка программирования - его можно применять в любом backend-проекте.
Также сразу отмечу:
это не волшебная таблетка, которая исправит все проблемы архитектуры.
Но при правильном использовании этот подход делает приложение заметно более понятным, структурированным и управляемым.
Отдельно хочу сказать спасибо человеку, который познакомил меня с этим подходом.
Александр, спасибо.
Что такое UseCase
Если говорить просто, UseCase - это один законченный бизнес-сценарий системы.
Например:
создать заказ
зарегистрировать пользователя
авторизовать пользователя
изменить статус заявки
сформировать счёт
UseCase описывает не техническую операцию, а действие системы с точки зрения бизнеса.
То есть не «записать строку в базу», а именно: пользователь оформляет заказ.
С точки зрения архитектуры:
это уровень приложения (Application Layer)
он управляет сценарием — что происходит и в каком порядке
содержит только управляющую логику
делегирует бизнес-правила доменным сервисам и сущностям
Если внутри сценария появляются сложные расчёты или правила — их нужно выносить в доменные компоненты.
Например, подсчёт стоимости заказа можно вынести в
PricingService.
Проблема, которую он решает, проявляется с ростом проекта.
Пока система маленькая, всё выглядит понятно. Но со временем логика начинает расползаться: часть в контроллерах, часть в сервисах. В какой-то момент становится сложно ответить на простой вопрос — что вообще умеет система. Код есть, методы есть, но целостной картины нет.
В итоге становится сложно ответить на простой вопрос:
Что вообще умеет делать наше приложение? - да именно такой вопрос возникает у меня когда я захожу на свои старые проекты где надо что-то исправить.
UseCase вводит понятную структуру: система описывается через явные бизнес-сценарии, а не через абстрактные сущности вроде Service, Manager или Helper.
В результате архитектура становится читаемой и понятной на уровне бизнес-логики.
Есть ли ограничения у UseCase?
В большинстве случаев не рекомендуется вызывать один UseCase из другого, чтобы:
не создавать скрытые зависимости между сценариями
не превращать UseCase в переиспользуемые "блоки логики"
Однако в сложных сценариях допустим вызов одного UseCase из другого, если:
нет циклических зависимостей
сохраняется явность сценариев
Если появляется необходимость повторного использования логики - её стоит вынести в отдельные сервисы или доменные компоненты. Эти компоненты затем могут использоваться в разных UseCase.
Такой подход помогает сохранить явные границы сценариев и делает архитектуру более предсказуемой.
Где вызываются UseCase
UseCase — это точка входа в бизнес-логику приложения. И вызываться он может практически откуда угодно:
Controller
Console Command
Job
Event Listener
Проще всего представить это так:
UseCase — это ручки, через которые внешний мир взаимодействует с бизнес-логикой приложения.
Почему одного Service-слоя может быть недостаточно?
На старте проекта одного Service-слоя действительно достаточно — так делают почти все. Проблема появляется позже, когда бизнес-логика начинает расти.
Сначала создание заказа выглядит просто: сохранить запись в базе.
Но со временем появляются дополнительные шаги:
проверить остатки
применить скидку
списать баланс
отправить уведомление
записать аналитику
И постепенно появляется класс OrderService, в котором:
много зависимостей
много методов
много побочных эффектов
сложно читать код
сложно писать тесты
Да, я сам писал сервисы на 1000+ строк с десятками методов — это распространённая ситуация в реальных проектах. И в каком-то смысле это логично: бизнес-процесс растёт вместе с продуктом. Но такой рост приводит к усложнению кода и потере управляемости.
Со временем сервис может превратиться в God-object - класс с большим количеством методов, зависимостей и побочных эффектов.
Важно понимать: проблема не в самом Service как паттерне, а в отсутствии явных границ сценариев. Без разделения на UseCase любая реализация со временем начинает разрастаться и усложняться.
Вы можете сказать: «Можно же просто разбить сервис на более мелкие, например
StoreOrderService, и использовать его везде».
И это действительно частично помогает. Но остаётся ключевой вопрос: где границы бизнес-сценария? Какие шаги входят в «создание заказа», а какие — нет?
Без явного выделения UseCase эти границы остаются неочевидными, и логика всё равно со временем начинает расползаться по коду.
UseCase предлагает простое правило:
один UseCase = один бизнес-процесс
А повторяющаяся логика при этом выносится в отдельные сервисы и доменные компоненты. Это позволяет сохранить код управляемым даже по мере роста сложности.
Структура UseCase
Я обычно разделяю UseCase на три части:
Handler
DataInput
DataOutput
Границы UseCase:
управляет последовательностью выполнения сценария (orchestration)
может содержать условия выполнения шагов
не должен содержать доменную бизнес-логику (инварианты, расчёты)
Доменная логика должна находиться в: сущностях (Entities), ValueObjects, доменных сервисах. К примеру как я писал ранее PricingService.
Пример: создание заказа
StoreOrderDataInput - объект входных данных
Без логики
Только типизированные данные
Данные могут быть провалидированы на уровне запроса (например, Request) или внутри DataInput, валидируя данные в DataInput вы создаёте единую точку проверки данных в вашем приложении, ещё лучше использовать ValueObject. Но важно выбрать единый подход в проекте
Желательно делать данные неизменяемыми (immutable), если это поддерживается языком
class StoreOrderDataInput { public function __construct( public readonly int $userId, public readonly array $items, ) {} }
StoreOrderDataOutput - объект результата выполнения
Рекомендуется не возвращать Eloquent напрямую, а формировать явный контракт (DTO или аналог)
Формируем явный контракт
class StoreOrderDataOutput { public function __construct( public readonly int $orderId, public readonly string $status, ) {} }
StoreOrderHandler - сам сценарий
class StoreOrderHandler { public function __construct( private OrderRepository $orders ) {} public function handle(StoreOrderDataInput $input): StoreOrderDataOutput { $order = new Order( userId: $input->userId, ); $orderSaved = $this->orders->save($order); return new StoreOrderDataOutput( orderId: $orderSaved->id, status: $orderSaved->status ); } }
Использование в Laravel
Контроллер становится максимально простым:
class OrderController { public function store(Request $request, StoreOrderHandler $handler) { $input = new StoreOrderDataInput( userId: auth()->id() ); $output = $handler->handle($input); return response()->json($output); } }
Получение DTO можете вынести в Request создав для него отдельный класс StoreOrderRequest. Или используя библиотеку laravel-spatie-data вы можете использовать ваши DTO в качестве Request - в таком случае валидация будет автоматической.
Создание заказа требует дополнительной логики? - Не проблема!
Всё, что относится к сценарию «создание заказа», остаётся внутри одного Handler.
А переиспользуемая логика выносится в отдельные классы: PaymentService, DiscountService, StockService, NotificationService.
Handler начинает выполнять оркестрацию - он управляет последовательностью шагов сценария.
class StoreOrderHandler { public function __construct( private OrderRepository $orders, private PaymentService $payment, private DiscountService $discounts, private StockService $stock, private NotificationService $notifications, ) {} public function handle(StoreOrderDataInput $input): StoreOrderDataOutput { $this->payment->charge($input->userId); $this->discounts->apply($input); $this->stock->reserve($input->items); $order = $this->orders->create($input); $this->notifications->sendOrderCreated($order); return new StoreOrderDataOutput( orderId: $order->id, status: $order->status ); } }
Представьте как тяжело бы было реализовать это в случае с Сервисом "Создание заказа" наверняка вы бы написали сервис на 1000 строк и более, а также начали бы писать логику в контроллере.
Как я храню UseCase в проекте
В Laravel я создаю структуру:
app/UseCases/V1
Как вы видите добавляется версия сценариев. В будущем это помогает безопасно менять контракты для ваших сценариев, но если вдруг ваши сценарии не меняются то вы как минимум подстелили себе солому.
Конечно использование версионирования должно быть опциональным и обсуждаться вместе с командой.
В большинстве случаев это имеет смысл только если:
1. У вас публичный API
2. Контракты сценариев меняются и нужно поддерживать старые версии
После этого каждая директория внутри отвечает на вопрос:
Что умеет делать система?
Например:
app/UseCases/StoreOder- создание заказа
app/UseCases/CancelOrder- отмена заказа
app/UseCases/RegisterUser - регистрация пользователя
app/UseCases/AuthorizeUser- авторизация пользователя
Структура проекта начинает отражать бизнес-возможности системы (что она умеет делать), а не технические детали реализации
Тестирование UseCase? - это ещё проще!
В данном случае я советую писать тесты которые проверят работоспособность вашего бизнес-процесса.
Пример теста StoreOrderHandler
use Tests\TestCase; use Illuminate\Foundation\Testing\RefreshDatabase; class StoreOrderFeatureTest extends TestCase { use RefreshDatabase; public function testUserCanCreateOrder(): void { $user = User::factory()->create(); $payload = [ 'items' => [ ['id' => 10, 'qty' => 2], ['id' => 15, 'qty' => 1], ], ]; $response = $this->actingAs($user) ->postJson('/api/orders', $payload); $response->assertStatus(200); $response->assertJsonStructure([ 'orderId', 'status' ]); $this->assertDatabaseHas('orders', [ 'user_id' => $user->id ]); } }
Тестирвоание лёгкое и понятное! Добавился модуль? - не беда, просто дополните свой тест проверкой того что данный модуль выполняет свою работу!
Итоги
Что нам это дало?
Явные контракты — теперь мы чётко видим и понимаем, какие данные принимает приложение при создании заказа и какие данные возвращает.
Изоляция бизнес-логики — бизнес-логика становится простой и линейной: понятно, что именно выполняется на каждом этапе сценария.
Удобство тестирования — теперь можно писать интеграционные тесты уровня сценария и проверять весь путь пользователя, не опасаясь, что изменения сломают бизнес-процесс при деплое.
Быстрое включение новых разработчиков в проект — структура приложения описана через бизнес-сценарии, поэтому новым участникам команды проще понять, что именно делает система и где находится нужная логика.
UseCase — это один из практических инструментов структурирования бизнес-логики, который можно внедрять постепенно: начните с одного сценария — например, создания заказа — и со временем структура проекта начнёт выстраиваться вокруг бизнес-процессов, а не технических классов.
Большое спасибо что дочитали!
Если вам нужно больше примеров с кодом или сравнений, пишите в комментариях.
Дополнительно для вас я создал репозиторий с примером.
Комментарии (20)

nektobit
26.03.2026 20:42Капец. Если б не ваша статья я бы считал что usecases я сам придумал неделю назад. Я их назвал прям так же. Использовал схожим образом. И был настолько рад этой идее что мне даже не пришло в голову нагуглить что-то подобное:)

adobry96 Автор
26.03.2026 20:42Большое спасибо что прочитали)
Да UseCase-ы как оказалось уже стандарт, особенно их ценят в бигтех, так как бизнес-процессы там большие и запутаться в них легко.

dmitriylanets
26.03.2026 20:42Так то usecase не должны возвращать результаты, результат выполнения события, посмотрите видео с DrupalCon которому уже 10лет, там все подробно разложено https://youtu.be/ajhqScWECMo

adobry96 Автор
26.03.2026 20:42Большое спасибо что прочитали статью)
Не могу отрицать такие реализации тоже имеют место быть, но в данном случае мы работаем с restAPI приходиться что-то возвращать)

MihaOo
26.03.2026 20:42Если речь о Rest API, то в некоторых случаях можно возвращать
204 No Content:)Имхо ситуация отличается от проекта к проекту, тут сложно предугадать.
Если можно сформировать DTO'шку из текущего агрегата, почему этого не сделать? Такой код не будет делать лишние запросы в базу и, скорее всего, его будет просто понять. Тут скорее важно только где и как вы мапите в DTO и что бы сам агрегат не был слишком раздут.
С другой стороны, если так поступить не получится, придётся обращаться к CQRS, который, как тут недавно писали, нарушает DDD, и делать запрос из ReadRepository.
Поэтому имхо баланс очень важен.

adobry96 Автор
26.03.2026 20:42Согласен с вами полностью!
Когда мы ничего не возвращаем наша архитектура становится похожа больше на команды CQRS паттерн.
Но даже самые смелые возвращают id записи чтобы была возможность у фронта обратиться за получением, либо прибегают к использованию websocket чтобы получить тот или инное событие системы.

likeapimp
26.03.2026 20:42Был у нас такой юзкейсер, до сих пор разгребаем..)

adobry96 Автор
26.03.2026 20:42Большое спасибо что прочитали статью)
Если не секрет что конкретно разгребаете? С какими проблемами столкнулись?

likeapimp
26.03.2026 20:42с проблемой полотна юзкейсов, которые оказываются избыточной прослойкой из двух доп. классов для любой операции

adobry96 Автор
26.03.2026 20:42Понял, да конечно каждый подход надо обсуждать с командой, нужен/устраивает ли он команде/у. За-то вы знаете что умеет делать ваше приложение, могу вас обрадовать)

pwflamy
26.03.2026 20:42Что бы знать, что делает приложение, в 90% случаях достаточно посмотреть на точки входа, разве нет? Оставшиеся 10% это какие нибудь бекграунд джобы.
Имхо, не важно как вы называете это, но это просто добавление 4 слоя к стандартной трехслойной архитектуре. И имхо в идеале добавлять это только для части сценариев, где наблюдается сложная бизнес логика, связанная с множеством сервисов и репозиториев. Таких сценариев часто не очень много, подавляющее большинство вполне укладываются в три слоя.
Как мне кажется выделить какой либо сценарий в 4 слой можно на любом этапе разработки, когда в этом появится необходимость (каждый сам проведет границу).

monco83
26.03.2026 20:421. Вот был у меня UseCase - создание приглашения о присоединении к системе для пользователя (в табличку запись добавляется, пользователю письмо улетает).
Потом появился новый сценарий - принятие запроса на временное присоединение к пространству пользователя специалиста поддержки, в который так же входит отправка приглашения пользователю (в табличку запись добавляется, пользователю письмо улетает). Теперь мой первый UseCase - уже не UseCase, если это часть другого UseCase? Что мне делать с "последовательностью управляющих конструкций"? Продублировать в двух UseCase'ах? Вынести код из первого UseCase в разделяемый сервис/модель? Вызвать один UseCase из другого?
2. Если сервисы делать маленькими, то это уже UseCase'ы?
adobry96 Автор
26.03.2026 20:42Большое спасибо что прочитали статью!)
Вы можете вынести общую логику в сервис, и сервис уже подключить в двух UseCase. Использовать один UseCase в другом UseCase лучше в самую последнюю очередь.
Маленькие сервисы это не UseCase. Размер класса не определяет его роль. UseCase - это один конкретный бизнес процесс, бизнес сценарий. А Service это переиспользуемая бизнес-логика, т.е. часть какого-либо сценария.

monco83
26.03.2026 20:42Ну и в чём тогда выигрыш. Вот мы разложили всё по "сервисам" - переиспользуемую бизнес-логику и "use-case'ы" - конкретные бизнес-сценарий. У нас система развивается и "конкретный бизнес-сценарий" превратился в "переиспользуемую бизнес-логику". Так что границы между этими понятиями, которые мы для себя нарисовали, оказались весьма условны.

adobry96 Автор
26.03.2026 20:42Граница между Service и UseCase действительно условна. Но цель UseCase не в строгой классификации классов. Его задача сделать структуру приложения отражением бизнес-сценариев системы.
karrakoliko
у разрабов на laravel какая то особая тяга к хаосу.
фичатест тестирует контроллер, а не фичу
фича - сохранить заказ (не обработать, не принять, не поставить в очередь на обработку)
название тест метода говорит чтт проверяется то что пользователь может ссоздать заказ, хотя фича сохранить заказ
фича сохранить заказ проверяется в тесте контроллера апишеи, который проверяет может ли пользователь создать заказ
фича сохранить заказ = обработать запрос "сохранить", вызвав обработчик сохранения заказа, который на самом деле юзкейс
тест фичи "сохранить заказ" проверяет что "пользователь может создать заказ", проверяя что "после вызова запроса апи от юзера 1 в базе есть заказ юзера 1"
плавает как утка, крякает как дятел, ведет себя как покемон, но в целом - енот охотничей породы
барабанная дробь... да, тестируется именно контроллер, все эти юзкейсы инпуты аутпуты были зря.
вопросы:
а зачем тогда вам были нужны все эти церемонии с выделением input/output/handler?
что, кроме бессмысленных в приведенном примере 3+ классов, вы потеряете в этом случае, если контроллер просто сохранит заказ из данных полученных в запросе?
как вы собираетесь использовать версионирование юзкейса "сохранить заказ"? будет ли ситуация что их 2 параллельно работают?
учиться и учиться!
adobry96 Автор
Большое спасибо за прочтение и за ваш комментарий!
Остальные классы вы также можете протестировать)
Да контроллер может сохранить заказ без проблем!
Все мы учимся!