Как каждый из подходов организует код, разделяет данные и какие библиотеки требует в экосистеме .NET.

Фото: Nicolás Gutiérrez на Unsplash

Разработчики редко выбирают архитектуру в вакууме. Обычно мы сталкиваемся с разными подходами поэтапно: классические монолиты попадаются в легаси-проектах или на старте новых продуктов, микросервисы появляются, когда компания хочет независимо масштабировать команды, а к модульному монолиту приходят, когда бизнес понимает, что распределённые системы тянут за собой слишком дорогие расходы на поддержку.

Каждый новый подход часто подают как очевидную замену предыдущему. Но на практике это просто разные точки на шкале компромиссов между простотой эксплуатации и жёсткой изоляцией кода.

В этой статье мы сравним классический монолит, модульный монолит и микросервисы по ключевым инженерным критериям, разберём конкретные библиотеки .NET для каждого варианта и определим понятные правила, как выбрать подходящую архитектуру под задачу.

1. Как устроен каждый подход на самом деле

КЛАССИЧЕСКИЙ МОНОЛИТ
┌───────────────────────────────────────────────┐
│                Один процесс                   │
│  Контроллеры ─── Сервисы ─── Доступ к данным  │
└───────────────────────┬───────────────────────┘
                        ▼
               [Единая база данных]

МОДУЛЬНЫЙ МОНОЛИТ
┌───────────────────────────────────────────────┐
│                Один процесс                   │
│  ┌─────────────┐ ┌─────────────┐ ┌──────────┐ │
│  │   Модуль    │ │   Модуль    │ │  Модуль  │ │
│  │  Каталога   │ │   Заказов   │ │  Оплаты  │ │
│  └──────┬──────┘ └──────┬──────┘ └────┬─────┘ │
│         │ (Изолированные схемы/таблицы)│      │
└─────────┼───────────────┼─────────────┼───────┘
          ▼               ▼             ▼
       [catalog.*]    [orders.*]    [billing.*]
               (Один инстанс БД)

МИКРОСЕРВИСЫ
┌──────────────┐   ┌──────────────┐   ┌──────────────┐
│ Catalog API  │   │  Orders API  │   │ Billing API  │
│  (Процесс 1) │   │ (Процесс 2)  │   │ (Процесс 3)  │
└──────┬───────┘   └──────┬───────┘   └──────┬───────┘
       │ Сеть             │ Сеть             │ Сеть
       ▼                  ▼                  ▼
 [Catalog DB]        [Orders DB]       [Billing DB]

Классический монолит

Классический монолит собирается в один исполняемый файл или единицу развёртывания (например, один проект ASP.NET Core Web API), где все бизнес-домены лежат в одном решении. Код обычно делят горизонтально по техническим слоям: представление (API), бизнес-логика и доступ к данным. Все слои читают и пишут в одну общую схему базы данных без каких-либо технических ограничений на междоменные SQL-запросы и JOIN.

Модульный монолит

Модульный монолит по-прежнему запускается как один процесс на одном сервере и обычно работает с одним инстансом базы данных. Однако внутри кодовая база строго разделена на независимые бизнес-модули (например, Orders, Inventory, Billing).

Модули не могут напрямую обращаться к внутренним классам других модулей. Наружу выставляются только явные публичные контракты (интерфейсы или DTO). Каждый модуль сам владеет своими данными — чаще всего это отдельные схемы в БД (orders.Orders, billing.Invoices) или изолированные наборы таблиц. Прямые SQL-запросы между таблицами разных модулей запрещены на уровне архитектуры кода.

Микросервисы

Микросервисная архитектура выносит каждую бизнес-возможность в самостоятельный сервис, который деплоится отдельно и работает в собственном процессе или контейнере. У каждого микросервиса своя закрытая база данных — ни один другой сервис не имеет права обращаться к ней напрямую. Всё общение идёт по сети через синхронные протоколы (HTTP/REST, gRPC) или асинхронные брокеры сообщений.

2. Сравнение по ключевым инженерным критериям

