MidТеория5 min

Monolith vs Microservices

Модульный монолит как компромисс, стоимость сетевых границ, когда микросервисы оправданы, матрица team size vs архитектура

Что это

  • Monolith: одно развёртываемое приложение, одна БД, один репо (обычно). Все модули зовут друг друга in-process.
  • Microservices: много небольших сервисов, каждый со своей БД (или хотя бы схемой), деплоятся независимо, общаются по сети.
  • Модульный монолит: монолит, но с чёткими внутренними модулями, которые могут быть вырезаны в сервисы, если потребуется.
MONOLITH                        MICROSERVICES
 ┌─────────────────┐             ┌────┐   ┌────┐   ┌────┐
 │ users │ orders  │             │usr │   │ord │   │pay │
 │ pay   │ catalog │             └─┬──┘   └─┬──┘   └─┬──┘
 └────────┬────────┘                └───HTTP/gRPC────┘
          │                                  │
        ┌─┴─┐                               ┌┴┐ ┌┴┐ ┌┴┐
        │DB │                              DB  DB  DB
        └───┘                              (у каждого своя)

Когда Monolith лучше

Стартап / ранняя стадия

  • Команда < 15 инженеров. Все помещаются в одном репо, общение -- в одной комнате.
  • Domain ещё неясен. Границы сервисов меняются -- внутри монолита дёшево, между сервисами -- больно.
  • Быстрая итерация. Один deploy, одна миграция, один git log.
  • Простая транзакционность. ACID через одну БД без saga.

Preservation of sanity

  • Меньше ops-сложности: один процесс на мониторинг, один log-stream, один dashboard.
  • Стек трассы показывает весь запрос.
  • Рефакторинг атомарный: "переименовал класс" -- IDE делает везде. В микросервисах это кросс-репозиторный ad-hoc.

"Majestic Monolith"

Shopify, Stack Overflow, Basecamp, GitHub -- крупные продукты на монолитах. Rails-монолит Shopify -- миллионы строк, десятки тысяч модулей, тысячи инженеров. Работает, потому что у них модульная дисциплина (packs, публичные API модулей, owned-by метаданные).

Stack Overflow -- работает на ~10 SQL Server'ах и монолитном приложении. Это нормально.

Когда Microservices оправданы

  • Разные скорости и циклы релизов разных доменов. Биллинг трогают раз в месяц, каталог -- 10 раз в день.
  • Независимое масштабирование. Video transcode надо 20 мощных машин, users -- одной хватает.
  • Разные технологии стека. ML-сервис на Python, realtime на Go, CRUD на PHP -- разделить естественно.
  • Разные команды, разные юрисдикции (GDPR, платежи в PCI-зоне).
  • Команды больше 50-100 инженеров. Conway: физически нельзя координировать 200 инженеров вокруг одного репо.
  • Разные SLA. Платежи -- 99.99%, фича "похожее" -- 99%.

Ключевое условие

Микросервисы работают, если у вас есть платформенная команда, которая сделала:

  • CI/CD с канареечными релизами,
  • observability (trace, metrics, log aggregation),
  • service mesh или хотя бы retry/circuit breaker в клиенте,
  • schema registry или хотя бы code-gen клиентов.

Без этого вы получаете "distributed monolith" -- все проблемы монолита плюс сетевые.

Транзитные издержки микросервисов

Издержка Что меняется Как смягчать
Сеть между сервисами Latency +1-10ms на hop, сбои Circuit breaker, retry, bulkhead
БД на каждый сервис Нет JOIN, нет ACID через границу Saga, outbox, CDC
Аутентификация распределённая JWT / mTLS везде Service mesh (Istio/Linkerd)
Версионирование API Сервис A ломает сервис B при деплое Contract testing, backward-compat правила
Дебаг Один запрос -- N сервисов Distributed tracing (OpenTelemetry)
Деплой Один процесс → десятки CI/CD, GitOps, Kubernetes
Мониторинг SLO per service Общий dashboard + алерт-матрица

Правило большого пальца: добавляя границу сервиса, вы добавляете стоимость сетевого, эксплуатационного и организационного coupling. Оплачивается эта цена только если даёт выгоду больше.

