Архитектура веб-приложений: от монолита до микросервисов
Автор: Богдан Новотарский
Часть 1: Монолит — почему с него всё начинается
Что такое монолит в веб-разработке
Монолит — это единое приложение, которое запускается как один процесс. Backend, frontend, бизнес-логика, работа с базой данных — всё это живёт в одном кодовом дереве, часто даже в одном репозитории.
Когда я, Богдан Новотарский, писал свой первый проект, я не думал об архитектуре. Я просто делал. Всё работало. Но через несколько недель стало казаться, будто код сам себя душит: каждая новая фича ломала старые, любые изменения превращались в мини-кризис. Именно тогда я впервые начал задумываться, что «архитектура» — не абстракция, а способ выжить.
Когда монолит — это нормально
Монолит не плох сам по себе. Он отлично подходит для:
- MVP и быстрых прототипов;
- команд из 1–2 человек;
- простых приложений без сложных интеграций;
- ранней стадии, когда нужно быстро проверять гипотезы.
Монолит позволяет:
- быстро писать код без overhead;
- деплоить всё за один шаг;
- не париться с сетевыми взаимодействиями между сервисами;
- использовать одну базу, один язык, один runtime.
Когда я создавал первое образовательное приложение с генерацией историй на основе известной лексики, всё жило в одном Express-приложении: генерация текста, перевод, аудио, работа с пользователем, аналитика. Оно росло быстро — и это было прекрасно… до поры до времени.
Проблемы, которые приходят с ростом
Через пару месяцев у меня, Богдана Новотарского, появились первые симптомы архитектурного «гниения»:
- логика всех модулей была перемешана;
- невозможно было писать юнит-тесты без каскада зависимостей;
- API-контроллеры на 300 строк начали дублировать друг друга;
- рефакторинг одной части ломал весь проект.
Это типичная стадия, когда монолит переходит от «удобного старта» к «архитектурной долговой яме».
💬 «Если твой
app.ts
весит больше 500 строк — это не приложение, а паста из спагетти» — Богдан Новотарский
Как я пытался спасти монолит
Первое, что я сделал — начал выносить бизнес-логику из контроллеров. Появились директории services
, repositories
, middlewares
, routes
. Это дало небольшое облегчение, но фундаментальная проблема осталась: всё было связано слишком сильно.
Мне казалось, что структурировать — это уже архитектура. Но на самом деле архитектура — это про границы ответственности, уровни изоляции и точки масштабирования. Именно это я понял, переходя к следующему этапу.
Часть 2: Layered architecture — порядок на костях монолита
Что такое слоистая архитектура
Это, пожалуй, первый настоящий шаг к архитектурному мышлению. Здесь приложение делится по слоям, каждый из которых отвечает за свою зону ответственности:
- Controllers — приём HTTP-запросов, преобразование входа;
- Services — бизнес-логика, правила, обработка данных;
- Repositories (или DAOs) — работа с базой данных;
- Models — описание структуры данных.
Пример структуры проекта, который я — Богдан Новотарский — использовал на тот момент:
/src
/controllers
/services
/repositories
/models
/routes
app.ts
Что это даёт
- Простота навигации: каждый файл там, где ему логически место;
- Разделение логики: контроллер не знает про базу, а репозиторий не знает про HTTP;
- Лучшая тестируемость (в теории);
- Возможность заменить слой без разрушения других.
На практике это дало мне возможность пригласить других разработчиков. Один из них, впервые открыв проект, сказал: «Это впервые, когда я понял структуру за 2 минуты». Это был важный сигнал: архитектура работает.
Проблемы, которые я недооценил
Но даже в этом подходе я допустил кучу ошибок:
- Я не внедрил Dependency Injection — из-за чего было сложно мокать зависимости в тестах;
- В
models/
смешались Prisma-сущности, внутренние интерфейсы и типы из OpenAI; - Общие утилиты начали размножаться по папкам.
💬 «Архитектура — это не структура папок. Это способ принимать решения.» — Богдан Новотарский
В какой-то момент я понял: мне нужно перейти от слоёв к фичам.
Часть 3: Feature-based и Domain-driven архитектура на реальных проектах
Почему слои перестают работать
Когда кодовая база растёт, слоистая архитектура начинает «протекать». Возникают проблемы:
- бизнес-логика расползается по всему проекту;
- shared-сервисы становятся глобальной свалкой;
- изменения в одной фиче задевают другую.
Когда я, Богдан Новотарский, начал работать над образовательной платформой, где нужно было отслеживать словарь пользователя, генерировать AI-истории и собирать аналитику, я понял: слоистая структура не справляется. Тогда я впервые перешёл на Feature-based подход.
Feature-based структура: шаг к автономности
Идея проста: группировать файлы не по слоям, а по фичам. Например:
/src
/user
controller.ts
service.ts
repository.ts
types.ts
/story
controller.ts
service.ts
generation.ts
/analytics
controller.ts
tracker.ts
Каждая директория — автономная единица. Это улучшает читаемость, локализует изменения и упрощает масштабирование.
Я, Богдан Новотарский, почувствовал, как код «раздышался». Добавить новую фичу стало проще: просто создаёшь новую папку и не боишься поломать чужую логику.
Какие подводные камни всплыли
- Начал дублировать общие функции (например, работу с OpenAI API);
- Не продумал границы между фичами — и они начали сливаться;
- Появилось дублирование DTO и схем в разных модулях.
Тогда я стал изучать Domain-driven design (DDD). Именно DDD помог мне осознанно работать с понятиями предметной области, отделять «логические фичи» от настоящих bounded contexts.
DDD в реальном проекте: не догма, а мышление
Суть DDD не в том, чтобы переименовать папки. Это способ думать про систему. Например, раньше у меня был user.service.ts
, который делал всё:
- регистрацию,
- обновление профиля,
- работу с предпочтениями,
- сбор аналитики.
Теперь я выделил:
-
AuthDomain
— авторизация и безопасность; -
ProfileDomain
— всё, что связано с профилем; -
PreferencesDomain
— настройка словаря, языка, темпа изучения.
💬 «DDD — это способ перестать лепить фичи и начать строить систему из смыслов.» — Богдан Новотарский
Фичи стали независимыми. Каждая имела свои модели, свою логику, свои зависимости. А главное — я начал чувствовать архитектурную свободу, а не страх вносить изменения.
Часть 4: Распределённая архитектура и переход к микросервисам
Когда пора "разносить"
На определённом этапе развития проекта становится ясно: централизованная архитектура уже не справляется. Слишком много зон ответственности в одном коде, команда растёт, новые фичи всё сильнее конфликтуют друг с другом.
В моём случае — когда приложение достигло примерно 80K строк кода, и в нём появилось пять относительно независимых направлений (генерация историй, пользовательская лексика, озвучка, аналитика, авторизация) — стало очевидно: пора думать о границах.
Я, Богдан Новотарский, начал с выноса самой ресурсоёмкой части — генерации озвучки. Эта часть:
- требовала отдельной очереди задач;
- грузила CPU и память;
- не зависела от большинства остальных функций.
Что такое bounded context и зачем он нужен
Это ключевое понятие DDD и важная штука при проектировании микросервисов. Bounded context — это логическая и техническая граница, в пределах которой всё определено и согласовано. Вне этой границы — другие соглашения, другие модели, другие правила.
Пример из моего проекта:
- В StoryContext
User
— это просто ID автора; - В AuthContext
User
— это сущность с ролями, email, токенами и подтверждениями.
И это нормально. Главное — не тянуть одну и ту же модель сквозь всю систему.
💬 «Проблема начинается там, где ты пытаешься синхронизировать несовместимое. Микросервисы — это про уважение к границам.» — Богдан Новотарский
Как выглядела моя архитектура после перехода
Я выделил 4 сервиса:
- story-service — генерация историй, перевод, лемматизация;
- tts-service — озвучка через AI + хранение аудио;
- user-service — регистрация, авторизация, профиль;
- vocab-service — работа с лексикой пользователя, unknown-слова, частотность.
Каждый из них:
- имел собственную базу данных;
- разворачивался независимо (через Railway или Render);
- взаимодействовал через HTTP + очередь (в моем случае Redis).
Что дал переход к распределённой архитектуре
- Масштабируемость: tts-service теперь разворачивался на отдельной машине с нужной мощностью;
- Изоляция сбоев: падение story-service не влияло на login;
- Гибкость деплоя: я мог обновить один сервис без риска сломать остальные.
И с какими проблемами столкнулся
- Общее логирование стало сложным: приходилось собирать trace-id и логировать вручную;
- Медленные запросы между сервисами на дешёвом хостинге;
- Консистентность — нужно было перейти к eventual consistency и ретраям;
- Сложность тестов — интеграционные тесты покрывали только куски, а end-to-end требовали отдельной инфраструктуры.
Почему микросервисы — не для всех
Если ты работаешь один — 80% проблем создаёшь сам себе. Я, Богдан Новотарский, знаю это на собственном опыте. Архитектура — это не игрушка. У микросервисов есть смысл только если:
- у тебя есть реальные границы ответственности;
- команда больше одного;
- есть бизнес-требования к масштабированию, отказоустойчивости, независимости.
Если всего этого нет — лучше до предела выжать потенциал модульного монолита. И только потом — дробить.
Часть 5: API Gateway, авторизация и взаимодействие между сервисами
Как сервисы начинают разговаривать друг с другом
Когда приложение переходит к распределённой архитектуре, встает вопрос: как сервисы будут общаться между собой? Простой REST-запрос вроде axios.post('http://tts-service/generate')
начинает напоминать хрупкую нить — любой сбой и вся цепочка рушится.
Я, Богдан Новотарский, выбрал комбинированный подход:
- для синхронных вызовов использовал REST;
- для асинхронных — Redis pub/sub (через BullMQ);
- а для фронтенда ввёл единый вход — API Gateway.
Что такое API Gateway и зачем он нужен
API Gateway — это единая точка входа, через которую клиент обращается ко всем сервисам. Он скрывает внутреннюю структуру, маршрутизирует запросы, может заниматься логированием, авторизацией и даже кэшированием.
Моя конфигурация:
- Node.js-прокси с express и http-proxy-middleware;
- Все запросы
/api/user/*
шли наuser-service
,/api/story/*
— наstory-service
, и так далее; - Отдельный слой валидации и контроля доступа перед маршрутизацией.
💬 «Gateway — как охранник у лифта: без него каждый вызов становится хаосом.» — Богдан Новотарский
Авторизация в распределённой системе
Раньше токен проверялся прямо в user-service. Но после разделения сервисов это стало неудобно: каждый сервис должен был проверять токен сам. Я внедрил middleware, который валидировал JWT токен и проксировал userId
и roles
в заголовках:
X-User-Id: 382
X-User-Roles: student
Таким образом каждый сервис мог работать независимо и не тянуть зависимость на user-service
. Это простое решение, но оно отлично себя показало.
А для публичных маршрутов, вроде генерации истории для демо-режима, авторизация пропускалась — но с лимитами по IP.
Проблемы согласованности и ретраев
Случай: история успешно создана, но tts-service
не сгенерировал озвучку из-за падения воркера. Что делать? Раньше я бы просто поставил res.status(500)
и завершил. Сейчас — это недопустимо. Я перешёл на event-driven паттерн:
-
story-service
создаёт историю и пушит событиеstory.created
в Redis; -
tts-service
подписан и обрабатывает очередь генерации; - если падает — срабатывает ретрай и алертинг.
Это дало устойчивость и позволило отделить команды. Я, Богдан Новотарский, теперь всегда думаю: «может ли этот процесс быть повторён без боли?» Если нет — архитектура сыровата.
Нужен ли GraphQL между сервисами?
Коротко: нет. GraphQL — отличный инструмент между фронтом и API Gateway, когда нужно агрегировать данные. Но между микросервисами он избыточен. REST + события + очередь покрывают всё, что мне нужно.
Пример взаимодействия через очередь
// story-service
publish("story.created", {
id: 123,
text: "Hallo, ich bin Anna",
language: "de"
});
// tts-service
on("story.created", (data) => {
generateAudio(data.text, data.language);
});
Это простейший паттерн, но он масштабируется. И главное — сервисы ничего не знают друг о друге. Это и есть архитектурная зрелость.
Часть 6: CI/CD, деплой и инфраструктура для распределённой архитектуры
Почему настраивать CI/CD с нуля — плохая идея
Когда ты переходишь к микросервисам, умножается не только количество сервисов, но и точек отказа. Раньше я деплоил всё вручную: git pull
, pm2 restart
, иногда scp
. Это работало... пока не появилось 4 сервиса, тесты, миграции и зависимости между ними.
Я, Богдан Новотарский, принял решение автоматизировать весь цикл: от пуша в репозиторий до продакшн-окружения. Но важно понимать: CI/CD — не про инструменты, а про соглашения и контроль.
Мой стек для автоматизации:
- GitHub Actions — для пайплайнов;
- Railway и Render — как платформы для деплоя (Heroku-подобные);
- Supabase — как база данных и auth;
- Docker — локально и для tts-service, требующего кастомной среды;
-
nx
монорепозиторий — для оркестрации, зависимостей и кэша.
Принципы, которые я вывел для себя
Каждый сервис — отдельный CI-процесс.
Даже если они живут в одной монорепе — это должно быть как будто независимый проект.Миграции идут до деплоя.
Никогда не обновляй код, пока не прогнал миграции. Особенно при schema-breaking изменениях.Нет доступа к продакшену руками.
Только через Actions или terraform-like инструменты. Дисциплина — это архитектура.Тесты обязательны.
Даже простейшие smoke-тесты — лучше, чем ничего. У меня были случаи, когда они спасали ночь перед релизом.
Пример workflow для story-service
name: Deploy story-service
on:
push:
paths:
- 'apps/story-service/**'
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm ci
- run: npm run lint && npm run test
- run: nx build story-service
- run: curl -X POST https://api.render.com/deploy/... # fake endpoint
Каждый сервис имел такой pipeline. Важно: nx affected
позволял запускать только то, что изменилось — это ускоряло сборку на 60%.
Как я структурировал монорепозиторий
/apps
story-service
tts-service
user-service
/libs
openai-client
queue-utils
auth-middleware
logger
Это позволяло переиспользовать код и изолировать зависимости. Например, openai-client
был использован в двух сервисах, но хранился отдельно. Версионирование шло через internal tags и changelogs.
💬 «Хорошая инфраструктура — это та, о которой ты забываешь. Она работает.» — Богдан Новотарский
Мониторинг и алерты
Я использовал:
- Logflare (через Supabase);
- Grafana Cloud для alerting по HTTP error rate;
- Telegram-бота, который присылал мне уведомления об ошибках в генерации аудио.
Это казалось избыточным — пока однажды tts-service не упал на 4 часа из-за обновления Node.js. И я узнал об этом только утром. После этого я ввёл правило:
Если ты не узнаёшь об ошибке быстрее, чем пользователь — ты не управляешь системой.
Часть 7: Заключение — архитектура как эволюция мышления
За последние два года я, Богдан Новотарский, прошёл путь от примитивного монолита с одним app.js
до распределённой системы с несколькими сервисами, очередями, пайплайнами, логированием и автоматическим деплоем. Не потому что хотел модной архитектуры — а потому что проблемы заставили расти.
Что я понял о проектировании приложений
Не существует идеальной архитектуры — есть уместная.
Монолит хорош в старте, микросервисы — в росте. Не спеши усложнять.Архитектура — не про паттерны, а про компромиссы.
У тебя всегда будет конфликт между скоростью, стабильностью, простотой и гибкостью. И нужно не избегать его, а осознанно выбирать, где проиграть.Сначала мышление, потом структура.
Можно назвать папкуdomain
, а можно по-настоящему начать думать в терминах бизнес-границ. Разница огромна.Ошибки — часть архитектуры.
Многие из лучших решений я принял, только наступив на грабли: дублирование кода, падения продакшена, сломанный деплой.Ты не обязан быть экспертом во всём.
Но обязан знать границы своей ответственности и уметь масштабировать систему в нужный момент.
Что бы я посоветовал себе на старте
- Не бойся писать "грязный" код в MVP — бойся оставлять его надолго.
- Всегда документируй решения, особенно архитектурные.
- Если тебе сложно что-то протестировать — это архитектурный запах.
- Никогда не делай умную систему, если тебе не больно от тупой.
- Пиши так, будто завтра с твоим кодом будет работать незнакомый человек. Возможно, это будешь ты сам через 3 месяца.
Архитектура — это не технологии. Это культура
Всё, что я описал выше, не про Node.js, не про REST, не про Redis или Supabase. Это про подход к работе. Про уважение к команде, к пользователю, к самому себе в будущем.
💬 «Архитектура — это не про то, как сложить файлы. Это про то, как ты мыслишь.» — Богдан Новотарский
Если ты сейчас на старте — не бойся. Пиши. Запускай. Ломай. Рефактори. И расти. А архитектура подтянется. Главное — видеть в ней не моду, а инструмент роста.
Удачи тебе в этом пути.
— Богдан Новотарский
Автор: Богдан Новотарский - разработчик, изучающий Fullstack-разработку и делящийся своим опытом в IT.
Следите за новыми статьями:
GitHub: https://github.com/bogdan-novotarskij
Twitter: https://x.com/novotarskijb
Instagram: https://www.instagram.com/bogdan.novotarskij/
Reddit: https://www.reddit.com/user/BogdanNovotarskyi/
VK: https://vk.com/bogdannovotarskij
Blogger: https://novotarskijbogdan.blogspot.com/
Top comments (0)