Деплой и сложность поддержки

  • Классический монолит: минимальная нагрузка на инфраструктуру. Один CI/CD-пайплайн, один артефакт сборки и элементарная локальная отладка (достаточно нажать F5 в Visual Studio или выполнить dotnet run).

  • Модульный монолит: сохраняет всю простоту деплоя обычного монолита. Вы собираете один Docker-контейнер или выкатываете один сервис, но жёстко контролируете порядок внутри кода на этапе компиляции.

  • Микросервисы: высокая операционная сложность. Потребуется оркестрация контейнеров (Kubernetes или аналоги), API-шлюзы, Service Discovery, централизованный сбор логов, распределённая трассировка и отдельные пайплайны деплоя для каждого сервиса.

Границы кода и инкапсуляция

  • Классический монолит: границы легко размываются. Когда горят сроки, разработчики начинают срезать углы — лезут напрямую в таблицы соседнего домена или инжектят внутренние сервисы чужого слоя. Со временем код превращается в запутанный клубок зависимостей.

  • Модульный монолит: границы защищены правилами компиляции (модификаторы доступа internal в C#, закрытые ссылки между проектами и архитектурные тесты вроде NetArchTest). Один модуль физически не видит внутреннюю реализацию другого.

  • Микросервисы: границы обеспечены на уровне процессов ОС и сетевых правил. Срезать угол через прямой вызов в памяти не получится, даже если очень захочется.

Работа с данными и согласованность

  • Классический и модульный монолиты: стандартные транзакции базы данных (ACID) работают из коробки. Если при оформлении заказа нужно зарезервировать товар на складе, обе операции спокойно коммитятся в одной транзакции. Откат происходит мгновенно и атомарно.

  • Микросервисы: обычные ACID-транзакции между сервисами невозможны, так как базы изолированы. Приходится переходить на согласованность в конечном счёте (Eventual Consistency), асинхронные события интеграции, паттерн Transactional Outbox и распределённые саги (Saga).

Производительность и задержки

  • Классический и модульный монолиты: общение идёт внутри одного процесса. Вызов соседнего домена — это обычный вызов метода в памяти, который занимает наносекунды без затрат на сериализацию и риска потери сетевых пакетов.

  • Микросервисы: любой запрос между сервисами уходит в сеть. Даже внутри одной облачной сети походы по HTTP или gRPC занимают миллисекунды и требуют настройки таймаутов, повторных попыток (retries) и предохранителей (circuit breakers).

3. Какие библиотеки и фреймворки нужны в .NET

Каждый архитектурный подход опирается на свой набор инструментов в экосистеме .NET.

Архитектура

Основные библиотеки и инструменты .NET

Классический монолит

• Entity Framework Core / Dapper

• Встроенный DI-контейнер ASP.NET Core

• FluentValidation

Модульный монолит

• NetArchTest.Rules (архитектурные тесты)

• MediatR / Wolverine (внутрипроцессный медиатор и события)

• EF Core (несколько DbContext / разделение по схемам БД)

• Quartz.NET / Hangfire (фоновая обработка задач и внутренний Outbox)

Микросервисы

• MassTransit (абстракция над шиной сообщений)

• RabbitMQ / Apache Kafka / Redis (брокеры сообщений)

• YARP (Reverse Proxy / API-шлюз)

• Polly / Microsoft.Extensions.Resilience (отказоустойчивость)

• OpenTelemetry (распределённая трассировка и метрики)

• gRPC.Net.Client (синхронное межсервисное взаимодействие)

Инструменты для классического монолита

Поскольку здесь нет распределённого состояния и строгой изоляции модулей, набор библиотек минимален:

  • Доступ к данным: Entity Framework Core или Dapper, работающие с единым контекстом базы данных.

  • Внедрение зависимостей: стандартный контейнер Microsoft.Extensions.DependencyInjection.

  • Валидация: FluentValidation, встроенный прямо в пайплайн ASP.NET Core до вызова контроллеров или сервисов.

Инструменты для модульного монолита

Здесь нужны библиотеки, которые помогают удерживать изоляцию модулей внутри одного решения:

  • Контроль границ: NetArchTest.Rules. Архитектурные тесты проверяют, что код из Modules.Orders нигде не ссылается на Modules.Billing.Infrastructure. Если кто-то нарушит правило, сборка упадёт ещё на этапе CI.

  • Общение внутри процесса: MediatR или Wolverine. Используются для отправки команд и доменных уведомлений между модулями без жёсткой связки их реализаций.

  • Изоляция данных: EF Core с отдельным классом DbContext для каждого модуля, привязанным к своей схеме БД (например, через modelBuilder.HasDefaultSchema("orders")).

  • Фоновые задачи: Hangfire или Quartz.NET для асинхронной обработки внутренних событий между модулями без поднятия внешнего брокера сообщений.

Инструменты для микросервисов

Микросервисам требуется целый стек инфраструктурных библиотек для работы по сети и обмена событиями:

  • Асинхронный обмен сообщениями: MassTransit выступает единой шиной поверх брокеров вроде RabbitMQ, Apache Kafka или Redis. Он берёт на себя реализацию паттерна «Издатель — Подписчик» (Pub/Sub), повторы при ошибках и очереди необработанных сообщений (Dead-Letter Queues).

  • Паттерн Transactional Outbox: встроенная интеграция MassTransit с EF Core или фоновые воркеры. Когда сервис сохраняет сущность в свою базу, он в той же локальной транзакции записывает сообщение в таблицу Outbox. Это гарантирует отправку события в брокер, даже если приложение упадёт сразу после коммита.

  • API-шлюзы: YARP (Yet Another Reverse Proxy) — быстрый реверс-прокси для маршрутизации клиентских запросов к нужным микросервисам, терминирования SSL и ограничения частоты запросов (Rate Limiting).

  • Отказоустойчивость: Polly (или Microsoft.Extensions.Resilience в свежих версиях .NET). Оборачивает синхронные HTTP- и gRPC-вызовы в политики повторов с экспоненциальной задержкой и размыкатели цепи (Circuit Breakers).

  • Распределённая наблюдаемость: OpenTelemetry. Прокидывает сквозной идентификатор запроса (Trace ID) через HTTP-заголовки и сообщения брокера, чтобы можно было отследить путь одного запроса пользователя через пять разных сервисов.

4. Проблема общей базовой библиотеки в микросервисах

В .NET-командах, пишущих микросервисы, часто создают общую базовую библиотеку и распространяют её как внутренний NuGet-пакет (например, Company.Common или Company.Core).

Обычно в такой пакет выносят:

  1. Настройку брокера сообщений (конфигурацию MassTransit и топологию очередей RabbitMQ).

  2. Единую обработку ошибок (middleware) и формат логирования.

  3. Подключение OpenTelemetry и трассировки.

  4. Базовые модели DTO и извлечение клеймов авторизации.

                  ┌───────────────────────────────┐
                  │      Общий NuGet-пакет        │
                  │        "Company.Core"         │
                  │  (MassTransit, Polly, DTO)    │
                  └───────┬───────────────┬───────┘
                          │               │
            ┌─────────────┴─────┐   ┌─────┴─────────────┐
            ▼                   ▼   ▼                   ▼
    ┌───────────────┐   ┌───────────────┐   ┌───────────────┐
    │  Catalog API  │   │  Orders API   │   │  Billing API  │
    │   (.NET 8)    │   │   (.NET 9)    │   │   (.NET 8)    │
    │   Core 1.2    │   │   Core 2.0    │   │   Core 1.0    │
    └───────────────┘   └───────────────┘   └───────────────┘

На старте такой подход экономит время, но со временем начинает тормозить разработку:

  • Ловушка распределённого монолита: если в базовый пакет попадают общие доменные модели или правила бизнес-валидации, любое изменение логики требует обновить библиотеку, пересобрать NuGet-пакет, а затем обновить и заново задеплоить все сервисы. Сервисы разделены физически, но намертво связаны логически.

  • Зависимость от версий фреймворка: если Company.Core переезжает на новую мажорную версию .NET или обновляет крупную зависимость, остальные сервисы не могут спокойно оставаться на старой версии. Приходится поддерживать мультитаргетинг (net8.0; net9.0) в десятках репозиториев.

  • Эволюция контрактов: при работе по паттерну Pub/Sub через RabbitMQ или Kafka контракты событий должны оставаться обратно совместимыми. Если общая библиотека переименует поле или поменяет тип данных в событии, старые консьюмеры, которые ещё не успели обновиться, начнут падать при десериализации.

Практическое правило: держите в общих библиотеках только чистую инфраструктурную обвязку (логирование, телеметрию, базовые примитивы транспорта). Никогда не кладите в общий NuGet-пакет бизнес-логику, общую валидацию или сущности базы данных.

5. Структура проектов (деревья решений .NET)

1. Классический монолит

CompanyApp.sln
├── CompanyApp.WebAPI/            # Контроллеры, middleware, настройка DI
├── CompanyApp.Core/              # Общие доменные сущности (Orders, Products, Users)
└── CompanyApp.Infrastructure/    # Единый DbContext со всеми DbSet и репозиториями

2. Модульный монолит

CompanyApp.sln
├── CompanyApp.Host/              # Единый хост ASP.NET Core Web API, связывает модули
├── Modules/
│   ├── Orders/
│   │   ├── Orders.Contracts/     # Только публичные интерфейсы и DTO
│   │   └── Orders.Core/          # Внутренние сущности, OrdersDbContext, обработчики
│   ├── Catalog/
│   │   ├── Catalog.Contracts/    # Только публичные интерфейсы и DTO
│   │   └── Catalog.Core/         # Внутренние сущности, CatalogDbContext, обработчики
│   └── Billing/
│       ├── Billing.Contracts/    # Только публичные интерфейсы и DTO
│       └── Billing.Core/         # Внутренние сущности, BillingDbContext
└── BuildingBlocks/               # NetArchTest, общие интерфейсы шины событий

3. Набор микросервисов

# Отдельные репозитории или независимые sln-файлы
CatalogService.sln
├── CatalogService.Api/
├── CatalogService.Infrastructure/   # Закрытая БД каталога
└── Dockerfile

OrdersService.sln
├── OrdersService.Api/
├── OrdersService.Infrastructure/    # Закрытая БД заказов
└── Dockerfile

ApiGateway.sln
└── YarpGateway/                     # Маршрутизация через реверс-прокси

SharedPackages/
└── Company.ServiceDefaults/        # OpenTelemetry, базовая настройка MassTransit (NuGet)

6. Как сделать выбор

Выбор архитектуры — это всегда оценка ограничений инфраструктуры, структуры команды и реальных требований к нагрузке.

                       Начало выбора
                             │
                             ▼
            У вас несколько независимых команд,
           которым нужны свои циклы релизов?
                          /     \
                        Да       Нет
                       /           \
                      ▼             ▼
               [Микросервисы]   Домен сложный и делится на
                                несколько отдельных областей?
                                       /     \
                                     Да       Нет
                                    /           \
                                   ▼             ▼
                         [Модульный монолит]   [Классический монолит]
  1. Выбирайте классический монолит, если:

    • Вы делаете небольшой или средний проект, прототип или внутренний инструмент с понятными требованиями.

    • У вас небольшая команда (1–4 разработчика), где важнее всего скорость выхода на рынок и минимум возни с инфраструктурой.

    • В проекте нет жёстко изолированных бизнес-доменов, и дробление на модули создаст только лишний бойлерплейт.

  2. Выбирайте модульный монолит, если:

    • Бизнес-домен растёт и включает несколько самостоятельных направлений (например, e-commerce с каталогом, оплатой, доставкой и бухгалтерией), которые не должны смешиваться в коде.

    • Вам нужны чистые границы и независимое тестирование, но вы не готовы платить за поддержку Kubernetes, распределённой трассировки и сетевых сбоев.

    • Вы хотите сохранить возможность в будущем безболезненно вынести отдельные модули в микросервисы без переписывания бизнес-логики.

  3. Выбирайте микросервисы, если:

    • В компании работает несколько независимых команд, которым нужно катить релизы по собственному графику без общей очереди деплоя и конфликтов при слиянии веток.

    • Отдельные части системы имеют радикально разную нагрузку (например, сервис трекинга в реальном времени требует 50 реплик, а сервис биллинга справляется на двух).

    • У команды уже есть зрелая DevOps-культура, мониторинг и опыт поддержки распределённых систем в продакшене.

Оригинал статьи опубликован на Medium.