Гибридные решения

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

Один процесс, но внутри -- изолированные модули с публичными API, приватной БД (схемой), запретом обращаться к чужим таблицам.

Symfony / Laravel / Spring: bundles / packages / modules. Ruby on Rails: Shopify packs. .NET: vertical slice architecture.

Ключевые правила:

  • Нет прямых обращений к чужим таблицам. Только через интерфейс модуля.
  • Публичный API модуля. Всё остальное приватно.
  • Owned-by. Каждый модуль имеет владельца.

Macroservices / Domain services

Не сотни нано-сервисов, а 5-15 "крупных" сервисов по границам bounded context (DDD). Typical Amazon / Google pattern in practice.

Strangler Fig pattern

Монолит обрастает сервисами на периферии, постепенно функциональность переносится. Легаси не переписывается разом.

Матрица выбора: team size vs архитектура

Размер команды Рекомендация Причина
1-5 Монолит Нет параллельного развития
5-15 Монолит, возможно модульный Всё ещё помещаются в голове
15-50 Модульный монолит Начать выделять границы
50-200 Macroservices / domain services Независимые команды
200+ Микросервисы + платформа Иначе координация умрёт

Другая ось -- скорость доставки vs сложность ops:

  deploys/day
     ▲
  100│                   micro
     │              .
   10│        .  macro
     │     .
    1│  monolith
     │
     └────────────────▶ ops complexity

Антипаттерны микросервисов

Антипаттерн Что не так
Distributed monolith Сервисы деплоятся вместе и знают друг о друге -- получили минусы обоих
Shared DB Несколько сервисов пишут в одни таблицы -- это монолит с сетевым интерфейсом
Nano services Сервис "add_two_numbers" -- overhead больше пользы
Chatty services 10 HTTP-вызовов на один пользовательский запрос -- p99 умер
No platform team Разработчики каждый сами поднимают K8s, трейсинг, CI -- квадратичные затраты

Код: один модуль двумя способами

<?php

declare(strict_types=1);

namespace App\Billing\Application;

use App\Orders\Contracts\OrderFinder; // Public API of Orders module.

/**
 * Modular monolith: Billing module calls Orders through a contract.
 * Billing does NOT touch orders table directly.
 * If Orders is extracted to a microservice later, this interface
 * becomes an HTTP / gRPC client without changing callers.
 */
final class ChargeOrderHandler
{
    public function __construct(
        private readonly OrderFinder $orders,
        private readonly PaymentGateway $gateway,
        private readonly InvoiceRepository $invoices,
    ) {}

    public function handle(ChargeOrderCommand $cmd): void
    {
        $order = $this->orders->findById($cmd->orderId)
            ?? throw new OrderNotFoundException($cmd->orderId);

        $payment = $this->gateway->charge($order->total, $cmd->cardToken);
        $this->invoices->save(new Invoice($order->id, $payment->id, $order->total));
    }
}
В монолитной версии `OrderFinder` -- интерфейс в рамках процесса; NPE невозможен, транзакция возможна. В сервисной версии та же семантика требует retry, timeout, circuit breaker, distributed tracing.

Миграция из монолита в сервисы

  1. Выделить границы в коде (модули) -- без декомпозиции инфраструктуры.
  2. Ввести контракты между модулями через явные интерфейсы.
  3. Внедрить инфраструктуру: CI/CD, tracing, mesh.
  4. Strangler Fig: вынести наименее связный модуль как сервис.
  5. Повторять только когда монолит реально мешает.

Правило: не трогай границы, пока они не болят.

Выводы

  • Монолит -- дефолт для команд < 50 инженеров и стадии до PMF.
  • Модульный монолит даёт 80% выгод микросервисов за 20% сложности.
  • Микросервисы оправданы, когда боль coordination превышает боль distribution.
  • Транзитные издержки микросервисов: сеть, распределённые транзакции, observability, CI/CD.
  • "Majestic Monolith" (Shopify, Stack Overflow) -- рабочая стратегия на годы.
  • Distributed monolith -- худший вариант, избегайте через независимые БД и явные контракты.