Консультация по архитектуре веб-приложения
Архитектурные решения меняются дорого: переход с монолита на микросервисы — это месяцы работы. Консультация позволяет сделать правильный выбор до написания кода или зафиксировать проблему до того, как она станет критической.
Монолит vs Микросервисы: когда что выбрать
Монолит — правильный выбор для большинства стартапов и команд до 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 рабочих дней в зависимости от размера системы.







