
Модульний моноліт на NestJS: практичний посібник з архітектури
- NestJS
- modular monolith
- bounded modules
- domain events
- transactions
- architecture testing
- microservices

Модульний моноліт на NestJS: практичний посібник з архітектури
Модульний моноліт — це один застосунок для розгортання, складений із бізнес-модулів із реальними межами. Він зберігає експлуатаційні переваги єдиного процесу: локальні виклики, зрозуміле налагодження, атомарні транзакції бази даних і один конвеєр випуску. Водночас така побудова стримує зв’язність, через яку звичайний моноліт із кожною зміною потребує дедалі більше зусиль.
Для багатьох продуктів це вдалий початковий вибір. Команда може вивчати предметну область і швидко розвивати систему, не оплачуючи завчасно мережеві відмови, розподілені дані, експлуатацію сукупності служб і наскрізне спостереження за ними. Проте слово «модульний» має означати відповідальність, яку можна захистити правилами. Самі лише каталоги й декоратори NestJS не завадять одній функціональній ділянці імпортувати сховище іншої.
NestJS надає потрібні механізми: модулі за замовчуванням приховують зареєстровані служби, властивість imports поєднує модулі, а exports визначає служби, відкриті споживачам. Проте архітектуру утворюють рішення про предметні можливості, мову, відкриті контракти, відповідальність за дані, транзакції, напрямки залежностей і право ухвалювати зміни. Коли ці рішення явні й захищені випробуваннями, моноліт може роками залишатися придатним до розвитку та забезпечувати чесний шлях до майбутнього виділення служб.
Починайте з меж бізнесу, а не зі структури тек
Визначайте модулі за можливостями бізнесу та його незмінними правилами, а не за горизонтальними технічними шарами. «Замовлення», «Розрахунки», «Запаси», «Виконання» та «Ідентифікація» можуть бути доречними межами, адже відповідають на різні бізнес-питання й змінюються з різних причин. Модуль має відповідати за сценарії використання, правила, термінологію, відображення об’єктів у сховище та міграції, потрібні для захисту його рішень. Контролери й обробники повідомлень слугують адаптерами, а сховища даних залишаються внутрішнім механізмом.
Проводьте сесії EventStorming або цільові зустрічі з фахівцями, які добре знають робочі процеси. Визначайте команди, факти, що вже відбулися, правила, суперечності в термінах, вимоги до узгодженості й тих, хто схвалює зміни. Наприклад, «клієнт» може означати покупця в модулі замовлень, юридичний рахунок у розрахунках та обліковий запис у керуванні доступом. Одна примусово спільна модель здатна створити більше зв’язності, ніж перетворення ідентифікаторів на межі.
Закріпіть кожну таблицю, чергу, планове завдання, ключ кешу та зовнішній контракт за одним модулем-власником. Інший модуль може зберігати ідентифікатор або локальний знімок, але не повинен змінювати таблиці власника чи імпортувати його сутність ORM. Спільна інфраструктура бази даних допустима, спільна відповідальність за запис — ні. У системах з ізольованим обслуговуванням багатьох клієнтів та сама межа має зберігати поділ їхніх даних, описаний у багатоклієнтській архітектурі NestJS.
Справді загальні механізми — налаштування, годинники, структуроване журналювання, трасування, криптографічні примітиви та підключення до бази даних — тримайте в невеликому платформному шарі без правил бізнесу. Не перетворюйте каталог shared на звалище всього спільного. Якщо об’єкт-значення малий і стабільний, його дублювання може коштувати менше за залежність, що змушує узгоджувати непов’язані випуски.
Залиште кожному модулю невеликий відкритий API
Ставтеся до exports модуля як до архітектурної межі, а не до переліку для зручності. NestJS приховує зареєстровані служби, доки модуль не відкриє їх, тому назовні мають виходити лише спеціалізовані фасади, порти команд і запитів та сталі ідентифікатори контрактів. Контролери, обробники, сховища, сутності ORM, засоби транзакцій і внутрішні служби залишайте закритими. Не робіть модулі предметних можливостей глобальними: невидима повсюдна доступність руйнує напрямок залежностей.
Відкрита операція має передавати намір бізнесу: оформити замовлення, зарезервувати товар, розрахувати ціну або отримати затверджену модель читання. Не повертайте конструктор запитів чи сутність сховища, які дозволяють викликачеві довільно керувати внутрішнім зберіганням. Використовуйте незалежні від способу передавання контракти запиту й відповіді, перевіряйте дані на вхідній межі та перетворюйте їх на предметні значення. Споживач має залежати від обіцянки модуля, а не від способу її виконання.
Обирайте між синхронним викликом і подією за змістом взаємодії. Якщо викликач не може ухвалити рішення без негайної відповіді, звертайтеся до власника через його відкритий порт. Якщо інший модуль лише реагує на вже підтверджений факт, публікуйте предметну або інтеграційну подію, сформульовану в минулому часі. Подія не повинна маскувати запит, якому потрібна відповідь, а синхронний фасад — ставати необмеженим чорним ходом до чужого модуля.
Розвивайте відкриті межі з тією самою дисципліною, що й зовнішній API. За можливості додавайте нове без руйнування сумісності, перед зміною поведінки закріплюйте її випробуваннями поточної поведінки та контрактів, а застарілі можливості вилучайте на підставі даних про використання. Ця межа також дає змогу провести поступовий перехід з Express на NestJS: старі й нові адаптери звертаються до того самого прикладного порту, поки реалізація змінюється за ним.
// orders/public/orders.port.ts
export abstract class OrdersPort {
abstract place(command: PlaceOrder): Promise<PlacedOrder>;
abstract find(id: string): Promise<OrderView | null>;
}
// orders/orders.module.ts
@Module({
imports: [OrdersPersistenceModule],
controllers: [OrdersController],
providers: [
PlaceOrderHandler,
FindOrderHandler,
{ provide: OrdersPort, useClass: NestOrdersFacade },
],
exports: [OrdersPort], // the complete public module API
})
export class OrdersModule {}
// Billing imports OrdersPort; it never imports OrdersRepository,
// OrderEntity, or a handler from an orders/internal path.Обмежуйте транзакції модулем і надійно передавайте події
Кожне правило, що потребує суворої узгодженості, розміщуйте всередині одного модуля й однієї локальної транзакції. Якщо під час оформлення замовлення потрібно перевірити його позиції та суму, модуль замовлень має володіти цими значеннями й записувати їх атомарно. Транзакція, що без обмежень змінює таблиці замовлень, запасів і розрахунків, лише створює видимість безпеки, водночас зчіплюючи їхні схеми та рішення про випуски.
Наскрізні процеси між модулями потребують явного вибору компромісів. Резервуйте товар синхронно, якщо без відповіді замовлення не можна прийняти. Коли кроки можуть завершуватися незалежно, застосовуйте надійно збережений процес із повторними спробами та компенсаційними діями. Визначте ідемпотентність, граничний час очікування, часткову відмову, втручання оператора й те, що побачить користувач. CQRS, саги та керовані робочі процеси допомагають упорядкувати справжню складність, але не мають прикрашати прості операції CRUD.
Події в пам’яті придатні для реакцій, втрата яких допустима, але можуть зникнути під час аварії або випуску. Якщо надіслати повідомлення брокерові після підтвердження транзакції, між успішним записом і доставленням виникає розрив; якщо до підтвердження — можна оголосити стан, який згодом буде скасовано. Для інтеграційної події, що мусить пережити відмову, використовуйте шаблон Transactional Outbox: записуйте повідомлення до транзакційної вихідної скриньки разом зі станом власника. Окремий виконавець забирає запис, публікує його й позначає доставленим, фіксуючи спроби та затримку.
Виходьте з гарантії «щонайменше один раз». Кожне повідомлення повинно мати сталий ідентифікатор, тип події та версію схеми, ідентифікатор сукупності об’єктів, час виникнення, дані для зв’язування подій і обмежене корисне навантаження. Споживачі мають запам’ятовувати опрацьовані ідентифікатори або виконувати записи природно ідемпотентно. Стежте за віком неопублікованих подій, кількістю спроб, чергою невручених повідомлень, відставанням споживачів, дублікатами та звірянням стану. Ці гарантії однаково потрібні для внутрішнього модуля, фонового завдання BullMQ і ширшої подієвої системи на NestJS.
await dataSource.transaction(async (manager) => {
const order = Order.place(command);
await manager.getRepository(OrderEntity).save(mapOrder(order));
await manager.getRepository(OutboxMessage).insert({
id: randomUUID(), // stable consumer idempotency key
type: 'orders.order-placed.v1',
aggregateId: order.id,
payload: JSON.stringify({
orderId: order.id,
customerId: order.customerId,
totalMinor: order.totalMinor,
}),
occurredAt: new Date(),
publishedAt: null,
});
});
// A worker claims unpublished rows, publishes them, and records delivery.
// Retries can create duplicates, so consumers must be idempotent.Перетворіть правила залежностей на автоматичні перевірки
Архітектурна документація пояснює задум, а автоматичні правила зберігають його під тиском строків. Визначте дозволені напрямки залежностей і відкриті точки входу. Зупиняйте CI, якщо модуль імпортує внутрішній шлях іншого модуля, предметний код залежить від NestJS чи пакетів ORM, з’являється цикл або прикладний шар безпосередньо звертається до сховища даних іншого власника.
Застосовуйте правила компілятора й аналізатора коду, засоби побудови графів залежностей та цільові архітектурні випробування. Повторне використання forwardRef вважайте приводом дослідити межу, а не звичайним виправленням: цей засіб розв’язує проблему впровадження залежностей, проте не пояснює, чому дві предметні можливості залежать одна від одної. Перевіряйте, щоб файли повторного експорту не створювали циклів на рівні файлів, а динамічні чи глобальні модулі — кількох або невидимих примірників служб.
Випробовуйте кожен модуль через його відкритий API. Швидкі предметні випробування захищають незмінні правила без NestJS. Прикладні випробування виконують команди й запити через керовані порти. Інтеграційні випробування звертаються до справжнього сховища, коли значення мають обмеження, блокування, транзакції чи відображення об’єктів. Контрактні випробування захищають схеми відкритих запитів, відповідей і подій. Невеликий набір наскрізних сценаріїв перевіряє з’єднання модулів та адаптери доставлення, не перетворюючи кожне правило на повільне випробування через HTTP.
Робіть порушення зрозумілими для виправлення: показуйте заборонену залежність, відповідальний модуль, дозволені способи взаємодії та настанови з перенесення. Для винятків із правил меж установлюйте відповідального й строк дії, замість того щоб послаблювати правило для всієї системи. Періодично переглядайте граф модулів і зв’язність змін: якщо кожна нова можливість змушена обходити певну межу, межу або обрано невдало, або їй бракує потрібної відкритої операції.
Виділяйте службу лише тоді, коли це підтверджують докази
Проєктуйте межі так, щоб модуль можна було виділити, але не вдавайте, ніби мережа вже існує. Модуль-кандидат має відповідати за доступ до своїх даних, відкривати контракти, придатні до передавання, випускати спостережувані події, не залежати від неявного контексту процесу й безпечно опрацьовувати повтори. Залишайте виклики локальними, доки достатньо одного розгортання. Завчасне додавання HTTP, виявлення служб, розподіленого трасування й узгодженості із затримкою створює справжні режими відмови заради уявної користі.
Виділяйте службу з вимірюваних причин: предметній можливості потрібні незалежний ритм випусків, істотно інше масштабування, сильніша ізоляція безпеки чи відмов, окремий цільовий рівень доступності, певне географічне розміщення або стала відповідальність автономної команди. Великий обсяг коду й організаційна мода — слабкі підстави. До схвалення поділу кількісно оцініть обмеження та експлуатаційну вартість ще однієї служби.
Готуйте перехід на наявному відкритому порті. Забезпечте сумісність контрактів із попередніми версіями, простежте виклики й залежності, визначте цільові показники служби та відповідальність за дані. Перенесіть реалізацію за транспортний адаптер, перемістіть або відтворіть власні дані зі звірянням і додайте граничний час очікування, повторні спроби, захист від каскадних відмов, автентифікацію та ідемпотентність. Якщо залучено шлюз API, залиште йому маршрутизацію та правила на зовнішній межі, а не витіснену з модуля предметну логіку.
Випускайте зміни поступово. Де можливо, порівнюйте старі й нові результати, спрямовуйте на новий шлях обмежену групу користувачів, спостерігайте за затримкою, помилками, насиченням ресурсів, відставанням подій і результатами для бізнесу, а тоді збільшуйте частку запитів. Заздалегідь визначте останню безпечну точку відкату й спосіб узгодження записів після повернення. Виділення завершене лише тоді, коли старий шлях, доступ до даних та експлуатаційну відповідальність вилучено, а не коли друге розгортання вперше повернуло код HTTP 200.
Перелік перевірок для впровадження модульного моноліту
- Межі: до створення модулів NestJS нанесіть на карту можливості бізнесу, мову, незмінні правила, право на зміни, дані, завдання, кеші та зовнішні контракти.
- Відкрита межа: надайте невеликий фасад, порт або контракт події; контролери, обробники, сховища, сутності, транзакції та внутрішнє керування залиште закритими.
- Узгодженість: тримайте кожне незмінне правило й транзакцію із суворою узгодженістю в одного власника; для надійного доставлення застосовуйте вихідну скриньку, версійовані події, ідемпотентних споживачів, звіряння та контроль відставання.
- Дотримання правил: зупиняйте CI через внутрішні імпорти між модулями, заборонений напрямок шарів, залежність предметного коду від програмної платформи та цикли; перевіряйте модулі через відкриті API й справжнє сховище там, де від нього залежить поведінка.
- Виділення: створюйте окрему службу лише через виміряну потребу в незалежному випуску, масштабуванні, ізоляції, розміщенні, доступності чи відповідальності; після цього планомірно переносьте контракти, дані, рух, телеметрію та відкат.
Чи кожен модуль NestJS автоматично є обмеженим контекстом?
Чи можуть модулі моноліту спільно використовувати одну базу даних?
Чи достатньо надійні події всередині процесу для важливих операцій?
За яких умов модуль NestJS слід виділити в мікросервіс?
Надайте граф модулів і залежностей, розподіл відповідальності за дані, критичні транзакції, потоки подій, межі команд та обмеження випуску. Ми знайдемо зв’язність, що справді заважає розвитку, і визначимо поступовий шлях до модульної побудови з випробуваннями, доказами прийнятності та відкатом.
Наші дослідження
Дослідження та розробка рішень на основі штучного інтелекту для оптимізації бізнес-процесів і підвищення ефективності прийняття рішень.
Аналіз моделей машинного навчання для прогнозної аналітики у фінансовій сфері, електронній комерції та SaaS-платформах.
Дослідження технологій обробки природної мови та комп'ютерного зору для посилення автоматизації, персоналізації та підтримки клієнтів.


