Когда меня впервые познакомили с 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 - объект входных данных

  1. Без логики

  2. Только типизированные данные

  3. Данные могут быть провалидированы на уровне запроса (например, Request) или внутри DataInput, валидируя данные в DataInput вы создаёте единую точку проверки данных в вашем приложении, ещё лучше использовать ValueObject. Но важно выбрать единый подход в проекте

  4. Желательно делать данные неизменяемыми (immutable), если это поддерживается языком

class StoreOrderDataInput
{
    public function __construct(
        public readonly int $userId,
        public readonly array $items,
    ) {}
}

StoreOrderDataOutput - объект результата выполнения

  1. Рекомендуется не возвращать Eloquent напрямую, а формировать явный контракт (DTO или аналог)

  2. Формируем явный контракт

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. Контракты сценариев меняются и нужно поддерживать старые версии

После этого каждая директория внутри отвечает на вопрос:

Что умеет делать система?

Например:

  1. app/UseCases/StoreOder- создание заказа

  2. app/UseCases/CancelOrder- отмена заказа

  3. app/UseCases/RegisterUser - регистрация пользователя

  4. 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
        ]);
    }
}

Тестирвоание лёгкое и понятное! Добавился модуль? - не беда, просто дополните свой тест проверкой того что данный модуль выполняет свою работу!

Итоги

Что нам это дало?

  1. Явные контракты — теперь мы чётко видим и понимаем, какие данные принимает приложение при создании заказа и какие данные возвращает.

  2. Изоляция бизнес-логики — бизнес-логика становится простой и линейной: понятно, что именно выполняется на каждом этапе сценария.

  3. Удобство тестирования — теперь можно писать интеграционные тесты уровня сценария и проверять весь путь пользователя, не опасаясь, что изменения сломают бизнес-процесс при деплое.

  4. Быстрое включение новых разработчиков в проект — структура приложения описана через бизнес-сценарии, поэтому новым участникам команды проще понять, что именно делает система и где находится нужная логика.

UseCase — это один из практических инструментов структурирования бизнес-логики, который можно внедрять постепенно: начните с одного сценария — например, создания заказа — и со временем структура проекта начнёт выстраиваться вокруг бизнес-процессов, а не технических классов.

Большое спасибо что дочитали!

Если вам нужно больше примеров с кодом или сравнений, пишите в комментариях.
Дополнительно для вас я создал репозиторий с примером.

