← Усі статті

Найдорожча помилка в архітектурі ERP — спроба зробити систему універсальною

Чому ілюзія універсальності перетворює ERP на EAV-монстра: втрата продуктивності, типізації та бізнес-сенсу — і як будувати систему навколо предметної області, а не абстрактних сутностей.

Зміст

Майже кожна ERP через кілька років після успішного впровадження починає гальмувати розвиток бізнесу. Звіти рахуються годинами. Просте поле в картці замовлення потребує погодження з трьома відділами й двома вендорами. Нові співробітники місяцями розбираються, «як тут взагалі влаштовано». Керівництво списує це на вік системи, нестачу адміністратора БД або «поганих розробників».

Але коренева причина часто глибша й дорожча за будь-який технічний борг. Це ілюзія універсальності — віра в те, що одна система може описати будь-який бізнес-процес, будь-яку сутність і будь-яке правило без програмування, через налаштування метаданих і «гнучку архітектуру».

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

Ця стаття — для архітекторів, CTO, власників ERP-продуктів і технічних лідерів, які проєктують систему сьогодні й через п’ять років не хочуть пояснювати бізнесу, чому «додати одне поле» коштує як міні-проєкт.

Ключові висновки

Універсальність — маркетингова ілюзія, а не архітектурна ціль. Один продукт «для всіх галузей» економить на розробці вендора, але перекладає складність на клієнта й його інтеграторів.

EAV і метадані — пастка для ядра ERP. Entity–Attribute–Value дає нескінченну гнучкість на демо й експоненційне зростання складності в продакшені.

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

Бізнес-логіка не абстрактна. Склад, виробництво, бухгалтерія й CRM вирішують різні задачі. Один універсальний механізм для замовлення, рахунку й виробничої операції втрачає сенс предметної області.

Гарна ERP — спеціалізоване ядро плюс обмежені точки розширення. 80% системи має бути типізованим доменом; 20% — контрольованою кастомізацією.

Найдорожча помилка в ERP — не поганий код. Це віра в те, що можна створити повністю універсальну систему для будь-яких бізнес-процесів.


Вступ: чому універсальність здається правильною відповіддю

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

Звідси народжується спокуса: якщо зробити систему достатньо абстрактною, вона підлаштується під будь-який склад, будь-яке виробництво, будь-яку схему погоджень. Продажі обіцяють «налаштувати без програмістів». Архітектор малює схему з Entity, Attribute, Value. Інвестори бачать масштабований продукт. Усі задоволені — до першого мільйона записів і п’ятого року експлуатації.


Розділ 1. Звідки береться ідея «універсальної ERP»

Історія проблеми

Коробкові ERP минулого — SAP, Oracle, 1С, Microsoft Dynamics — будувалися на ідеї одного продукту для багатьох галузей. Вендор не може дозволити собі окрему кодову базу для металургії, ритейлу й логістики. Економіка розробки диктує: один код, багато клієнтів, відмінності — у конфігурації.

Маркетинг посилює тренд. Слово «універсальна» продає краще, ніж «підходить для виробничих компаній середнього розміру з дискретною збіркою». Універсальність звучить як страховка: «якщо завтра відкриємо новий напрям — система підлаштується». Для замовника це знижує сприйнятий ризик. Для розробника — створює архітектурний тиск у бік абстракцій.

Як виглядає універсальність в очах бізнесу

Типові обіцянки на презентації:

  • Налаштувати можна все — будь-який процес описується через конфігуратор.
  • Будь-який процес без програмування — бізнес-аналітик сам збере робочий процес.
  • Нові сутності за хвилини — адміністратор створює «Об’єкт обліку» в інтерфейсі.
  • Будь-які поля через UI — прапорець «додати атрибут», тип обирається зі списку.

На демо це працює. Картка «Універсального документа» приймає десять полів. Звіт будується перетягуванням блоків. Замовник підписує контракт.

Чому ідея здається правильною

Логіка переконлива:

