0% прочитано

Application Service і Domain Service: як я перестала плутати їхню відповідальність

Мені довго було легко назвати будь-яку складнішу логіку сервісом. На практиці допомогло просте розділення: прикладний сервіс координує сценарій, транзакцію, сховище та зовнішні залежності, а доменний сервіс виражає правило предметної області, яке природно не належить одному об’єкту.

14 жовтня 2026 р. 10 хв читанняЛаравель

У попередньому матеріалі про те, де має жити бізнес-правило я розділяла контролер, прикладний сервіс і модель предметної області.

Але ще довго після цього слово "сервіс" залишалося для мене занадто широким.

Клас щось робить? Назвемо його Service. Працює з кількома об’єктами? Service. Є трохи логіки? Знову Service.

Проблема в тому, що Application Service і Domain Service можуть обидва називатися "сервісами", але відповідають на зовсім різні питання.

Найпростіше правило, яке допомогло мені: Application Service координує сценарій. Domain Service приймає або допомагає прийняти доменне рішення.

Почнемо з одного простого сценарію

Один сценарій містить і координацію, і доменне рішення
Сценарій використання: Активація продукту Кроки:1. завантажити поточний стан2. перевірити бізнес-правило3. змінити стан4. зберегти результат5. ініціювати додаткову дію

Саме через такі сценарії я раніше змішувала дві відповідальності.

В одному методі потрібно завантажити дані, відкрити транзакцію, перевірити правило, змінити стан, зберегти результат і, можливо, запустити подальшу дію.

На перший погляд усе це одна "бізнес-операція". Але не весь цей код є доменною логікою.

Application Service для мене - координатор сценарію

