Что это
- 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));
}
}
Миграция из монолита в сервисы
- Выделить границы в коде (модули) -- без декомпозиции инфраструктуры.
- Ввести контракты между модулями через явные интерфейсы.
- Внедрить инфраструктуру: CI/CD, tracing, mesh.
- Strangler Fig: вынести наименее связный модуль как сервис.
- Повторять только когда монолит реально мешает.
Правило: не трогай границы, пока они не болят.
Выводы
- Монолит -- дефолт для команд < 50 инженеров и стадии до PMF.
- Модульный монолит даёт 80% выгод микросервисов за 20% сложности.
- Микросервисы оправданы, когда боль coordination превышает боль distribution.
- Транзитные издержки микросервисов: сеть, распределённые транзакции, observability, CI/CD.
- "Majestic Monolith" (Shopify, Stack Overflow) -- рабочая стратегия на годы.
- Distributed monolith -- худший вариант, избегайте через независимые БД и явные контракты.