Аргумент Чому звучить розумно
Менше кастомної розробки Клієнт сам налаштовує, інтегратор лише допомагає
Швидше впровадження Не чекаємо спринт розробки на кожне поле
Один код для всіх клієнтів Вендор лагодить баг один раз — виграють усі
Масштабування бізнесу вендора Новий клієнт = нова ліцензія, не новий продукт

Проблема в тому, що ці вигоди реальні на горизонті першого року і перетворюються на витрати на горизонті п’яти–десяти років. Універсальність — це кредит під високий відсоток. Відсотки нараховуються в запитах до БД, у шарах абстракції в коді й у головах розробників.


Розділ 2. Перший крок до катастрофи — універсальні сутності

Мрія архітектора

На білій дошці схема виглядає елегантно:

┌──────────┐     ┌────────────┐     ┌───────────┐
│  Entity  │────▶│ Attribute  │────▶│   Value   │
│  (тип)   │     │  (имя,тип) │     │ (значение)│
└──────────┘     └────────────┘     └───────────┘

Три таблиці — і ви «описали всю всесвіт». Потрібен новий тип документа? Додаєте запис у entities. Потрібне поле «термін придатності»? Рядок у attributes. Значення лежать у values. Демо вражає. Архітектор отримує схвалення.

Це класичний патерн EAV (Entity–Attribute–Value) — або його варіації: JSONB з динамічною схемою, key-value сховище поверх реляційної БД, «універсальні» таблиці custom_fields.

Як народжується EAV

Типова структура:

-- Универсальная сущность
CREATE TABLE entities (
    id          BIGSERIAL PRIMARY KEY,
    entity_type VARCHAR(64) NOT NULL,  -- 'order', 'invoice', 'operation'
    created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Определения атрибутов
CREATE TABLE attributes (
    id           BIGSERIAL PRIMARY KEY,
    entity_type  VARCHAR(64) NOT NULL,
    code         VARCHAR(128) NOT NULL,
    data_type    VARCHAR(32) NOT NULL,  -- string, number, date, ref
    is_required  BOOLEAN DEFAULT false
);

-- Значения (горизонтальное хранение — один ряд на атрибут)
CREATE TABLE attribute_values (
    id           BIGSERIAL PRIMARY KEY,
    entity_id    BIGINT NOT NULL REFERENCES entities(id),
    attribute_id BIGINT NOT NULL REFERENCES attributes(id),
    value_string TEXT,
    value_number NUMERIC,
    value_date   DATE,
    value_ref    BIGINT
);

Зв’язки між сутностями теж стають універсальними: таблиця entity_relations з полями from_entity_id, to_entity_id, relation_type. Через рік у вас немає «замовлень» і «клієнтів» — є граф абстрактних вузлів.

Що відбувається через рік

Перші симптоми:

  • Запити ускладнюються. Вибірка «усі замовлення клієнта X зі статусом "відвантажено" і сумою > 100 000» перетворюється на п’ять JOIN і три підзапити з розгортанням рядків.
  • Індекси перестають допомагати. Складений індекс на (entity_type, ...) не покриває всі комбінації фільтрів за динамічними атрибутами.
  • Звіти гальмують. BI-інструмент або внутрішній конструктор звітів генерує SQL, який адміністратор БД дивиться з жахом.
  • Розробники бояться змінювати код. Будь-яка зміна в шарі метаданих може зламати десяток клієнтських конфігурацій.

Реальний приклад: нормалізоване замовлення vs EAV

Нормалізована модель — зрозуміла, типізована:

CREATE TABLE orders (
    id           BIGSERIAL PRIMARY KEY,
    client_id    BIGINT NOT NULL REFERENCES clients(id),
    status       order_status NOT NULL,  -- enum: draft, confirmed, shipped, ...
    total_amount NUMERIC(15,2) NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_client_status ON orders(client_id, status);
CREATE INDEX idx_orders_created ON orders(created_at);

-- Запрос: заказы клиента 42 со статусом 'shipped' за последний месяц
SELECT o.id, o.total_amount, o.created_at
FROM orders o
WHERE o.client_id = 42
  AND o.status = 'shipped'
  AND o.created_at >= now() - interval '1 month';

План виконання: сканування за індексом idx_orders_client_status, фільтр за датою. Передбачувано. Адміністратор БД задоволений.

EAV-представлення того ж замовлення:

-- «Заказ» — entity с type='order'
-- client_id хранится как attribute_values где attribute.code='client_id'
-- status — attribute.code='status'
-- total_amount — attribute.code='total_amount'

SELECT e.id,
       MAX(CASE WHEN a.code = 'total_amount' THEN av.value_number END) AS total_amount,
       e.created_at
FROM entities e
JOIN attribute_values av ON av.entity_id = e.id
JOIN attributes a ON a.id = av.attribute_id
WHERE e.entity_type = 'order'
  AND e.id IN (
      SELECT av2.entity_id
      FROM attribute_values av2
      JOIN attributes a2 ON a2.id = av2.attribute_id
      WHERE a2.code = 'client_id' AND av2.value_ref = 42
  )
  AND e.id IN (
      SELECT av3.entity_id
      FROM attribute_values av3
      JOIN attributes a3 ON a3.id = av3.attribute_id
      WHERE a3.code = 'status' AND av3.value_string = 'shipped'
  )
  AND e.created_at >= now() - interval '1 month'
GROUP BY e.id, e.created_at;

Те саме бізнес-питання — у три рази більше JOIN, GROUP BY, підзапитів. При 10 000 замовленнях ще терпимо. При 10 мільйонах — окремий проєкт з оптимізації, матеріалізовані представлення, денормалізовані копії «для звітів», по суті — друга, нормалізована модель, побудована поверх першої, щоб виправити архітектурну помилку.


Розділ 3. Універсальність знищує продуктивність

Чому база даних любить конкретику

Реляційні СУБД оптимізовані під стабільну схему:

  • Типізовані поля — СУБД знає розмір, порівнює числа як числа, дати як дати.
  • Передбачувані індекси — B-tree на (client_id, status) працює, бо поля фіксовані.
  • Прості плани виконання — оптимізатор оцінює селективність за статистикою стовпців, а не за евристиками запитів з розгортанням.

Коли схема динамічна, оптимізатор «сліпне». Статистика по value_string в EAV безглузда: в одному стовпці лежать і імена, і коди, і серіалізований JSON.

Що ламає універсальна архітектура

Проблема Наслідок
JOIN на JOIN на JOIN Лінійне зростання вартості запиту з числом атрибутів
Динамічні атрибути Неможливість скласти один ефективний індекс
Перетворення типів CAST(value_string AS NUMERIC) вбиває сканування лише за індексом
Розгортання у застосунку або SQL CPU на кожному рядку замість читання готових стовпців
Відсутність FK на рівні БД «Посилання» в value_ref не перевіряється — ростуть сироти й сміття

Експоненційне зростання складності

Чому проблеми помітні лише через кілька років:

Масштаб Що відбувається
100 записів Усе летить. EAV здається геніяльним.
10 000 Перші скарги на «повільний список». Додають кеш.
1 000 000 Адміністратора БД наймають на повну ставку. З’являється репліка для читання «лише для звітів».
100 000 000 Окрема команда платформи даних. ETL у колоночне сховище. Ядро ERP фактично обходять стороною.

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

Ціна помилки

Універсальна архітектура обкладає податком всю організацію:

  • Дорожчі сервери — більше CPU, RAM, швидкі диски компенсують погані запити.
  • Дорожчі адміністратори БД — потрібні люди, які вміють лагодити те, що не мало ламатися.
  • Дорожча розробка — кожна фіча проходить через шар метаданих, мапінгу й валідації.
  • Повільніші користувачі — оператори чекають форми, втрачають час, обходять систему в таблицях.

Загалом за п’ять років «економія на кастомній розробці» часто перевішується вартістю інфраструктури, підтримки й упущеної швидкості бізнесу.


Розділ 4. Універсальність знищує код

Початок виглядає красиво

На першому спринті API елегантний:

$entity = $entityRepository->find($orderId);
$price = $entity->getField('price');
$status = $entity->getField('status');

Один інтерфейс для всього. Новий тип документа — не новий клас, а конфіг. Розробники задоволені.

Через кілька років

Реальність:

  • Неможливо зрозуміти структуру даних — «які поля у замовлення?» відповідь: «дивись у таблицю attributes для entity_type=order, плюс оверрайди клієнта 17».
  • Немає автодоповнення IDEgetField('prcie') друкарська помилка в рядку, помилка лише в рантаймі.
  • Немає статичної типізації$price може бути string, null або масивом «для складового поля».
  • Немає безпечного рефакторингу — перейменувати price на total_amount = пошук по рядкових літералах по всій кодовій базі й у JSON-конфігах клієнтів.

Ефект снігової кулі

Щоб жити з універсальністю, з’являються шари:

Entity
  → EntityWrapper
    → TypedEntityAdapter (псевдотипизация)
      → FieldConverter (string → Money)
        → ValidationHelper
          → LegacyFieldAliasResolver (старое имя 'sum' → 'total_amount')

Кожен шар — відповідь на біль попереднього. Через п’ять років половина кодової бази обслуговує абстракцію, а не бізнес. Новий розробник читає не домен «замовлення / відвантаження / оплата», а внутрішній фреймворк метаданих, який ніде не задокументований повністю.

Коли розробники починають боятися системи

Типові сигнали:

  • Зміна однієї функції ламає звіти, інтеграції й три клієнтські конфігурації.
  • Код-рев’ю перетворюється на вгадування: «а що якщо у клієнта X увімкнено режим сумісності?»
  • Адаптація займає місяці — не тому що домен складний, а тому що домен схований за універсальним API.
  • «Не чіпай EntityService» стає неписаним правилом команди.

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


Розділ 5. Чому універсальність вбиває бізнес-логіку

Бізнес завжди конкретний

ERP на папері — «єдина система обліку». На практиці це стик різних світів:

Область Що важливо Що ламає універсальність
Склад Залишки, партії, адреси комірок, FIFO/FEFO «Універсальний документ руху» без семантики списання
Виробництво Специфікації, маршрути, брак, норми Операція = замовлення = рахунок в одній таблиці
Бухгалтерія Проводки, регістри, періоди, закриття Динамічні поля без подвійного запису
CRM Воронка, активності, контакти Гнучкість ціною відсутності жорстких правил погодження

Склад працює не так, як CRM. Виробництво — не так, як документообіг. Універсальний механізм усереднює поведінку до найменшого спільного знаменника.

Помилка абстракції

Архітектор намагається описати одним механізмом:

  • Замовлення — погодження, резерв, відвантаження.
  • Рахунок — ПДВ, проводки, оплата.
  • Виробничу операцію — норми, брак, облік часу.
  • Платіж — валюта, курс, комісія банку.

Усі вони стають entity_type з різним набором attributes. Бізнес-правила перетворюються на умови на метаданих:

Якщо entity_type = 'order' І attribute 'channel' = 'b2b'
  І attribute 'payment_terms' > 30
  Тоді workflow_id = 7
Інакше якщо entity_type = 'invoice' ...

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

Втрата сенсів

Система починає оперувати:

  • сутностями, атрибутами, зв’язками, workflow_id.

Замість:

  • замовленнями, клієнтами, поставками, платежами, специфікаціями, актами звірки.

Розробник і аналітик перестають говорити однією мовою з бізнесом. Між «нам потрібно блокувати відвантаження при простроченій дебіторці» і реалізацією — прослойка універсальної абстракції, яку ніхто з фінансів не розуміє.

Предметно-орієнтоване проєктування (DDD) якраз бореться з цим: спільна предметна мова, обмежені контексти, явні агрегати. Універсальна ERP — антипод DDD на рівні ядра.


Розділ 6. Чому універсальні ERP перетворюються на монолітних монстрів

Цей розділ перетинається з окремою темою — чому ERP стають монолітами — але універсальність прискорює деградацію.

Початок проєкту

  • Кілька типів сутностей в EAV.
  • Кілька екранів-конструкторів.
  • Проста логіка: «прочитай атрибути, покажи форму».

Через п’ять років

  • Сотні entity_type і тисячі attributes — частина застаріла, частина дублює одна одну.
  • Тисячі налаштувань на рівні клієнта, філії, ролі.
  • Десятки if (legacy_mode) і спеціальних гілок для «того самого» великого замовника.

Універсальність не стримує зростання — вона не дає природних меж. У нормалізованій моделі новий модуль — нові таблиці й сервіс. В EAV усе — той самий EntityService, той самий звітний рушій, та сама точка розширення. Моноліт роздувається зсередини без модульних швів.

Через десять років

  • Ніхто не розуміє систему цілком.
  • Документація описує «як задумано», а не «як працює зараз».
  • Будь-яка зміна — проєкт з оцінкою в людино-місяцях.

Симптоми архітектурної деградації

Симптом Прояв
Величезні сервіси EntityService на 15 000 рядків
Величезні контролери Одна кінцева точка «зберегти універсальний об’єкт» на всі випадки
Величезні SQL Звіти на 500 рядків з динамічним розгортанням
Величезні конфігурації JSON метаданих більший, ніж код модуля

Монстр тут — не лише наслідок успіху бізнесу. Це успіх бізнесу на фундаменті універсальної абстракції, який не витримує ваги реальності.


Розділ 7. Що працює краще за універсальність

Принцип 80/20

Практичне правило для архітектора ERP:

  • 80% системи — спеціалізовані, типізовані доменні модулі: замовлення, склад, фінанси, виробництво.
  • 20% — розширюваність: користувацькі поля, налаштування UI, вебхуки, плагіни.

Спроба зробити 80% універсальним переносить складність у ті 20%, які мали залишитися простими.

Ядро і розширення

Здорова структура:

┌─────────────────────────────────────────┐
│              Core (типизированный)       │
│  Orders │ Inventory │ Finance │ Mfg     │
└─────────────────┬───────────────────────┘
                  │ явные API / события
┌─────────────────▼───────────────────────┐
│  Domain modules (границы по контексту)   │
└─────────────────┬───────────────────────┘
                  │ plugin contract
┌─────────────────▼───────────────────────┐
│  Plugins / custom fields / integrations  │
└─────────────────────────────────────────┘

Core не знає про плагіни деталей реалізації — лише про контракти. Плагіни не лізуть у ядро через спільну таблицю Entity.

Предметно-орієнтоване проєктування

Чому бізнес-сутності важливіші за технічні абстракції:

  • Обмежений контекст — склад і бухгалтерія можуть по-різному називати «товар», і це нормально; інтеграція через антикорупційний шар.
  • Агрегати — замовлення як корінь з інваріантами: не можна відвантажити більше, ніж зарезервовано.
  • Доменні подіїOrderShipped, а не EntityUpdated(type=order, diff=...).

Технічна абстракція економить рядки коду на старті. Доменна модель економить роки супроводу.

Обмежена гнучкість краща за нескінченну

Нескінченна гнучкість Обмежена гнучкість
Будь-яке поле де завгодно Користувацькі поля лише в дозволених сутностях
Будь-який робочий процес Каталог шаблонів + параметри
Будь-який зв’язок Явні FK і доменні сервіси
«Налаштуй без коду» скрізь Конструктори без коду лише на периферії

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


Розділ 8. Як будувати ERP правильно

Спочатку предметна область

Старт не з таблиць і форм, а з:

  • Процесів — як замовлення перетворюється на відвантаження й оплату.
  • Ролей — хто що бачить і хто що підтверджує.
  • Подій — що відбувається при зміні статусу, які побічні ефекти.
  • Правил бізнесу — інваріанти, які не можна порушити заради «гнучкості».

Робоча сесія з власником процесу → глосарій → модель → схема БД. Не навпаки.

Типізовані моделі там, де це можливо

Ядро описується явними сутностями:

interface Order {
  id: OrderId;
  clientId: ClientId;
  status: OrderStatus;
  lines: OrderLine[];
  total: Money;
  createdAt: Instant;
}

// Инвариант в домене, не в getField()
function ship(order: Order, inventory: Inventory): Result<Shipment, DomainError> {
  if (order.status !== 'confirmed') return err('ORDER_NOT_CONFIRMED');
  // ...
}

Замовлення, товар, складська комірка, платіж — окремі типи з окремими правилами. Спільне виноситься в спільне ядро (гроші, адреси, одиниці виміру), не в «універсальний документ».

Метадані лише там, де вони дійсно потрібні

Гарні кандидати на метадані:

  • користувацькі поля в картці клієнта (обмежений набір типів);
  • налаштування інтерфейсу, видимість стовпців;
  • конструктори простих форм для опитувань і заявок;
  • довідники, які дійсно змінюються без релізу (обережно).

Погані кандидати:

  • структура замовлення й рядків замовлення;
  • проводки й регістри бухобліку;
  • складські рухи й залишки;
  • виробничі специфікації.

Якщо від поля залежить коректність обліку або податків — воно не має бути attribute_id = 847.

Архітектура як набір обмежень

Сильний архітектор ERP не питає «як зробити, щоб можна було все». Він питає:

  • які зміни мають проходити через реліз і код-рев’ю;
  • де допустиме налаштування силами інтегратора;
  • де межа між продуктом і кастомом конкретного клієнта.

Обмеження — не слабкість. Це збереження цілісності системи, на якій тримається бізнес.


Розділ 9. Винятки: коли універсальність дійсно потрібна

Універсальність — не гріх. Гріх — застосовувати її в ядрі ERP, де ціна помилки максимальна.

Платформи без коду

Коли продукт сам по собі — конструктор (Airtable, Retool, внутрішня платформа заявок), EAV або сховище документів виправдані. Користувач розуміє компроміс: гнучкість важливіша за продуктивність і жорстку типізацію. Це не ERP для заводу на 10 000 SKU.

Конструктори CRM

Воронка продажів, кастомні поля ліда, довільні стадії — класична зона метаданих. Помилка в картці ліда рідко ламає баланс на рахунку 51. Контекст інший.

Прототипування

Мінімальний продукт за шість тижнів, вимоги плавають — універсальна модель прискорює перевірку гіпотези. План виходу має бути явним: «після підтвердження попиту переписуємо замовлення в типізовану модель». Без плану прототип стає спадковим кодом.

Дослідницькі проєкти

Коли домен невідомий — гнучка схема допомагає вчитися. Щойно домен стабілізувався (зазвичай після 2–3 ітерацій з реальними користувачами), фіксуйте модель. Дослідження без кристалізації — той самий кредит під відсотки.


Висновок

Головна думка

Найдорожча помилка в архітектурі ERP — не поганий код і не «не той» PostgreSQL vs Oracle. Найдорожча помилка — віра в повністю універсальну систему для будь-яких бізнес-процесів.

Чим більше архітектура прагне до універсальності, тим сильніше вона:

  • втрачає продуктивність — запити, індекси, інфраструктура;
  • втрачає зрозумілість — для людей і для IDE;
  • втрачає типізацію — помилки потрапляють у прод;
  • втрачає передбачуваність — кожна зміна лотерея;
  • збільшує вартість підтримки — адміністратори БД, розробка, інтегратори, простій бізнесу.

Гарна ERP не універсальна

Гарна ERP — це спеціалізоване ядро, що відображає реальні процеси компанії або галузі, плюс обмежений набір продуманих механізмів розширення: плагіни, події, користувацькі поля на периферії, явні API між модулями.

Універсальність добра в маркетинговій презентації й у прототипі. В ядрі системи, через яку проходять гроші, товар і відповідальність — це найдорожча помилка, яку можна закласти в фундамент.

Якщо ви проєктуєте ERP сьогодні, запитайте себе не «як описати будь-який об’єкт», а «яке замовлення, який склад і який платіж ми не можемо дозволити собі втратити в абстракції». Відповідь на це питання — і є архітектура.