Комментарии (20)


  1. karrakoliko
    26.03.2026 20:42

    Явные контракты — теперь мы чётко видим и понимаем, какие данные принимает приложение при создании заказа и какие данные возвращает

    StoreOrderFeatureTest

    app/UseCases/V1

    StoreOrderHandler

    testUserCanCreateOrder

    у разрабов на laravel какая то особая тяга к хаосу.

    • фичатест тестирует контроллер, а не фичу

    • фича - сохранить заказ (не обработать, не принять, не поставить в очередь на обработку)

    • название тест метода говорит чтт проверяется то что пользователь может ссоздать заказ, хотя фича сохранить заказ

    • фича сохранить заказ проверяется в тесте контроллера апишеи, который проверяет может ли пользователь создать заказ

    • фича сохранить заказ = обработать запрос "сохранить", вызвав обработчик сохранения заказа, который на самом деле юзкейс

    • тест фичи "сохранить заказ" проверяет что "пользователь может создать заказ", проверяя что "после вызова запроса апи от юзера 1 в базе есть заказ юзера 1"

    плавает как утка, крякает как дятел, ведет себя как покемон, но в целом - енот охотничей породы

    Тестирование UseCase? - это ещё проще!
    В данном случае я советую писать тесты которые проверят работоспособность вашего бизнес-процесса.

    барабанная дробь... да, тестируется именно контроллер, все эти юзкейсы инпуты аутпуты были зря.

    вопросы:

    • а зачем тогда вам были нужны все эти церемонии с выделением input/output/handler?

    • что, кроме бессмысленных в приведенном примере 3+ классов, вы потеряете в этом случае, если контроллер просто сохранит заказ из данных полученных в запросе?

    • как вы собираетесь использовать версионирование юзкейса "сохранить заказ"? будет ли ситуация что их 2 параллельно работают?

    учиться и учиться!


    1. adobry96 Автор
      26.03.2026 20:42

      Большое спасибо за прочтение и за ваш комментарий!
      Остальные классы вы также можете протестировать)
      Да контроллер может сохранить заказ без проблем!

      Все мы учимся!


  1. nektobit
    26.03.2026 20:42

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


    1. adobry96 Автор
      26.03.2026 20:42

      Большое спасибо что прочитали)

      Да UseCase-ы как оказалось уже стандарт, особенно их ценят в бигтех, так как бизнес-процессы там большие и запутаться в них легко.


      1. vkGrove
        26.03.2026 20:42

        Почитайте про Vertical Slice Architecture (VSA).

        Почему то в ру сегменте подход, и информация о нем, слабо распространены.


        1. adobry96 Автор
          26.03.2026 20:42

          Спасибо, обязательно посмотрю!


  1. dmitriylanets
    26.03.2026 20:42

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


    1. adobry96 Автор
      26.03.2026 20:42

      Большое спасибо что прочитали статью)

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


      1. MihaOo
        26.03.2026 20:42

        Если речь о Rest API, то в некоторых случаях можно возвращать 204 No Content :)

        Имхо ситуация отличается от проекта к проекту, тут сложно предугадать.

        Если можно сформировать DTO'шку из текущего агрегата, почему этого не сделать? Такой код не будет делать лишние запросы в базу и, скорее всего, его будет просто понять. Тут скорее важно только где и как вы мапите в DTO и что бы сам агрегат не был слишком раздут.

        С другой стороны, если так поступить не получится, придётся обращаться к CQRS, который, как тут недавно писали, нарушает DDD, и делать запрос из ReadRepository.

        Поэтому имхо баланс очень важен.


        1. adobry96 Автор
          26.03.2026 20:42

          Согласен с вами полностью!

          Когда мы ничего не возвращаем наша архитектура становится похожа больше на команды CQRS паттерн.

          Но даже самые смелые возвращают id записи чтобы была возможность у фронта обратиться за получением, либо прибегают к использованию websocket чтобы получить тот или инное событие системы.


  1. likeapimp
    26.03.2026 20:42

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


    1. adobry96 Автор
      26.03.2026 20:42

      Большое спасибо что прочитали статью)

      Если не секрет что конкретно разгребаете? С какими проблемами столкнулись?


      1. likeapimp
        26.03.2026 20:42

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


        1. adobry96 Автор
          26.03.2026 20:42

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


          1. pwflamy
            26.03.2026 20:42

            Что бы знать, что делает приложение, в 90% случаях достаточно посмотреть на точки входа, разве нет? Оставшиеся 10% это какие нибудь бекграунд джобы.

            Имхо, не важно как вы называете это, но это просто добавление 4 слоя к стандартной трехслойной архитектуре. И имхо в идеале добавлять это только для части сценариев, где наблюдается сложная бизнес логика, связанная с множеством сервисов и репозиториев. Таких сценариев часто не очень много, подавляющее большинство вполне укладываются в три слоя.

            Как мне кажется выделить какой либо сценарий в 4 слой можно на любом этапе разработки, когда в этом появится необходимость (каждый сам проведет границу).


            1. adobry96 Автор
              26.03.2026 20:42

              Полностью с вами согласен!


  1. monco83
    26.03.2026 20:42

    1. Вот был у меня UseCase - создание приглашения о присоединении к системе для пользователя (в табличку запись добавляется, пользователю письмо улетает).
    Потом появился новый сценарий - принятие запроса на временное присоединение к пространству пользователя специалиста поддержки, в который так же входит отправка приглашения пользователю (в табличку запись добавляется, пользователю письмо улетает). Теперь мой первый UseCase - уже не UseCase, если это часть другого UseCase? Что мне делать с "последовательностью управляющих конструкций"? Продублировать в двух UseCase'ах? Вынести код из первого UseCase в разделяемый сервис/модель? Вызвать один UseCase из другого?

    2. Если сервисы делать маленькими, то это уже UseCase'ы?


    1. adobry96 Автор
      26.03.2026 20:42

      Большое спасибо что прочитали статью!)

      1. Вы можете вынести общую логику в сервис, и сервис уже подключить в двух UseCase. Использовать один UseCase в другом UseCase лучше в самую последнюю очередь.

      2. Маленькие сервисы это не UseCase. Размер класса не определяет его роль. UseCase - это один конкретный бизнес процесс, бизнес сценарий. А Service это переиспользуемая бизнес-логика, т.е. часть какого-либо сценария.


      1. monco83
        26.03.2026 20:42

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


        1. adobry96 Автор
          26.03.2026 20:42

          Граница между Service и UseCase действительно условна. Но цель UseCase не в строгой классификации классов. Его задача сделать структуру приложения отражением бизнес-сценариев системы.