
Микросервисы тут, микросервисы там… Из каждого утюга доносится дивный сказ про прекрасный мир микросервисов. А ведь это всего лишь один вид из десятка архитектурных стилей, который имеет свои достоинства и недостатки.
В этом эксперименте мы внедрим микросервисы в личный сайт, нарушив ключевые принципы DDD. Я создам антипаттерн «бедных сервисов» (Anemic Domain Model) и покажу, чем опасен прямой доступ к данным между микросервисами.
Эта статья о том, что за прекрасным внешним видом, большой функциональностью, высокопроизводительным исправным кодом может скрываться абсолютное зло.
Содержание
1. Сбор и анализ требований
1.1 Обзор вариантов реализации личного сайта
У многих разработчиков есть личные сайты. Как правило, их создают с помощью конструкторов сайтов (например, Tilda). Это быстро и просто. Но у такого подхода есть и недостатки. Например, ограниченная функциональность. Например, для переключения темы в Tilda приходится вручную дублировать блоки и скрывать их CSS. Также невозможны сложные кастомные анимации, сложная логика и интернационализация (i18n).
Тут на помощь нам приходят чистый HTML, CSS, JavaScript – можно на них ��аписать сайт и опубликовать. Проблема в том, что мы не можем автоматизировать манипуляцию с данными. Нам придется всё делать вручную: добавлять статьи, картинки. Также здесь невозможно взаимодействие с пользователем — он может только смотреть на наш сайт и ничего не делать. А может мы хотим получить обратную связь? Почему ему не дать возможность комментировать?
И вот уже стоит посмотреть в сторону разделения на клиентскую часть (фронтенд) и серверную (бекенд). Здесь также существует несколько подходов к реализации. Самый частый подход — это создать монолитное приложение. То есть фронтенд и бекенд находятся в пределах одного процесса или контейнера. Такой функционал реализуют веб-фреймворки типа Django или Flask. Django – классический пример реализации MVC (там он называется MTV). Существуют модели данных, которые через view-функцию отправляются в шаблон и генерируется веб-страница. Локальные вызовы в монолите быстрее, чем межсервисные HTTP-запросы. Проблема в том, что если возникает критическая ошибка внутри любого компонента — упадет вся система.
Распределённая архитектура позволяет изолировать отказы: например, при падении сервиса комментариев фронтенд останется доступным (хотя функциональность будет ограничена). Сайт доступен, пусть и не с полным функционалом. Вот здесь именно реализуются все достоинства распределенной архитектуры. Но какие накладные расходы: сетевые задержки, сложные транзакции, вместо 1 контейнера развертываем 10, время на разработку в среднем увеличивается на 50%.
Вывод отсюда такой: у любой реализации есть достоинства и недостатк��. Не существует идеальных решений.
1.2 Определяем функциональность
Функция |
Описание |
Отображение контента |
Пользователь видит контент при переходе по ссылкам: «Главная», «Обо мне», «Технологии», «Проекты», «Сертификаты», «Публикации», «Контакты». |
Аутентификация |
Пользователь может зарегистрироваться на сайте, получив JWT-токен для возможности комментирования. В случае потери пароля — он получает письмо по email с восстановлением. |
Оставление комментариев |
Зарегистрированный пользователь может оставлять комментарии в разделе «Проекты», также он может обновлять или удалять свои комментарии. |
Выбор цветовой темы |
Автоматическое установление и возможность переключения светлой и темной темы. |
Выбор языка контента |
Можно выбрать отображение контента между русским и английским языком. |
Адаптивная верстка |
Сайт автоматически подстраивается под разрешения экрана пользователя. Внешний вид сайта зависит от устройства: компьютер, планшет, мобильный телефон. |
Получение уведомлений |
Пользователь может быть забанен, он может быть удалён из системы администратором. А также пользователь может изменить свой пароль. Всё должно сопровождаться отправкой письма на email пользователя. |
2. Делаем архитектурную ошибку
2.1 Выбор архитектуры
Понятие архитектуры программного обеспечения относительно. Когда говорят что-то типа «я сделал монолит», то здесь речь идет об архитектурном стиле. А сама архитектура — это намного шире. Это не только стиль. Архитектура помимо стиля включает свойства (доступность, надежность, безопасность и т. п.), свои правила и принципы проектирования.
Свойства, правила и принципы мы разберем чуть позже, а к этом разделе поговорим об архитектурном стиле.
Архитектурный стиль — это про то, как у нас организован исходный код. Существует всего 2 типа архитектурных стилей: монолитный стиль и распределенный. А у этих типов существуют свои виды.
Существуют различные классификации архитектурных стилей. Ниже приведена классификация, которая была дана в книге Марка Ричардса и Нила Форда «Фундаментальный подход к программной архитектуре: паттерны, свойства, проверенные методы».

