Межу видно з мови, і найпростіший орієнтир тут омонім. У рекрутинговому продукті «кандидат» для підбору це воронка з етапами, резюме й нотатками рекрутера; для перевірки на комплаєнс це пакет документів зі статусами й датами дії; для білінгу це одиниця тарифікації з датою активації. Атрибути перетинаються відсотків на тридцять, змінюються з різних причин і за різними правилами. Другий орієнтир дає інваріант: те, що зобовʼязане лишатися узгодженим в одному COMMIT, не ріжеться навпіл, тому межа контексту завжди йде по зовнішньому краю таких правил. Третій орієнтир організаційний, і його зазвичай недооцінюють: якщо зміни в двох частинах системи замовляють різні люди з різним ритмом, ці частини вже живуть окремо, і код рано чи пізно це визнає.
Розділення за таблицями дає протилежний ефект. Схема бази описує спосіб зберігання, а не бізнес-можливості, тому модулі Users, Orders і Notifications мають рівно ту саму звʼязність, що й до розділення, тільки з новими namespace: будь-яка осмислена фіча проходить крізь усі три. Свої межі швидко перевіряють по історії комітів: подивіться, які файли зазвичай змінюються разом. Стабільні кластери спільних змін показують справжні модулі, і якщо вони не збігаються з теками, теки треба переставити, а не захищати. Гіпотезу межі корисно проговорити вголос: у контексту має бути назва з мови бізнесу і зрозуміла відповідь на питання, які рішення він приймає самостійно.
Зі спільними даними працює правило одного власника факту: у кожного факту рівно один контекст, який має право його змінювати. Далі є сходинки, і спускатися ними варто лише за потреби. Найдешевше читати у власника через порт, тобто через інтерфейс, оголошений споживачем. Якщо читання гаряче або власник може бути недоступним, споживач тримає власну проєкцію, оновлену подіями (у моноліті це листенер плюс outbox, щоб подія не поїхала раніше за COMMIT), і свідомо приймає відставання разом із джобою звірки. Окремо стоять дані, які треба заморозити: ціна й назва на момент замовлення стають власним фактом замовлення і від каталогу більше не залежать. Посилання між контекстами живуть як ідентифікатори: без зовнішнього ключа, без JOIN і без Eloquent-звʼязку, бо FK намертво звʼязує міграції двох частин і порядок їхнього деплою. У PostgreSQL контекст природно лягає на окрему schema з власною роллю, у MySQL schema є синонімом бази, тож беруть окрему базу або префікс плюс користувача з правами тільки на свої таблиці. Права тут працюють краще за домовленості: доки технічно можливо зробити JOIN у чужу таблицю, він зʼявиться в першому ж терміновому фіксі.
Зі спільним кодом рішення протилежне звичному інстинкту. Shared kernel має сенс для дрібного, стабільного і позбавленого бізнес-правил: Money, типи ідентифікаторів, Clock, базові контракти помилок. Ядро перебуває у спільному володінні, зміни узгоджуються обома сторонами, і саме ця вартість узгодження тримає його малим. Усе інше, що виглядає однаково, дублюється. Два класи Client по пʼять полів дешевші за один спільний, який через рік має loyalty_tier, kyc_status і unsubscribed_at, причому кожне поле потрібне комусь одному. Тут дублювання структури купує незалежність релізів. А от повторення бізнес-правила в двох контекстах, скажімо однакового розрахунку знижки, сигналізує про помилкову межу: правило належить комусь одному, і другий має його питати, а не переписувати.
Антикорупційний шар потрібен там, де чужа модель заходить усередину вашої. Він стоїть з боку споживача, за портом, який споживач оголосив своєю мовою, і за цю межу не виходять чужі назви полів, коди станів, одиниці виміру й формати дат. Від мапера ACL відрізняється тим, що приймає рішення: контексту підбору потрібне не поле STATE, а відповідь «чи може цей роботодавець публікувати вакансії», і коли постачальник додасть новий стан, змінюється один клас. Карта контекстів Еванса пропонує й дешевші стосунки: conformist, тобто прийняти чужу модель як є, цілком розумний, коли постачальник стабільний і його модель адекватна; open host service з published language перекладає роботу на бік постачальника, якщо споживачів багато; separate ways чесно фіксує, що інтеграція коштує дорожче за користь. ACL обирають під legacy, зовнішній API або шматок моноліту, який ви душите strangler-підходом.
Ціна всього цього конкретна: копії застарівають, тож потрібні звірки й дашборд розбіжностей; кількість коду зростає; частина запитів, які були одним JOIN, перетворюється на два звернення. Тому контекстів має бути стільки, скільки в системі справді різних мов, і жодним більше. Про помилкову межу найпевніше сигналить сценарій, який регулярно вимагає атомарної транзакції через два контексти: ви розрізали інваріант навпіл, і межу треба переносити, а не героїчно обходити sagʼою.
// Контекст «Підбір» описує роботодавця власною мовою і власним правилом.
final readonly class Employer
{
public function __construct(
public EmployerId $id,
public string $displayName,
public bool $canPublishVacancies,
) {}
}
interface Employers
{
public function byId(EmployerId $id): ?Employer;
}
// Антикорупційний шар: назви полів і коди станів білінгу далі не йдуть.
final readonly class BillingEmployers implements Employers
{
public function __construct(private LegacyBillingApi $billing) {}
public function byId(EmployerId $id): ?Employer
{
// ['ACC_NAME' => ..., 'STATE' => 'A'|'S'|'X', 'BALANCE_UAH' => ...]
$account = $this->billing->fetchAccount($id->value);
if ($account === null) {
return null;
}
return new Employer(
id: $id,
displayName: trim($account['ACC_NAME']) ?: 'Роботодавець',
// Чужий стан перекладається в наше правило, а не копіюється полем
canPublishVacancies: $account['STATE'] === 'A'
&& (int) $account['BALANCE_UAH'] >= 0,
);
}
}
// Гаряче читання йде з власної проєкції, джерело правди лишається в білінгу.
final class ProjectEmployerStatus
{
public function __invoke(AccountStatusChanged $event): void
{
// Таблиця належить «Підбору»: білінг у неї не пише і не робить JOIN.
DB::table('hiring_employers')->updateOrInsert(
['employer_id' => $event->accountId],
['can_publish' => $event->isActive, 'synced_at' => $event->occurredAt],
);
}
}
Наведіть конкретний омонім зі свого продукту і покажіть, як він розводиться по контекстах, а потім одразу дайте правило одного власника факту: хто пише, хто читає через контракт, у кого лежить копія і чим ця копія оновлюється. Окремо назвіть, що ви віддали в shared kernel, і чому там немає жодного бізнес-правила.