Архітектурні рішення веб-додатків: від ідеї до продакшену
Уявіть: ваш стартап зростає, навантаження подвоюється кожні три місяці, а база даних починає гальмувати. Монолітний додаток, який писали нашвидкуруч, більше не тягне. Замість того щоб переписувати все з нуля, ви наймаєте архітектора. Він дивиться на поточний код, виявляє вузькі місця та пропонує план рефакторингу. Ми робимо те саме — але до того, як система впаде.
Архітектура — це набір рішень, які складно змінити потім. Вибір бази даних, спосіб організації сервісів, стратегія масштабування — кожне рішення закладає межі того, що можна побудувати через рік без переписування. Хороше архітектурне рішення враховує реальні обмеження: розмір команди, очікувані навантаження, бюджет на експлуатацію та швидкість змін продукту.
Наш досвід — 12 років у проєктуванні веб-архітектур та понад 50 успішних проєктів. Ми гарантуємо задокументовані рішення з ADR та планом міграції. Правильно спроєктована архітектура економить до 40% на експлуатації та окупається за 2–3 місяці.
З чого починається проєктування
Перш ніж вибирати технології, потрібно відповісти на структурні питання:
- Характер навантаження визначає стратегію кешування. Read-heavy (новинарний портал) — одна стратегія, Write-heavy (біржа) — інша, Mixed (e-commerce) — третя.
- Допустима затримка. Для торгової платформи 100ms — катастрофа, для CMS — прийнятно.
- Піки трафіку. Black Friday дає 100x навантаження — потрібен autoscaling або буферизація через черги.
- Межі транзакційності. Чи можна розділити базу даних або все зав'язано на ACID.
Шари типового веб-додатку
[Клієнт]
↓ HTTPS
[CDN / Edge Cache]
↓ Cache Miss
[Load Balancer]
↓
[Додаток — N інстансів]
├── [Кеш — Redis/Memcached]
├── [Черга — RabbitMQ/Kafka]
└── [База даних — Primary + Replica]
↓
[Object Storage — S3]
Кожен шар вирішує одне завдання. CDN — статика та кеш на краю. Load Balancer — розподіл і термінування TLS. Додаток — бізнес-логіка. Redis — гарячі дані та сесії. Черга — асинхронні завдання, які не можна виконати в межах HTTP-запиту.
Моноліт чи мікросервіси: що вибрати?
Стандартне питання, на яке часто дають неправильну відповідь. Моноліт — правильний вибір для більшості нових проєктів з командою до 15–20 осіб. Причини:
- Одна транзакція на кілька агрегатів без saga-патернів.
- Простий деплой і спостережуваність (один процес — один лог).
- Рефакторинг без мережевих контрактів.
- Немає проблеми узгодженості при розподілених даних.
Перехід до мікросервісів виправданий, коли команди працюють над незалежними доменами, деплої починають заважати, і конкретні сервіси потребують різного масштабування (наприклад, сервіс обробки зображень vs CRUD API). За нашими оцінками, мікросервіси дають приріст продуктивності в 1.5–2 рази при правильній декомпозиції.
| Критерій | Моноліт | Мікросервіси |
|---|---|---|
| Розмір команди | до 20 осіб | від 20+ осіб |
| Складність деплою | низька | висока (деплой кожного сервісу) |
| Транзакційність | проста (ACID) | складна (Saga, 2PC) |
| Масштабування | вертикальне | горизонтальне (за сервісами) |
| Вартість експлуатації | нижча | вища (оркестрація, моніторинг) |
Моноліт із чіткими межами модулів:
src/
├── modules/
│ ├── catalog/ # продукти, категорії, пошук
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ ├── orders/ # замовлення, кошик, checkout
│ ├── users/ # аутентифікація, профілі
│ └── notifications/ # email, push, sms
└── shared/
├── events/ # доменні події (для майбутньої декомпозиції)
└── infrastructure/ # HTTP клієнт, логгер
Така структура дозволяє витягти модуль у сервіс, коли це стане необхідним — межі вже проведені.
Чому PostgreSQL — правильний вибір для більшості проєктів?
PostgreSQL вирішує 90% завдань. Реляційна модель, JSONB для гнучких даних, повнотекстовий пошук, партиціювання, реплікація — все з коробки. Починати з PostgreSQL і змінювати при конкретних проблемах — правильна стратегія. Наприклад, PostgreSQL з JSONB обробляє запити до вкладених документів у 2–3 рази ефективніше за MySQL при однаковому навантаженні.
Додаткові сховища за призначенням:
| Завдання | Інструмент |
|---|---|
| Сесії, кеш, rate limiting | Redis |
| Повнотекстовий пошук з фасетами | Elasticsearch / OpenSearch |
| Аналітика та OLAP | ClickHouse |
| Граф-дані | Neo4j / PostgreSQL з recursive CTE |
| Черги повідомлень | Redis Streams, RabbitMQ, Kafka |
Як спроєктувати схему даних без помилок?
Ранні помилки в схемі даних — найдорожчі. Декілька принципів:
Використовуйте UUID замість serial/bigint для ID, якщо планується горизонтальне масштабування або публічний API. UUID v7 сортований і добре працює як кластерний індекс.
-- UUID v7 генерується в додатку
CREATE TABLE orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'draft',
total_cents INTEGER NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'RUB',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Тригер для updated_at (краще ніж в ORM)
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW EXECUTE FUNCTION trigger_set_timestamp();
Міграції — тільки вперед, не backward-incompatible. Цикл: додаємо колонку (nullable) → деплоїмо код, який її пише → робимо NOT NULL з DEFAULT → видаляємо стару колонку.
Кешування
Три рівні:
HTTP кеш — для публічних ресурсів. Cache-Control: public, max-age=3600, stale-while-revalidate=86400. CDN кешує на краю, браузер — локально.
Application cache — Redis для даних, які дорого обчислювати. Патерн Cache-Aside:
async function getProduct(id: string): Promise<Product> {
const cached = await redis.get(`product:${id}`);
if (cached) return JSON.parse(cached);
const product = await db.product.findUniqueOrThrow({ where: { id } });
await redis.set(`product:${id}`, JSON.stringify(product), 'EX', 3600);
return product;
}
// Інвалідація при оновленні
async function updateProduct(id: string, data: Partial<Product>) {
const updated = await db.product.update({ where: { id }, data });
await redis.del(`product:${id}`);
// Інвалідуємо залежні ключі
await redis.del(`category:products:${updated.categoryId}`);
return updated;
}
Query cache — PostgreSQL сам кешує плани запитів. Правильні індекси важливіші за будь-який application-рівень.
Асинхронна обробка
Все, що займає більше 200ms або може впасти, повинно йти в чергу:
- Відправка email
- Генерація PDF/зображень
- Інтеграції з зовнішніми сервісами
- Імпорт даних
- Перерахунок агрегатів
// Патерн: API приймає, ставить у чергу, відповідає 202
app.post('/api/orders/:id/invoice', async (req, res) => {
const { id } = req.params;
await queue.add('generate-invoice', {
orderId: id,
userId: req.user.id,
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 },
});
res.status(202).json({ message: 'Рахунок генерується, надішлемо на email' });
});
Спостережуваність
Три стовпи: логи, метрики, трасування.
Структуровані логи (Pino). Прив'язуємо request-id до всіх логів у межах запиту. Метрики через Prometheus-формат: /metrics endpoint з RED-метриками (Rate, Errors, Duration) на кожен роут.
Типові помилки при проєктуванні
- Вибір мікросервісів «на майбутнє» без реальної необхідності.
- Відсутність ADR — рішення не документуються, їх складно переглянути.
- Ігнорування обмежень команди та бюджету.
- Пізнє усвідомлення необхідності кешування.
Що входить у проєктування архітектури
Проєктування архітектури — це ітеративний процес. У нашу послугу входить:
- Аналіз вимог та обмежень (2 дні).
- Вибір технологічного стеку з обґрунтуванням (ADR).
- Проєктування схеми даних та міграцій.
- Архітектурна документація у вигляді рішень, що перевіряються.
- План масштабування та кешування.
- Рекомендації щодо спостережуваності (логи, метрики, трейси).
Результат — не Visio-діаграма, а набір рішень, що перевіряються, з обґрунтуванням компромісів. Архітектурний рев'ю існуючого проєкту займає 3–5 днів.
Зв'яжіться з нами, щоб отримати попередню оцінку архітектури вашого додатку. Замовте архітектурне рев'ю — це займе 3–5 днів, і ви отримаєте документований план рефакторингу.