Application Service координує кроки сценарію
final class ActivateProduct{    public function handle(        ProductId $productId,    ): void {        DB::transaction(function () use (            $productId,        ) {            $product = $this->products                ->getForUpdate($productId);             $product->activate();             $this->products->save(                $product,            );

У цьому прикладі прикладний сервіс робить кілька важливих речей.

  • відкриває транзакцію;
  • отримує потрібний aggregate зі сховища;
  • викликає доменну дію;
  • зберігає результат.

Він знає порядок кроків, але не повинен знати всі деталі того, чому саме activate() зараз дозволений або заборонений.

Transaction, repository і зовнішній API ще не роблять сервіс доменним

Координація зовнішніх залежностей залишається прикладною відповідальністю
Application Service може координувати: - transaction- repository- cache- queue- email- external API- file storage Це питання, що стосуються сценаріїв використання,а не змісту предметної області.

Це була важлива межа для мене.

Якщо клас працює з repository, чергою або зовнішнім API, це не означає, що він є Domain Service.

Навпаки, така робота часто добре показує прикладну відповідальність: сервіс організовує, як сценарій проходить через систему.

Предметна область не повинна знати, через яку чергу піде повідомлення або який клієнт викликає зовнішній API.

Domain Service з’являється, коли правило не має природного одного власника

Правило стосується кількох доменних понять і не належить одному з них повністю
final class ActivationEligibility{    public function canActivate(        Product $product,        Policy $policy,    ): bool {        return $product->isReady()            && $policy->allowsActivation();    }}

Тут ситуація інша.

Сервіс не відкриває транзакцію і не відправляє повідомлення. Він відповідає на доменне питання.

Правило використовує Product і Policy, але жоден із цих об’єктів природно не повинен знати всю логіку другого.

У такому випадку окремий Domain Service для мене виглядає чесніше, ніж штучно запихати правило в одну сутність.

Але я не створюю Domain Service, якщо правило нормально живе в entity

Якщо Product сам володіє рішенням, окремий сервіс не потрібен
$product->activate();

Це друга помилка, яку я робила: щойно бачила складніше правило, хотілося одразу створити Domain Service.

Але якщо правило стосується тільки стану Product і Product має достатньо даних, щоб прийняти рішення, метод на самому Product читається простіше.

Domain Service для мене не "кращий рівень архітектури". Це запасний варіант для справжнього доменного правила без природного одного власника.

Іноді правило ще простіше: воно належить value object

Не кожне правило між даними потребує окремого сервісу
final readonly class Version{    public function canUpgradeTo(        self $target,    ): bool {        return $target->isGreaterThan(            $this,        );    }}

Якщо правило природно описує саме значення, value object часто є кращим місцем.

Це допомагає мені не перетворювати Domain/Services на нову папку, куди складається все, що не помістилося в entity.

Як виглядає Application Service, який почав робити занадто багато

Сервіс уже не координує, а сам пояснює всю предметну область
final class ActivateProduct{    public function handle(        Product $product,        Policy $policy,    ): void {        if ($product->status !== 'draft') {            throw new LogicException();        }         if (! $product->hasRequiredData()) {            throw new LogicException();        }         if (! $policy->allowsActivation()) {            throw new LogicException();

Такий сервіс може працювати, але його роль стала нечіткою.

Він одночасно знає стан Product, правило готовності, Policy і спосіб збереження.

Як тільки частина цих правил знадобиться іншому сценарію, починається або копіювання, або повторне використання великого сервісу там, де його координація вже не потрібна.

Як я розділяю цей самий сценарій

Application Service координує, Domain Service відповідає на доменне питання
final class ActivateProduct{    public function handle(        ProductId $productId,    ): void {        DB::transaction(function () use (            $productId,        ) {            $product = $this->products                ->getForUpdate($productId);             $policy = $this->policies                ->current();             if (! $this->eligibility->canActivate(                $product,

Тепер ролі видно прямо з коду.

ActivateProduct знає послідовність сценарію. ActivationEligibility знає правило, яке використовує кілька доменних понять. Product сам знає, як змінити свій стан після позитивного рішення.

Це не єдиний правильний варіант, але для мене такий поділ набагато легше підтримувати.

Ще одна підказка: подивитися, від чого залежить сервіс

Залежності часто підказують реальну відповідальність
Application Service часто залежить від: - repository interfaces- transaction boundary- queue- mailer- external gateway- clock  Domain Service має залежати від: - domain objects- value objects- domain policies- domain abstractions

Це не абсолютне правило, але дуже корисна перевірка.

Якщо "Domain Service" раптом потребує Request, Response, Eloquent builder або конкретний HTTP-клієнт, я перевіряю, чи він справді доменний.

Чим ближче клас до предметної області, тим менше він повинен знати про спосіб доставки даних і конкретну інфраструктуру.

Тести цих двох сервісів теж виглядають по-різному

Domain Service тестується як правило предметної області
public function test_policy_blocks_activation(): void{    $product = Product::ready();    $policy = Policy::activationDisabled();     self::assertFalse(        $this->eligibility->canActivate(            $product,            $policy,        )    );}

Тест Domain Service часто можна написати без бази, HTTP і черги.

Він перевіряє правило через доменні об’єкти.

Тест Application Service, навпаки, більше цікавиться координацією: чи завантажено потрібний стан, чи викликана доменна дія, чи збережений результат, чи не сталася побічна дія до commit.

Aggregate допомагає не створювати зайвий Domain Service

Коли я чітко розумію aggregate, вибір стає простішим.

Якщо правило повністю захищає стан одного aggregate, я спочатку шукаю місце всередині нього. Про це я детальніше писала в матеріалі про aggregate і межу узгодженості.

Domain Service стає потрібним тоді, коли правило справді перетинає кілька доменних понять і не має одного природного власника.

Помилки, які я тепер помічаю швидше

Кілька сигналів для мене:

  • Application Service містить довгі списки правил стану.
  • Domain Service відправляє email або працює з HTTP.
  • Для кожного правила автоматично створюється новий Service.
  • Entity залишається лише набором getter/setter, а всі рішення живуть зовні.
  • Один Service одночасно приймає доменне рішення, відкриває транзакцію і викликає зовнішній API.
  • Назва класу закінчується на Service, але з коду незрозуміло, чи це координація, чи правило.

У AVN Center це стало зрозуміло через повторювані сценарії

У роботі над AVN Center я не починала з ідеального поділу сервісів.

Різниця ставала очевиднішою тоді, коли один і той самий інваріант починав використовуватися з кількох сценаріїв, а прикладні кроки навколо нього залишалися різними.

Це продовжує думку з першому матеріалі цієї DDD-послідовності: архітектура починає прояснюватися тоді, коли система накопичує реальні правила.

Я не вважаю це недоліком першої версії. Часто ми просто ще не мали достатньо інформації, щоб побачити правильну відповідальність.

Моя коротка перевірка: Application Service чи Domain Service?

Три питання часто достатні, щоб побачити відповідальність
Чи координує клас: - транзакцію- репозиторій- зовнішній сервіс- чергу- послідовність дій сценарію використання (use-case) -> Сервіс застосунку (Application Service)  Чи виражає клас: - бізнес-правило- рішення в межах предметної області- логіку, що охоплює кілька концепцій предметної області

Тепер я розділяю координацію і зміст правила

Для мене різниця між Application Service і Domain Service стала простою лише після практики.

Application Service відповідає на питання: як виконати цей сценарій у системі?

Domain Service відповідає на питання: яке доменне рішення правильне, якщо воно не належить одному об’єкту?

А якщо правило природно належить entity або value object, окремий Domain Service взагалі не потрібен.

Я більше не використовую слово Service як місце для "всієї логіки". Спочатку визначаю відповідальність, а вже потім обираю клас.

Application Service організовує виконання. Domain Service виражає правило. Якщо правило має природного власника, воно взагалі може не потребувати сервісу.