Проектування архітектури веб-додатку
Архітектура веб-додатку — це набір прийнятих рішень, які складно або дорого змінювати пізніше. Вибір бази даних, спосіб організації сервісів, стратегія масштабування — кожне з цих рішень встановлює межі того, що можна буде побудувати через два роки без переписування.
Хороше архітектурне рішення — не те, яке використовує найсучасніші технології. Це те, яке враховує реальні обмеження: розмір команди, очікувані навантаження, бюджет на експлуатацію та темп змін продукту.
З чого почати
Перш ніж вибирати технології, відповідайте на структурні запитання:
Якої природи навантаження? Read-heavy (новинний портал, довідник) — одна стратегія кешування. Write-heavy (біржа, система моніторингу) — інша. Mixed (електронна комерція) — третя.
Яка допустима затримка? Для торгової платформи 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).
Моноліт з чіткими межами модулів:
src/
├── modules/
│ ├── catalog/ # продукти, категорії, пошук
│ │ ├── domain/
│ │ ├── application/
│ │ └── infrastructure/
│ ├── orders/ # замовлення, кошик, checkout
│ ├── users/ # аутентифікація, профілі
│ └── notifications/ # email, push, sms
└── shared/
├── events/ # доменні події (для майбутньої декомпозиції)
└── infrastructure/ # HTTP клієнт, логер
Така структура дозволяє витягти модуль у сервіс, коли це стане необхідним — межі вже проведені.
Вибір бази даних
PostgreSQL підходить для 90% завдань. Реляційна модель, JSONB для гнучких даних, повнотекстовий пошук, партиціонування, реплікація — все з коробки. Починати з PostgreSQL і змінювати при конкретних проблемах — правильна стратегія.
Додаткові сховища за призначенням:
| Завдання | Інструмент |
|---|---|
| Сесії, кеш, 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 'USD',
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)
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
formatters: {
level: (label) => ({ level: label }),
},
});
// Прив'язуємо request-id до всіх логів у межах запиту
app.use((req, res, next) => {
req.log = logger.child({
requestId: req.headers['x-request-id'] ?? crypto.randomUUID(),
method: req.method,
path: req.path,
});
next();
});
Метрики через формат Prometheus: /metrics endpoint з RED-метриками (Rate, Errors, Duration) на кожен маршрут.
Терміни
Проектування архітектури — не одноразовий документ, а ітеративний процес. Первинне проектування для нового продукту: один–два тижні на дослідження вимог, ADR (Architecture Decision Records) по ключовим рішенням, схему даних, вибір технологічного стеку. Результат — не Visio-діаграма, а набір перевірених рішень з обґрунтуванням компромісів.
Архітектурний огляд існуючого проекту — три–п'ять днів: аналіз кодової бази, виявлення вузьких місць, план еволюції без переписування.