Надо хорошо понимать следующую вещь: любое техническое задание можно реализовать в любом архитектурном стиле.
Приведу аналогию из реальной жизни: вам дали задание построить общежитие для студентов. Варианты строительства:
• деревянный барак с туалетом на улице
• кирпичное многоэтажное здание с общим коридором и кухнями в разных концах здания (коммунальный тип)
• кирпичное здание с отдельными комнатами, где есть санузел и своя кухня (квартирный тип)
• отдельные домики для студентов со своим санузлом и кухней
Все эти решения решают одну и ту же задачу. И у каждого из них есть свои преимущества и недостатки. Бараки очень дешевые и быстро возводятся, но студентов это будет отпугивать. Отдельные домики строить дорого, но студентов это будет привлекать. Смысл в том, что всё соткано из компромиссов. Не бывает идеальной архитектуры. Везде есть свои плюсы и минусы.
Именно поэтому, что распределенный архитектурный стиль, что монолитный архитектурный стиль — это не идеальные решения. Идеальных не существует. И то, и другое имеет свои плюсы и минусы. Самое главное в первую очередь ответить на вопрос «почему мы выбрали именно этот архитектурный стиль?», а потом уже идёт вопрос «как это реализовать?».
Наш проект экспериментальный и мы выбрали архитектурный стиль микросервисов. Далее производим разбиение по предметным областям — по DDD.
2.2 Разбиение по DDD
Чтобы у нас не получился большой ком грязи, мы должны выполнить разбиения проекта на логические части. Существует два основных подхода:
1. Техническое разбиение
2. Предметное разбиение
Техническое разбиение — деление системы на части по техническим критерия. Это может проявляться в виде уровней, соответствующих разделению по схеме Модель - Представление - Контроллер (MVC).
Предметное разбиение — деление системы на компоненты по бизнес-логике и предметной области. В основе лежит DDD - Domain-driven Design. Это метод моделирования для декомпозиции сложных программных систем.
В архитектуре микросервисов чаще всего используется предметное разбиение. Давайте его выполним.
Вначале надо понять, что является доменом (предметной областью). В моем случае это персональный сайт-портфолио с интерактивными элементами.
Далее мы должны определить ядро (Core Domain) — критически важную часть системы, которая обеспечивает ключевое конкурентное преимущество и основную бизнес-ценность. В нашем случае — это Content Service.
Далее определяем поддерживающие поддомены (Supporting Subdomain) — специфичные для бизнеса компоненты, которые поддерживают Core Domain, но не являются источником конкурентного преимущества. Сюда входят Admin Service, Comments Service.
И наконец, надо определить обобщенные поддомены (Generic Subdomain) — система уведомлений Notification Service – он решает стандартную задачу отправки email и может быть заменен на любой готовый email-сервис без потери бизнес-логики. Тоже самое и с Auth Service. Также сюда будут входить наш кастомный API Gateway (API шлюз - его можно заменить на Envoy Gateway или Traefik Gateway), а также базы данных (MongoDB, PostgreSQL, Redis, Elasticsearch), кластер Apache Kafka и S3-объектное хранилище MinIO.

Самый главный вопрос: зачем всё это надо определять? Такое разбиение нам позволяет понять, что важно, а что не важно. У нас может не работать аутентификация, у нас могут не работать комментарии, но у нас работает ядро — продукт живет. Второе пре��мущество такого разбиения влияет на уменьшает связанности (coupling) - это мера того, насколько сильно модули зависят друг от друга. Отображение контента не зависит от аутентификации, также отправка email не зависит от комментариев. Но есть еще и третье преимущество, которое вытекает из второго: представьте, что я решил добавить блог. Это будет отдельный поддомен, который не будет влиять на сущности. Он будет работать независимо от всего другого — и если он упадет, сайт останется работоспособным.
2.3 Проектирование системного дизайна с микросервисами
Клиент попадает на фронтенд, написанный на Vue.js. Далее запрос с фронтенда поступает через балансировщик нагрузки на API шлюз (API Gateway). Как правило, в коммерческой разработке выбирают уже готовые решения для реализации API шлюза. Например, Envoy Gateway или Traefik Gateway. Но в рамках моего IT-творчества я решил самостоятельно реализовать API шлюз, написав его с нуля. API шлюз кеширует запросы, проверяет их количество с целью ограничения (и там, и там будем использовать Redis), проверяет права доступа, а самое главное — выполняет маршрутизацию на конкретный микросервис, перекладывая JSON на Protobuf. По классике жанра, у каждого микросервиса есть своя база данных.
Как и в реальной коммерческой разработке, в нашем проекте в качестве «центрально нервной системы» будет выступать распределенная стриминговая платформа Apache Kafka. Проект новый, поэтому мы будем сразу реализовывать самый современный подход к оркестрации брокерами — Kraft и забудем, что такое Zookeeper.
Ну и, конечно, реализуем Elastic Stack для логов, метрик и мониторинга.
Чего-то не хватает? Ну, конечно, Kubernetes! Как же без него!
Вот так это будет визуально:

Что здесь не так? У нас здесь появляются так называемые бедные сервисы (Anemic Domain Model в контексте DDD). Но мы проводим именно эксперимент. В эталонной микросервисной архитектуре в каждом микросервисе Content Service, Auth Service, Comments Service, Notification Service должен быть создан полный CRUD, а Admin GUI будет ходить на эндпоинты /admin через API шлюз. Поэтому в идеале надо бы выбросить Admin Service из системного дизайна. Это пример ошибки при принятии архитектурных решений. А что будет если мы оставим?
Если в реальной коммерческой разработке такое встречается, то далее следует эволюция архитектурных решений. Иногда полезно пройти через антипаттерн, чтобы глубоко понять почему существуют лучшие практики.
2.4 Проектирование пользовательских интерфейсов (UX/UI)
Важнейшей составляющей успеха и популярности любого программного обеспечения является положительный пользовательский опыт. Термин «пользовательский опыт» сокращенно называют UX (англ. User Experience). Пользователь ПО должен быть полностью удовлетворен и внешним видом, и производительностью продукта. Удовлетворенность достигается в том числе благодаря удобному и интуитивно понятному пользовательскому интерфейсу (сокр. UI - User Interface).
Не надо легкомысленно относиться к UX/UI. Это целая наука и искусство. И как у любой науки у UX/UI есть свои принципы и законы.
Принципы UX:
Название принципа |
Суть |
Полезный и удобный |
Хороший продукт должен быть одновременно полезным (решать реальные задачи пользователя) и удобным (обладать интуитивно понятным интерфейсом) |
Легко осваиваемый и запоминаемый |
Пользователи быстро понимают, как им пользоваться, даже при первом знакомстве. Даже после перерыва в использовании, пользователь легко вспоминает ключевые функции и навигацию |
Доверительный и дающий контроль пользователю |
Пользователь уверен, что продукт надёжен и безопасен. Пользователь чувствует, что управляет процессом, а не система диктует ему условия |
Принципы UI:
Название принципа |
Суть |
Принцип 60-30-10 |
Классический принцип цветового дизайна: 60% — доминирующий (основной) цвет, 30% — вторичный цвет (дополняющий), 10% — акцентный цвет |
Стимулирующее повторение и поддержание консистентности |
Использование одинаковых визуальных элементов (цветов, шрифтов, кнопок, отступов) для создания ритма и узнаваемости. Сохранение единого стиля и логики взаимодействия во всех частях продукта |
Правильное расположение элементов |
Правильное выравнивание создаёт порядок и облегчает сканирование контента. Направление задаёт поток восприятия и подсказывает, куда двигаться взгляду. |
Законы UX/UI:
Название закона |
Суть |
Закон близости |
Элементы, расположенные близко друг к другу, воспринимаются как связанные, а удалённые — как отдельные группы |
Закон Фиттса |
Чем ближе и крупнее элемент, тем быстрее пользователь сможет с ним взаимодействовать |
Закон Хика |
Чем больше элементов меню, кнопок или опций вы показываете пользователю, тем дольше он будет думать и тем выше вероятность ошибки |
Законы и правила Бена Шнейдермана |
1) Стремитесь к единообразию; 2) Удовлетворяйте потребности как опытных, так и начинающих пользователей; 3) Предлагайте информативную обратную связь; 4) Дизайн диалогов должен сообщать о завершении действий; 5) Обеспечивайте простую обработку ошибок; 6) Разрешайте легко отменять действия; 7) Давайте пользователям чувство контроля; 8) Сокращайте кратковременную память |
Когда мы начинаем непосредственно создавать дизайн клиентской части в Figma мы должны пользоваться каждым принципом, правилом и законом из этих таблиц.
2.5 Проектирование БД
Базы данных бывают реляционные SQL и нереляционные NoSQL. У нерялиционных есть также еще свои виды для решения определенных задач: документоориетированные, ключ-значение, колоночные, графовые, векторные (для ИИ и нейросетей) и другие.
Какая база данных подойдет для хранения данных контента? Что такое контент? Это коллекция документов. Из определения уже понятно, что хорошей идеей будет использование документоориетированной базы данных MongoDB.
А для микросервиса аутентификации? Данные пользователя (хэш пароля, email, токены обновления) отлично ложатся на документную модель. Каждый пользователь — один документ в коллекции. Получается тоже MongoDB.
Ну а как для уведомлений? У разных типов уведомлений (email, push, in-app) могут быть разные данные. В реляционной БД пришлось бы делать несколько таблиц или столбец с JSON. В MongoDB это решается естественно.
Ну и комментарии. Mon..! Нет! Комментарии часто образуют древовидные структуры (ответы на ответы). Операции «добавить комментарий -> увеличить счетчик комментариев в посте» должны быть атомарными. MongoDB поддерживает многодокументные транзакции, но они дорогие с точки зрения производительности по сравнению с транзакциями в реляционных БД. В реляционной БД это стандартная и быстрая операция. Для комментариев выбираем PostgreSQL.
Как будет выглядеть модель такой базы данных?
А вот так:

В реляционных базах данных мы привыкли к трем видам связей:
• one-to-one (один-ко-одному)
• one-to-many (один-ко-многим)
• many-to-many (многие-ко-многим)
• [здесь кое-чего не хватает]
А это что за вид связи? Это 4-ый вид связи, про который многие забывают. Он называется Self-referencing. Это когда таблица ссылается сама на себя. Такой вид связи идеален для создания иерархических структур. Система комментариев и есть иерархическая структура.
Вам, наверно, интересно, а как это выглядит в базе данных. Что же, я приведу реальную выгрузку данных из базы с вложенными комментариями:
{
"id": 112,
"project_id": "68dc3c354a3d786189d88077",
"author_id": "68d5a8b16b11bde9670809f2",
"author_email": "admin@admin.com",
"comment_text": "Вложенный комментарий уровня 2 (первый)",
"created_at": "2025-10-01T15:50:13.704689",
"parent_comment_id": 111,
"likes": 0,
"dislikes": 0
},
{
"id": 111,
"project_id": "68dc3c354a3d786189d88077",
"author_id": "68d5a8b16b11bde9670809f2",
"author_email": "admin@admin.com",
"comment_text": "Вложенный комментарий уровня 1 (второй)",
"created_at": "2025-10-01T15:49:57.661543",
"parent_comment_id": 109,
"likes": 0,
"dislikes": 0
},
{
"id": 110,
"project_id": "68dc3c354a3d786189d88077",
"author_id": "68d5a8b16b11bde9670809f2",
"author_email": "admin@admin.com",
"comment_text": "Вложенный комментарий уровня 1 (первый)",
"created_at": "2025-10-01T15:49:48.908385",
"parent_comment_id": 109,
"likes": 0,
"dislikes": 0
},
{
"id": 109,
"project_id": "68dc3c354a3d786189d88077",
"author_id": "68d5a8b16b11bde9670809f2",
"author_email": "admin@admin.com",
"comment_text": "Отличный проект!",
"created_at": "2025-10-01T15:48:51.064197",
"parent_comment_id": null,
"likes": 0,
"dislikes": 0
}
]
Страшно? Да как бы ни так. Современный JavaScript без каких либо проблем обработает такую выгрузку и расположит все как положено. В подтверждении своих слов мы с вами чуть-чуть забежим вперед. В рамках проекта мы создадим кастомную админку на Quasar (это фреймворк на основе Vue 3), в которой будет возможность править комментарии. Так вот, как эта самая выгрузка будет выглядеть в нашей админке:

Красиво? А ведь это те самые запутанные данные из PostgreSQL.
2.6 Проектирование API
Я сразу отойду от классического определения API. По сути дела, API – это контракт, определяющий, как две системы должны взаимодействовать друг с другом.
API могут быть:
• stateful – с сохранением состояния
• stateless – без сохранения состояния
Stateful API сохраняет состояние клиента на сервере между запросами, требуя поддержания сессии.
Stateless API обрабатывает каждый запрос полностью независимо, без хранения состояния клиента на сервере.
Существует несколько подходов API:
• RPC-протоколы (SOAP, gRPC) для удаленного вызова процедур
• REST-архитектура для работы с ресурсами через HTTP
• GraphQL - язык запросов для гибкого получения данных
SOAP используется в корпоративном сегменте – там, где важна безопасность и всё построено на транзакциях.
REST доминирует при создании публичных (открытых) API.
gRPC популярен для внутренней коммуникации сервисов.
GraphQL решает проблемы получения данных по множественным параметрам запроса (например, надо указать цвет, год, габариты, вес и другие параметры) в сложных клиентских приложениях.
В нашем случае мы буде�� использовать REST для коммуникации между фронтендом и API шлюзом, а также между Admin Service и Admin GUI. REST хорош тем, что он очень прост и понятен. В качестве спецификации он использует OpenAPI/Swagger.
Внутри бэкенда для коммуникации API шлюза с другими микросервисами будет использоваться gRPC.
3. Пока все хорошо: разработка клиентской части приложения
3.1 Создание ассетов с помощью ИИ
В настоящее время у любого появилась уникальная возможность создавать графические элементы интерфейса с помощью искусственного интеллекта (ИИ) абсолютно бесплатно. Например, я хочу, чтобы у меня было анимированное небо на заднем фоне, а на переднем средневековый монастырь. И то, и то другое можно без проблем генерировать с помощью ИИ. Возьмем, например, нейросеть GigaChat – по текстовому запросу (промту) можно получить готовый и качественный ассет:

Тоже самое и с облаками — ввести нужный промпт и получить ассет. Также нейросети отлично решают вопрос с генерацией favicon – это иконки на вкладке вашего сайта, а также на всех возможных устройствах.
3.2 Создание UX/UI сайта с помощью Figma
Как по мне, онлайн-программа Figma представляет один из лучших функционалов для создания UX/UI. Если вы не знакомы с этим приложением — изучить его можно за 1 день.
Вооружившись принципами и законами UX/UI создадим дизайн сайта:

Как видно мы поместили сгенерированные ИИ ассеты на общий макет.
Когда дизайн готов можно приступать к вёрстке на HTML, CSS.
3.3 Разработка основного фронтенда Frontend с помощью Vue.js
Современный фронтенд можно классифицировать по принципу рендеринга и архитектуре на две фундаментальные модели:
Серверный рендеринг (Server-Side Rendering, SSR): В этой модели пользовательский интерфейс генерируется на стороне сервера. Бэкенд выполняет код, извлекает данные из базы, вставляет их в HTML-шаблон и отправляет клиенту полностью готовую к отображению HTML-страницу. Классическими примерами фреймворков, использующих этот подход, являются Django и Flask.
Клиентский рендеринг (Client-Side Rendering, CSR): В этой парадигме сервер отправляет клиенту минимальный HTML-каркас и JavaScript-бандл (как правило, это SPA — Single Page Application). Браузер загружает и исполняет JavaScript, который затем самостоятельно управляет DOM, динамически запрашивает данные у бэкенда (обычно через JSON API) и рендерит интерфейс непосредственно на стороне клиента. Такое приложение является независимым от бэкенда. Яркие примеры — приложения, созданные на React, Angular или Vue.js.
Также существует комплексное решение, где сочетаются SSR и CSR – это фреймворк Nuxt.js.
В проекте использован CSR, мы создадим SPA на Vue.js. Почему Vue.js? Концепция, которая лежит в основе данного фреймворка, мне лично очень нравится. Приложение на Vue.js собирают из компонентов, в каждом из которых инкапсулированы HTML-шаблон (template), логика (script) и стиль (style). Каждый компонент помещают во vue-файл.
Теперь надо обратиться к нашему дизайну на Figma и произвести разбивку нашего SPA на компоненты со связями между компонентами. В моем случае декомпозиция выглядит так:

Мне кажется, что даже и не надо описывать каждый компонент — по его названию сразу понятно, что это такое и какую функцию он выполняет.
Для каждого представления (View) я решил выбрать абсолютно разные способы реализации:
AboutMeView – карусель
TechnologiesView – интерактивная карта
ProjectsView – карточки со всплывающими модальными окнами с динамическим контентом
CertificatesView – галерея
PublicationView – таблица
ContactsView – анимированные 3D-ссылки
Здесь я использовал как внешние библиотеки для Vue.js, так и встроенные функции, а местами чистый JavaScript.
В итоге, если все собрать получилось вот это:

Напоминаю, что это SPA. Никакой перезагрузки страницы не происходит, но тем не менее в URL-строке браузера появляются адреса вида /about. Дело в том, что Vue Router автоматически переключает компоненты, бесшовно, без перезагрузки страницы, что резко улучшает UX.
Также у нас динамические переключается язык и темы (с темной на светлую и наоборот).
Еще один важный нюанс — это то, как наш SPA будет ходить на бэкенд. В языке JavaScript есть встроенные решения:
XMLHttpRequest — он считается устаревшим, но до сих пор используется
fetch — современный подход, основанный на Promise’ах
Но мы не будем использовать ни то, не другое. Мы будем использовать стороннюю библиотеку axios, так как axios предоставляет более удобный API: автоматическая трансформация JSON-ответов, перехватчики для глобальной обработки запросов, отмена запросов и встроенная защита от XSRF-атак.
SPA необходимо оптимизировать. Это достигается помощью ленивой загрузке маршрутов, асинхронных компонентов, сборщика пакетов Vite.
Полный код фронтенда Frontend доступен здесь.
3.4 Разработка Admin GUI с помощью Quasar
Я скажу так: если вы знаете Vue, то вы автоматически знаете Quasar. Quasar – это тот же самый Vue с уже готовыми компонентами из коробки. Ваша задача просто собрать эти компоненты воедино. Quasar – это не про уникальность и авторский дизайн, это про функциональность. Нам не нужна красивая графика в Admin GUI. Мы её соберем из компонентов.
В итоге получается это:

Долго ли собрать такую панель? С Copilot её можно собрать за пару вечеров.
Полный код Admin GUI доступен здесь.
4. Погружение в ад: разработка серверной части приложения
4.1 Обзор основных концепций
Перед тем, как мы приступим к реализации микросервисов, мы с вами должны понять основные концепции, которые будут реализованы в этих микросервисах. Я постараюсь вас не душить, буду предельно краток и лаконичен.
Важный момент: мы будем говорить как это всё выглядит на языке Python. Следующие темы относятся только к языку Python. В других языках это реализуется иначе.
4.1.1 Асинхронное программирование
Если есть асинхронное программирование, значит есть синхронное. Что это означает? Синхронное означает последовательное, то есть каждая последующая строчка кода ждёт пока выполнится предыдущая. А что если одна строчка кода выполняется очень долго? Это называется блокировкой. А что именно блокируется? Когда вы запускаете Python-код создаётся Python-процесс. Этому процессу выделяется свой объем оперативной памяти и процессорного времени. Внутри этого процесса находится много всего: PID, файловые дескрипторы и в том числе основной поток. Именно этот поток и блокируется.
Блокировку необходимо увидеть каждому программисту и прочувствовать её. Давайте напишем синхронный код и сделаем блокировку. Все библиотеки Python можно разделить на блокирующие и неблокируюшие. Стандартная библиотека time является блокируюшей и там есть блокирующая функция sleep
from time import sleep, time
def start_timer(seconds: float) -> None:
sleep(seconds)
return
def main() -> None:
start = time()
sleep(3) # Блокирует основной поток на 3 с.
sleep(4) # Блокирует основной поток на 4 с.
sleep(5) # Блокирует основной поток на 5 с.
end = time()
print(f"Код выполнился за время {end - start:.2f} с.")
if __name__ == "__main__":
main()
Код выполнился за 12 секунд.
А теперь давайте сделаем тоже самое, только с применением асинхронного программирования.
import asyncio
from time import time
async def start_timer(seconds: float) -> None:
await asyncio.sleep(seconds)
return
async def main() -> None:
start = time()
tasks = [
asyncio.create_task(start_timer(3)),
asyncio.create_task(start_timer(4)),
asyncio.create_task(start_timer(5)),
]
await asyncio.gather(*tasks)
end = time()
print(f"Код выполнился за время {end - start:.2f} с.")
if __name__ == "__main__":
asyncio.run(main())
Код выполнился за 5 секунд. Но что здесь происходит? Здесь мы запускаем асинхронный цикл. Туда мы помещает корутину main. Внутри main мы создаем задачи — это такие обёртки вокруг корутины start_timer. Задача нужна для того, чтобы корутины выполнялись конкурентно. То есть каждая корутина стремится выполниться как можно скорее. У нас в основном потоке есть только асинхронный цикл и всё. Есть ли блокировка. Да, есть! Блокируется основной поток этим самым асинхронным циклом, но он эффективно используется за счет переключения между корутинами при операциях ввода-вывода.
Тоже самое будет происходить в наших микросервисах. Самое главное условие — использование неблокирующих библиотек и клиентов:
• неблокирующий клиент FastAPI для HTTP-запросов
• небокирующие сессии SQLAlchemy 2
• неблокирущий клиента Pymongo
• неблокирующий клиент Redis
• неблокирующший клиент aiokafka
Всё это помещается в асинхронный цикл событий asyncio и выполняется конкурентно.
Асинхронное программирование — это огромная тема. Вам надо хорошо знать, что такое CPU bound – операции и IO Bound операции. Существует прекрасная книга на эту тему, который должен изучить как учебник любой Python-программист — М. Фаулер: "Python Concurrency with asyncio"
4.1.2 Использование Redis для кэширования и ограничения запросов
На уровне железа есть разные компоненты компьютера для хранения данных. Это могут быть SSD, оперативная память (RAM), flash-карта. Нам надо как можно быстрее получить данные. Что мы выберем? Оперативная память работает быстрее всего (если не брать L1/L2/L3 кэши процессора). У неё очень высокая скорость записи и чтения данных. Но она довольно дорогая и объем её небольшой. А зачем нам нужна эта скорость? Это улучшает опыт пользователя — чем он быстрее получит данные, тем лучше. Мы поняли, что нам нужна оперативная память, а как именно хранить в ней данные?
Существует решение – in-memory NoSQL база данных типа «ключ-значение» Redis. Зачем нам лезть каждый раз в основную базу данных MongoDB или PostgreSQL, когда можно извлечь один раз данные и поместить их в Redis в оперативной памяти!? Эта операция называется кэширование — от английского слова «cache», которое переводится как «тайник». Именно в тайник мы помещаем данные и оттуда извлекаем. Всё бы хорошо, но есть одно «но». Что если данные часто меняются? Каждые 20 секунд. Надо ли нам класть данные в кэш? Нет, это замедлит работу сервиса. Поэтому кэширование применяем только для данных, которые редко меняются. Для данных, которые часто меняются либо кэш не используется, либо можно использовать короткое время жизни кэша (TTL — анг. Time to live).
Правда, есть одно важное замечание - «инвалидация кэша». Смысл этой концепции в том, что когда у нас происходит какая-то CUD-операция (Create, Update, Delete) – мы можем сбросить кэш. Обнулить его. Это будет использовано в нашем проекте
Еще Redis используется для хранения количества запросов для каждого IP-адреса с целью их ограничения. Зачем это нужно? Это элемент информационной безопасности — чтобы защититься от DoS-атак (отказ от обслуживания).
За тем, что происходит в Redis, можно наблюдать. Существует инструмент Redis Insight. Он запускается отдельным контейнером и мы можем полностью манипулировать данными в Redis. Это нам пригодится в будущем, чтобы убедиться, что данные загружаются действительно из Redis, а не из базы данных.
4.1.3 Асинхронная коммуникация Apache Kafka
Мы уже с вами выяснили, что между микросервисами должна быть минимальная связанность. В идеале, один микросервис ничего не должен знать о другом микросервисе. Именно для этого применяют события.
Apache Kafka обладает невероятной производительностью. Здесь есть всё, что нужно для комфортной работы — гарантированная доставка сообщений, механизм предотвращение дублирования сообщений, горизонтальное масштабирование партиций в топиках и еще огромное число полезных функций.
Apache Kafka – это огромная тема. В 2025 году вышла потрясающая книга, после прочтения которой у вас не останется ни одного вопроса по этой технологии - Anatoly Zelenin, Alexander Kropp: “Apache Kafka in Action: From basics to production”
4.1.4 Elastic Stack
У нас каждый микросерсис имеет собственные логи. В продакшене будет очень сложно лезть в каждый контейнер и смотреть, что там происходит. Надо как-то агрегировать все логи, сохранять их, анализировать, визуализировать.
Именно эту функцию выполняет Elastic Stack. Раньше эта технология называлась ELK Stack. ELK – это сокращение трех технологий:
• E -Elasticsearch – нереляционная база данных для полнотекстового поиска
• L – Logstash – инструмент для извлечения данных, обработки их и фильтрации с последующей передачей в Elasticsearch.
• K – Kibana – это графический интерфейс для Elasticsearch, у которого огромное количество возможностей: визуализация, построение дашбордов, диаграмм, графиков. В последнюю версию Kibana завези ИИ, который может находить аномалии и коллизии данных.
Проблема была именно с Logstash. Инструмент классный, но иногда избыточный. Он прекрасно справляется со своей задачей и по сей день. И сегодня он очень распространён. Но иногда хочется чего-то полегче. Поэтому придумали легковесные решения — Beats. Их много видов.
В нашем проекте будет использоваться Filebeat. Этот инструмент может извлекать логи отовсюду: из txt-файла с логами, из консоли, из Docker-контейнера и самое главное, что нам надо — из топика Apache Kafka.
Filebeat требует конфигурации и отдельного Docker-файла. Вы можете посмотреть в моём проекте как правильно написать конфигурацию Filebeat.
И опять я скажу, что Elastic Stack – это огромная тема. Что почитать на эту тему? Существует прекрасная книга 2025 года - Шривастава Анураг: “Elasticsearch для разработчиков: индексирование, анализ, поиск и агрегирование данных. 2-е изд.”
Не утихают споры по коммерческому использованию Elasticsearch выше 7 версии. Действительно, где-то в районе 2021 года из-за пертурбаций с лицензией Elasticsearch многие продуктовые компании перешли к эксплуатации OpenSearch. В нашем случае проект некоммерческий — поэтому спокойно пользуемся самой последней версией Elasticsearch.
4.2 Разработка Admin Service
Представьте: 50 микросервисов и 50 админок — это и есть тот самый Admin Hell. Чтобы не свести администраторов с ума, мы создали единую админ-панель как центр управления полетами (запомните эту фразу — она нам пригодится в будущем). Однако здесь мы пошли на эту архитектурную авантюру.
Admin Service в нашем эксперименте — это антипаттерн во плоти.
Нарушения DDD и микросервисных принципов:
• Прямой CRUD к MongoDB и PostgreSQL других сервисов
• Отбирает функции доменных сервисов (хеширование паролей, генерация токенов)
• Нарушение границ контекстов (отправка писем, работа с файлами)
• Создание распределенного монолита
Что внутри этого Admin Service:
• Прямые манипуляции с чужими БД
• Бизнес-логика, принадлежащая Auth/Notification Service
• Функции преобразования файлов, нарушающие isolation
• Kafka-продюсер, дублирующий функционал специализированных сервисов
Почему мы так сделали? Это выбор для эксперимента: показать, как НЕ надо строить микросервисы. В продакшене каждая из этих функций должна жить в своем доменном сервисе.
Полный код Admin Service доступен здесь.
4.3 Разработка Content Service
Давайте рассмотрим Content Service — идеальный пример того, как микросервис выполняет функцию «Сбегай мне за пивом». Этот gRPC-сервис технически безупречен: асинхронная работа с MongoDB, строгая типизация через Pydantic, полноценный health-check. Но архитектурно он «нищий» — лишен какой-либо бизнес-логики.
Что делает этот сервис на самом деле? Отдает готовые данные проектов, технологий, сертификатов и т.п. Поддерживает сортировку и мультиязычность. И... всё. Никакой обработки, никаких решений, никакой доменной логики.
Content Service стал просто прокси к базе данных. Вся бизнес-логика работы с контентом — валидация, преобразование, бизнес-правила — живет в других сервисах (в основном в том самом Admin Service). Наш микросервис не управляет своим доменом, а лишь обслуживает запросы к хранилищу.
Полный код Content Service доступен здесь.
4.4 Разработка Auth Service
На фоне «нищего» Content Service наш Auth Service выглядит образцом доменной полноты — но это лишь иллюзия. Формально здесь есть вся необходимая бизнес-логика: JWT-токены, bcrypt-хеширование, Kafka-интеграция для уведомлений. Однако в рамках нашего эксперимента этот сервис оказался лишённым важнейшего права — управлять своим доменом полностью.
Что здесь не так: критическая функция бана пользователей вынесена в Admin Service, нарушен принцип единственной ответственности — бан должен быть частью домена аутентификации, Admin Service может напрямую манипулировать состоянием пользователей, обходя бизнес-логику Auth Service.
Когда администратор банит пользователя через Admin Service, он нарушает инкапсуляцию домена аутентификации. Auth Service не участвует в этом процессе.
Технически Auth Service идеален — асинхронные операции, безопасное хеширование, полноценная работа с токенами. Но архитектурно он урезан в правах: все церемонии соблюдены, но реальная власть находится в другом месте.
Полный код Auth Service доступен здесь.
4.5 Разработка Notification Service
Notification Service лишь притворяется самодостаточным. Да, он грамотно обрабатывает события из Kafka и отправляет письма, но его суверенитет нарушен тем же монстром — Admin Service.
Admin Service имеет прямой доступ к БД notification_db. Самый архитектурно чистый сервис в нашем стеке оказывается таким же «нищим», как и Content Service. Разница лишь в том, что Content Service изначально был пустым, а Notification Service ограблен.
Теперь наш эксперимент действительно «адский»: мы доказали, что достаточно одной архитектурной ошибки (прямой доступ к БД), чтобы обесценить все правильные решения в микросервисной системе.
Полный код Notification Service доступен здесь.
4.6 Разработка API Шлюза (API Gateway)
API Gateway в нашем эксперименте оказался тем редким случаем, где мы следовали лучшим практикам. Кэширование на уровне шлюза — это действительно оправданный паттерн, позволяющий разгрузить бэкенд-сервисы и мгновенно отдавать закэшированные данные. Когда фронтенд запрашивает контент, шлюз сначала проверяет Redis. И тот выдает оттуда кэш, если он там есть.
Полный код API Gateway доступен здесь.
5. Ложное благополучие: тестирование и обеспечение качества
Ирония нашего эксперимента в том, что все модульные и интеграционные тесты успешно проходят. Pytest с гордостью сообщает о 100% покрытии, MongoDB-моки корректно работают, Kafka-консьюмеры исправно обрабатывают сообщения. Технически сервисы безупречны — но не архитектурно.
5.1 Тестирование API с помощью Postman
Давайте протестируем самого главного смутьяна Admin Service:

Отлично, все тесты прошли! Но что-то не так.
Успешное тестирование !== правильная архитектура (специально использую символ строгого неравенства из JavaScript). Мы можем иметь идеальные тесты с полным покрытием, но при этом создавать распределённый монолит, где сервисы бессмысленны, а данные текут через дыры в архитектуре.
5.2 Модульные и интеграционные тесты с помощью pytest
Несмотря на все нарушения DDD и микросервисных принципов, тестовая культура в проекте осталась на высоте. Каждый сервис, будь то «нищий» Content Service или «ограбленный» Notification Service, покрыт полноценными тестами — и это создаёт иллюзию качества.
Мы используем pytest с фикстурами для изолированного тестирования компонентов. Например, Auth Service тестируется с моками MongoDB, где каждый тестовый случай проверяет корректность хеширования паролей, генерации JWT-токенов и работы с refresh-токенами. Интеграционные тесты через TestClient FastAPI и асинхронные pytest-плагины гарантируют, что API-эндпоинты возвращают ожидаемые структуры данных. Даже прямой доступ Admin Service к чужим базам данных не мешает тестам проходить — ведь мы мокаем все сторонние зависимости, создавая изолированную тестовую среду.
Интересно, что наши тесты доказывают: технически сервисы работают безупречно. Content Service корректно отдаёт данные из MongoDB, Notification Service отправляет письма через SMTP, а Auth Service валидирует токены. Проблема в том, что тесты проверяют корректность кода, а не архитектурную целостность. Мы можем иметь 100% покрытие и при этом архитектурный ад — тесты просто не умеют проверять соблюдение DDD-принципов и границ контекстов. Это важный урок: успешное тестирование не равно успешной архитектуре, и наш эксперимент наглядно это демонстрирует.
5.3 О контрактном и сквозном тестировании
В нашем эксперименте мы сознательно отказались от реализации контрактного тестирования (contract testing) и сквозного тестирования (end-to-end testing).
Контрактное тестирование позволило бы нам формализовать взаимодействия между микросервисами и гарантировать, что изменения в одном сервисе не сломают его потребителей. Но в мире, где Admin Service напрямую лезет в чужие базы данных, эти контракты теряют смысл — зачем проверять API, если половина взаимодействий происходит в обход них?
Цена отсутствия E2E-тестов оказалась выше, чем мы предполагали. Без сквозных тестов, которые проходят через весь стек приложения — от интерфейса админки до сохранения данных в MongoDB — мы не смогли обнаружить системные проблемы, вызванные нашими архитектурными компромиссами. Все модульные и интеграционные тесты проходили, но в продакшене могли всплыть тонкие баги, связанные именно с нарушением границ контекстов и прямым доступом к данным.
Эти тесты служат системой раннего предупреждения об архитектурных проблемах, которые модульные тесты просто не в состоянии отследить. В нашем случае их отсутствие позволило антипаттернам беспрепятственно укорениться в системе.
6. Ад продолжается: Внедрение и развертывание
Если вы думали, что архитектурные компромиссы остаются на уровне кода, вы сильно ошибались. Наш «адский эксперимент» перешёл на новый уровень, когда мы начали развёртывание. Все те архитектурные грехи, которые казались безобидными в разработке, превратились в настоящий кошмар при деплое.
6.1 Docker Compose
Docker Compose как зеркало архитектурных проблем... Наш docker-compose.yaml разросся до невероятных размеров, отражая все скрытые зависимости между сервисами. Admin Service требовал доступ к 4 разным базам данных, порядок запуска контейнеров напоминал запуск спутника на Марс. Помните мою фразу про «центр управления полетами».
Тем не менее всё прекрасно работает. Docker Desktop не знает какая мина замедленного действия заложена в приложении (об этом в разделе 7):

