Консультація з архітектури веб-додатку
Архітектурні рішення змінюються дорого: перехід з моноліту на мікросервіси — це місяці роботи. Консультація дозволяє зробити правильний вибір до написання коду або зафіксувати проблему до того, як вона стане критичною.
Моноліт проти мікросервісів: коли що вибрати
Моноліт — правильний вибір для більшості стартапів та команд до 10 розробників:
- Немає overhead на міжсервісне взаємодія
- Простіше дебажити (один процес, один лог)
- Дешевше в підтримці
- Модульний моноліт з чіткими межами — відмінна вихідна точка
Мікросервіси виправдані коли:
- Різні частини системи потребують незалежного масштабування
- Команди працюють ізольовано на різних доменах
- Різні вимоги до технологій (ML-сервіс на Python, API на Go)
- Throughput потребує горизонтального масштабування окремих компонентів
Проміжний варіант — Modular Monolith з подальшим виділенням сервісів за необхідністю:
src/
modules/
auth/ # Bounded Context: авторизація
domain/
application/
infrastructure/
billing/ # Bounded Context: оплата
notifications/ # Bounded Context: сповіщення
shared/
kernel/ # Спільні примітиви (Money, UserId)
infrastructure/ # DB, HTTP-клієнти
Патерни API-шару
REST vs GraphQL vs tRPC:
// tRPC: type-safe RPC без код-генерації
// server/routers/users.ts
const usersRouter = router({
getById: publicProcedure
.input(z.object({ id: z.string().uuid() }))
.query(async ({ input }) => {
return db.user.findUnique({ where: { id: input.id } });
}),
create: protectedProcedure
.input(createUserSchema)
.mutation(async ({ input, ctx }) => {
// ctx.user — авторизований користувач
}),
});
// client/pages/users.tsx — типи поділяються автоматично
const { data } = trpc.users.getById.useQuery({ id: userId });
tRPC оптимальний для монолітних Next.js/Nuxt додатків. GraphQL — для публічного API з різними клієнтами. REST — для інтеграцій з зовнішніми сервісами та відкритого API.
Робота з даними: патерни
Repository Pattern з Prisma:
// domain/repositories/UserRepository.ts
interface UserRepository {
findById(id: UserId): Promise<User | null>;
save(user: User): Promise<void>;
findByEmail(email: Email): Promise<User | null>;
}
// infrastructure/prisma/PrismaUserRepository.ts
class PrismaUserRepository implements UserRepository {
constructor(private readonly db: PrismaClient) {}
async findById(id: UserId): Promise<User | null> {
const record = await this.db.user.findUnique({
where: { id: id.value }
});
return record ? UserMapper.toDomain(record) : null;
}
}
CQRS для складних доменів:
// Commands — змінюють стан
class CreateOrderCommand {
constructor(
public readonly userId: string,
public readonly items: OrderItem[]
) {}
}
// Queries — тільки читання, можуть бути денормалізовані
class GetOrderSummaryQuery {
constructor(public readonly orderId: string) {}
}
// Різні моделі читання та запису — кожна оптимізована під свої потреби
Кешування: стратегії
// Cache-Aside (Lazy Loading)
async function getUser(id: string): Promise<User> {
const cached = await redis.get(`user:${id}`);
if (cached) return JSON.parse(cached);
const user = await db.user.findUnique({ where: { id } });
await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
return user;
}
// Write-Through (синхронне оновлення кешу)
async function updateUser(id: string, data: UpdateUserDto): Promise<User> {
const user = await db.user.update({ where: { id }, data });
await redis.setex(`user:${id}`, 3600, JSON.stringify(user));
return user;
}
// Cache Invalidation за тегами (через Redis)
// Усі кеші для користувача інвалідуються разом
async function invalidateUserCache(userId: string) {
const keys = await redis.keys(`*user:${userId}*`);
if (keys.length) await redis.del(...keys);
}
Черги та фонові завдання
// BullMQ: типізовані завдання
interface EmailJobData {
to: string;
template: 'welcome' | 'password-reset' | 'invoice';
variables: Record<string, string>;
}
const emailQueue = new Queue<EmailJobData>('emails', { connection: redis });
// Producer (з основного коду)
await emailQueue.add('send', {
to: user.email,
template: 'welcome',
variables: { name: user.name }
}, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});
// Consumer (окремий воркер)
const worker = new Worker<EmailJobData>('emails', async (job) => {
await emailService.send(job.data);
}, { connection: redis, concurrency: 5 });
Що включає архітектурна консультація
| Крок | Вміст | Час |
|---|---|---|
| Discovery | Бізнес-вимоги, поточні болі, команда | 2–3 години |
| Ревю поточної архітектури | Аналіз коду та схем, якщо проект існує | 1–2 дні |
| Проектування | Схема компонентів, ADR, ризики | 2–3 дні |
| Документація | Architecture Decision Records, C4-діаграми | 1 день |
| Q&A з командою | Розбір неясностей, альтернатив | 2–4 години |
Результат — набір ADR (Architecture Decision Records) з обґрунтуванням кожного рішення, C4-діаграми (Context, Container, Component) та пріоритизований план рефакторингу якщо проект уже існує.
Консультація по архітектурі нового проекту — 3–5 робочих днів. Аудит існуючої архітектури — 5–10 робочих днів залежно від розміру системи.







