Бізнес-логіка живе в шарі, який нічого не знає про HTTP: у моделі домену або в use case (сервісі), який модель оркеструє. Контролеру лишаються переклад запиту в дані, виклик одного use case і вибір відповіді, бо контролер прив'язаний до транспорту і тому непридатний для повторного використання й дорогий у тестуванні.
Питання на співбесіду з архітектури
Архітектурні питання для Senior і Lead: межі сервісів, транзакційна цілісність, ідемпотентність, кешування, розрізання моноліту, компроміси рішень.
Адаптер підганяє один чужий обʼєкт під інтерфейс, якого вже чекає ваш код, і поведінки не змінює. Фасад вводить новий, простіший вхід до кількох сервісів підсистеми й сам знає порядок їх виклику. Фасади Laravel (`Cache::`, `Mail::`) - це статичні проксі до одного сервісу з контейнера, підсистеми за ними немає, тож з GoF Facade їх повʼязує лише назва.
Структура в них однакова: контекст тримає обʼєкт за інтерфейсом і делегує йому роботу. Різниця в тому, хто і коли цей обʼєкт міняє: стратегію обирає клієнт або конфіг, і вона нічого не знає про інші, а стан обʼєкт перемикає сам після дії, і кожен стан знає, куди з нього можна перейти.
Репозиторій виправданий, коли він виглядає як колекція агрегатів у мові домену й тримає доменний шар без Illuminate чи Doctrine; він шкодить, коли перетворюється на generic CRUD-обгортку, що повертає той самий Builder або модель і лише додає шар.
На практиці найчастіше працюють два принципи: єдина відповідальність тримає класи малими й зрозумілими, інверсія залежностей робить код тестованим; решта є наслідками, і сліпе дотримання всіх пʼяти породжує зайві абстракції.
Value object — незмінний обʼєкт без ідентичності, який описується лише своїм значенням: він валідує себе в конструкторі, тому в системі не існує невалідного екземпляра, і порівнюється за вмістом, а не за посиланням.
Доменна подія - це зафіксований факт у минулому часі, названий мовою бізнесу (`OrderPaid`), який сутність записує в себе, а застосунковий шар публікує після коміту. Події фреймворку (`kernel.request`, `eloquent.updated`, `postPersist`) описують технічний життєвий цикл запиту чи рядка, а інтеграційна подія - це окремий публічний контракт для інших сервісів, з власною версійованою схемою, який виїжджає через outbox і доставляється at-least-once.
Робочий набір у PHP невеликий: Strategy за інтерфейсом із вибором у фабриці, Decorator для наскрізних дрібниць типу кешу й логів, Observer для реакцій на зміну стану. Фреймворки вже зібрані з них (драйвери кешу і сесій, `#[AsDecorator]`, Eloquent-обсервери), а Singleton у PHP-застосунку реалізує контейнер через `singleton()` чи shared-сервіс, не сам клас.
Обидва патерни передають запит послідовністю обробників, але з різним наміром: у Chain of Responsibility запит обробляє перша ланка, яка за нього взялася, і далі він не йде, а в Pipeline працює кожен крок і передає свій результат наступному. Middleware в Laravel і Symfony Messenger поєднує обидва: зазвичай шар передає запит далі й може змінити відповідь на зворотному шляху, але будь-який шар може зупинити обробку, повернувши відповідь сам.
Factory Method відповідає на питання «який клас створити»: базовий код просить продукт у власного методу, а конкретний клас визначає підклас, як `createRedisDriver()` у нащадках `Illuminate\Support\Manager`. Builder відповідає на питання «як зібрати один складний обʼєкт»: кроки з назвами й перевірка всіх правил разом у `build()`. Model factories у Laravel з Factory Method мають спільну лише назву: за змістом це незмінний будівельник тестових даних.
Стиль API вибирається за клієнтами і за тим, що ви готові втратити: REST дає безкоштовне HTTP-кешування й прозорі логи, але ліпить over-fetching і зайві раунд-трипи; GraphQL забирає вибірку полів у клієнта, натомість вимикає кеш на CDN і переносить N+1 та авторизацію в резолвери; RPC виграє у внутрішній комунікації зі строгим контрактом. Для одного власного фронтенду GraphQL зазвичай зайвий - дешевше sparse fieldsets, include і BFF-ендпоінт під екран.
CQRS — це розділення операцій зміни стану (команди) і операцій читання (запити) на різні моделі: команди йдуть через доменні агрегати з інваріантами, читання — окремими DTO або навіть сирим SQL. Event sourcing і окрема база для читання — необовʼязкові додатки, а не частина визначення.
Порт - це інтерфейс, який оголошує саме ядро застосунку під свою потребу, адаптер - реалізація цього інтерфейсу в інфраструктурі поверх конкретної технології. Домен і Application залежать лише від портів, зв'язування адаптерів робить service provider у Laravel або services.yaml у Symfony, а в тестах ті самі порти закриваються in-memory адаптерами.
Command пакує намір разом з усіма даними в обʼєкт, щоб виконати його зараз, пізніше, повторно або з іншої точки входу. Command bus додає одне місце, через яке проходять усі команди: він знаходить обробник за класом команди й загортає виконання в middleware для транзакцій, логування й валідації. Queued job у Laravel - це команда й обробник в одному класі з доставкою через чергу, а Symfony Messenger тримає повідомлення й обробник окремо і вирішує, синхронно чи через транспорт, у конфігу `routing`.
За кодом їх не відрізнити: обидва реалізують той самий інтерфейс і тримають посилання на справжній обʼєкт. Декоратор додає поведінку (кеш, лог, повтори) і складається шарами там, де збирають обʼєкт, а проксі керує доступом до справжнього обʼєкта: відкладає його створення, перевіряє права або пересилає виклик кудись далі. Кешувальна обгортка репозиторію працює, лише якщо інвалідація живе в тій самій обгортці й спрацьовує після коміту.
DDD має сенс там, де складність саме в домені: багато правил, які часто змінюються, і потрібна спільна мова з бізнесом; для CRUD-адмінки чи інтеграційного сервісу агрегати й репозиторії додають шарів без вигоди.
Модулі й сервіси розділяє не код, а межа процесу: усередині моноліта виклик модуля — це синхронний метод у спільній транзакції, між сервісами — мережа з таймаутами, частковими збоями й eventual consistency. Тому за замовчуванням беруть модульний моноліт із твердими межами, а окремий сервіс виділяють лише там, де є конкретна причина — незалежний профіль навантаження, окремий цикл релізу під окрему команду або інший рантайм — і платять за неї розподіленими транзакціями та експлуатацією.
Outbox pattern прибирає проблему подвійного запису: подію зберігають рядком у таблиці `outbox` тією ж транзакцією, що й зміну стану, а окремий процес-релей уже після коміту читає ці рядки й публікує в брокер. База і подія стають атомарними, ціною чого є доставка at-least-once — споживач зобовʼязаний бути ідемпотентним.
Агрегат - це найменша група об'єктів, яку доводиться читати й писати разом, щоб гарантувати бізнес-правило: межа проходить там, де є інваріант, який мусить виконуватися після кожного коміту. Корінь агрегату - єдина точка входу, одна транзакція змінює один екземпляр, а зв'язки з іншими агрегатами тримаються за ідентифікатором; що більший агрегат, то більше конкурентних записів він серіалізує.
Версія - це ярлик на контракті, а сумісність визначається поведінкою: ламає не номер, а звужений тип, нове значення enum, суворіша валідація чи змінений статус-код. Робочий підхід - additive-зміни всередині версії, нова major лише на справжній злам, машиночитані заголовки Deprecation і Sunset плюс телеметрія по клієнтах перед вимкненням.
Межа контексту проходить там, де слово змінює значення і де змінюється склад людей, які замовляють зміни, а не там, де закінчується таблиця. Спільні дані розвʼязуються правилом одного власника факту: решта контекстів читають через контракт або тримають власну проєкцію, спільний код лишається мікроскопічним ядром без бізнес-правил, а чужа модель заходить усередину тільки через антикорупційний шар.
Клієнт передає idempotency key, сервер зберігає його разом із результатом операції під унікальним індексом і на повторний запит повертає збережений результат замість другого платежу.
Тут немає правильних відповідей: перевіряють, чи ви бачите компроміси. Сильна відповідь завжди містить «за яку ціну».