Посмотреть docker-compose.yaml можно здесь.
6.2 Kubernetes
Наше путешествие по кругам микросервисного ада достигло логического финала — развертывания в Kubernetes. Казалось бы, что может быть лучше для микросервисов, чем их родная среда оркестрации? Но как показал эксперимент, Kubernetes лишь многократно усилил все наши архитектурные промахи. Наш docker-compose.yaml, и без того напоминавший спагетти, превратился в целую пачку манифестов, где каждый новый Deployment добавлял в систему новые точки отказа.
Kubernetes не спасает от плохой архитектуры — он лишь делает ее недостатки более явными и дорогостоящими. Наш кластер стал живым воплощением принципа GIGO (Garbage in, garbage out).
Но самое смешное, что всё исправно и работает:

Посмотреть полную реализацию Kubernetes можно здесь.
7. Вечные страдания: эксплуатация и техническая поддержка
Если вы думали, что боль заканчивается после деплоя, вы ошибались — она только начинается. Наш «адский эксперимент» перешёл в вечные страдания, когда мы начали эксплуатацию системы. OOMKiller стал нашим постоянным спутником. Казалось бы, мы выделили сервисам достаточное количество оперативной памяти, но оказалось, что архитектурные антипаттерны потребляют ресурсы с аппетитом голодного кота с помойки.
Не верите? А вот вам нотариально заверенный скриншот:

