Когда меня впервые познакомили с 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 — это один из практических инструментов структурирования бизнес-логики, который можно внедрять постепенно: начните с одного сценария — например, создания заказа — и со временем структура проекта начнёт выстраиваться вокруг бизнес-процессов, а не технических классов.
Большое спасибо что дочитали!
Если вам нужно больше примеров с кодом или сравнений, пишите в комментариях.
Дополнительно для вас я создал репозиторий с примером.