8. Важнейшие выводы, которые я сделал
Вывод №1: Микросервисы — это про границы контекстов, а не про технологии.
Мы идеально настроили gRPC, Kafka и Kubernetes, но полностью провалили главное — разделение доменной ответственности. Получилась распределённая монолитная система, где сервисы формально независимы, но фактически сросшиеся через общие базы данных.
Вывод №2: Прямой доступ к одной БД с разных сервисов — не надо так.
Сначала даёт ощущение скорости разработки, а потом превращает систему в кошмар поддержки. OOMKiller был не причиной, а следствием — он лишь наказывал нас за архитектурные косяки.
Вывод №3: «Бедные сервисы» убивают микросервисную архитектуру на корню.
Сервис без доменной логики — это просто дорогой прокси к базе данных. Content Service, лишённый бизнес-правил, стал бесполезным звеном в цепочке, потребляющим ресурсы и добавляющим сложности.
Вывод №4: Тесты не видят архитектурных проблем.
Можно иметь 100% покрытие и при этом идеально протестированную архитектурную катастрофу. Тесты проверяют код, а не соблюдение DDD-принципов.
Вывод №5: Kubernetes не спасает от плохой архитектуры — он её лишь дороже делает.
Оркестратор многократно усиливает все архитектурные ошибки, превращая их из теоретических проблем в реальные финансовые потери на инфраструктуре.
Вывод №6: Админка — это UI, а не раздутый огромный сервис.
Admin Service не должен быть свалкой всей бизнес-логики системы.
Вывод №7: Архитектурные компромиссы имеют свойство накапливаться.
Одна маленькая уступка («пусть пока Admin Service лезет прямо в БД») тянет за собой шлейф проблем, которые проявляются только на этапе эксплуатации.
Ну и в завершении статьи я вам покажу правильный системный дизайн:

9. Эпилог
История коммерческой разработки полна примеров, где провалы архитектуры приводили к катастрофическим последствиям — наш «адский эксперимент» оказался удивительно точным воспроизведением реальных антипаттернов. Healthcare.gov в 2013 году повторил нашу ошибку с монолитной архитектурой и прямыми подключениями к БД, рухнув при первых же 1100 пользователях и потребовав $2.1 млрд на переделку. Knight Capital в 2012 году наглядно показал, к чему ведет архитектурный хаос — их торговые системы с 8 версиями одного кода, работавшими одновременно, сожгли $460 млн за 45 минут. Даже гиганты вроде Uber и Twitter прошли через мучительный переход от монолитов, где Twitter с его знаменитым «Fail Whale» и Uber с репозиторием на 8 млн строк кодом демонстрируют, как нарушение границ контекстов и отказ от модульности превращают систему в неподдерживаемого зомби.
А ведь сервисы всех этих компаний создавали большие профессиональные команды, где были свои архитекторы и senior-специалисты.
Наш проект, по сути, стал испытательным полигоном, где мы повторили все эти ошибки — от «бедных сервисов» до прямого доступа к чужим базам данных, — чтобы на практике показать, почему архитектурные принципы существуют не просто как теория, а как суровая необходимость, проверенная миллиардными потерями.
Полный код проекта доступен здесь.
Комментарии (4)

chemtech
09.11.2025 06:48ELK слишком жирный. Вместо ELK лучше использовать https://github.com/VictoriaMetrics/VictoriaLogs/

Virviil
09.11.2025 06:48Правильный системный дизайн в данном случае и в остальных 99.9% - это монолит.
olku
Интересная статья, если проработать каждый пункт принятия решений, получится книжка. Некоторые моменты:
Полное название - Traefik Hub API Gateway
Событийная архитектура может жить и в монолите. Так рекомендуют строить небольшие системы "на вырост", не делая их сразу распределенными.
Выбор БД зафейлен. Если уж идти по DDD, то каждый домен может иметь собственные требования к согласованности и персистентости. Движок подбирается индивидуально под домен, in-memory тоже вариант.
Жаль что не рассмотрена декомпозиция методом Event Storming. Это как раз DDD штука.
ELK все. OpenTelemetry.