MidТеория2 min

Обзор паттернов системного дизайна

Классификация паттернов: data, reliability, scaling, coordination. Когда какой применять

Зачем нужны паттерны

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

В отличие от "банды четырёх" (GoF), которая описывает паттерны внутри одного процесса, системные паттерны работают на уровне сервисов, баз данных, очередей и сетей. Здесь нет shared memory и транзакций -- только сеть, которая иногда лежит.

Четыре категории паттернов

Условная классификация по цели применения:

┌─────────────────────────────────────────────────────────────┐
│                    Паттерны системного дизайна              │
├──────────────┬──────────────┬──────────────┬────────────────┤
│     Data     │  Reliability │   Scaling    │  Coordination  │
│              │              │              │                │
│ Saga         │ Circuit      │ Fan-out      │ Distributed    │
│ Outbox       │ breaker      │ Rate limit   │ lock           │
│ CDC          │ Retry +      │ Unique ID    │ Leader         │
│ Materialized │ backoff      │ Sharding     │ election       │
│ view         │ Bulkhead     │ CDN          │ Consensus      │
│ Event        │ DLQ          │ Load         │ Service        │
│ sourcing     │ Timeout      │ balancing    │ discovery      │
│              │ Hedging      │              │                │
└──────────────┴──────────────┴──────────────┴────────────────┘

Data patterns -- управление данными

Главная проблема: как гарантировать консистентность данных между разными сервисами и хранилищами без распределённых транзакций.

  • Saga -- длинные бизнес-транзакции через последовательность локальных + компенсации
  • Transactional Outbox -- атомарность записи в БД и отправки в очередь
  • CDC (Change Data Capture) -- стриминг изменений БД через лог репликации
  • Materialized view -- предвычисленный результат агрегации
  • Event Sourcing -- хранить события, а не состояние (покрыто в 17.arch-patterns/4.cqrs-es)

Reliability patterns -- надёжность при сбоях

Сеть ненадёжна, сервисы падают. Эти паттерны ограничивают blast radius.

  • Circuit breaker -- отключение вызовов к упавшему сервису
  • Retry + exponential backoff + jitter -- умное повторение
  • Bulkhead -- изоляция ресурсов (как отсеки корабля)
  • DLQ (Dead Letter Queue) -- куда складывать отравленные сообщения
  • Timeout propagation -- контекст с дедлайном через всю цепочку вызовов
  • Hedged requests -- дублирование запроса к медленным реплика

Scaling patterns -- горизонтальное масштабирование

Проблема: одна машина не тянет нагрузку. Эти паттерны распределяют работу.

  • Fan-out (write vs read) -- предвычисление лент/фидов
  • Rate limiter -- защита от перегрузки (token/leaky bucket, sliding window)
  • Unique ID generators -- Snowflake, UUID v7, ULID для распределённой генерации
  • Two-stage processing -- быстрый ack + асинхронная обработка

Coordination patterns -- координация узлов

Когда N узлов должны договориться о чём-то (кто лидер, кто держит лок).

  • Distributed lock -- Redis/etcd/ZooKeeper для критических секций
  • Leader election -- выбор одного активного узла из кластера
  • Consensus -- Raft, Paxos для реплицированного состояния
  • Fencing tokens -- защита от stale lock holders

Когда применять: cheat sheet

Проблема Паттерн Пример применения
Транзакция через N сервисов Saga Оформление заказа: платёж + резерв + доставка
Надо отправить событие после commit Outbox После создания user -- push в Kafka
Миграция БД без downtime CDC (Debezium) PostgreSQL -> Elasticsearch sync
Лента пользователя с 1000 подписок Fan-out on write (push) Twitter timeline
Лента знаменитости с 100M фолловеров Fan-out on read (pull) Celebrity feed
Дашборд с агрегацией 1B строк Materialized view Analytics realtime
API внешнего партнёра иногда лежит Circuit breaker Payment gateway
Транзиентные сетевые ошибки Retry + jitter HTTP call to microservice
Один медленный endpoint не должен валить всё Bulkhead Separate thread pool per downstream
Poison message не даёт обрабатывать очередь DLQ Сообщение с невалидным JSON
1M RPS на endpoint Rate limiter Public API
Распределённая генерация ID Snowflake / UUID v7 Order IDs across shards
Только один worker должен обработать задачу Distributed lock Cron job в кластере
Кто лидер в HA кластере Leader election Active-passive failover

Anti-patterns и подводные камни

Retry без idempotency -- вы только что списали деньги дважды. Retry работает только с идемпотентными операциями (см. 02.design-approaches/9.idempotency.md).

Circuit breaker без fallback -- закрытый цепной выключатель без плана B = ошибка пользователю. Подумайте: cached response, graceful degradation, queue for later.

Saga без компенсаций -- если шаг 4 упал, а компенсаций нет, данные неконсистентны навсегда.

Distributed lock без fencing -- GC pause в JVM может удержать лок дольше TTL, два процесса получат "эксклюзивный" доступ одновременно.

Dual-write в БД и очередь без outbox -- сеть мигнула между коммитом и publish, событие потерялось.

Fan-out on write для всех -- у Лионеля Месси 500M подписчиков. Push каждого твита в 500M inbox -- это 500M записей. Убьёт БД.

Rate limit только на балансере -- легитимный пользователь с одного IP получает 429, а атакующий с ботнета проходит.

Композиция паттернов

Реальные системы комбинируют паттерны:

Order placement flow:

Client --> API Gateway [rate limit, auth]
              │
              v
        [Order Service]
              │
              ├── INSERT order + INSERT outbox (1 transaction)
              │
              v
        [Outbox Publisher] -- worker
              │
              v
          [Kafka: orders.created]
              │
              ├──> [Payment Service] ---> Saga step 1
              │         [circuit breaker on Stripe]
              │         [retry with jitter]
              │
              ├──> [Inventory Service] --> Saga step 2
              │
              └──> [Notification Service]
                        [DLQ for failed emails]

Один сценарий использует: rate limit, outbox, saga, circuit breaker, retry, DLQ. Это нормально.

Trade-offs каждого паттерна

Каждый паттерн добавляет сложности. Перед применением спросите:

  1. Нужна ли вообще эта проблема? Saga нужна, только если операция реально распределённая. Для монолитной БД хватит ACID транзакции.
  2. Какая цена? Outbox = +1 таблица, +worker, +мониторинг. Оправдано при важности событий.
  3. Где ломается? Rate limiter с Redis -- Redis single point of failure. Нужен кластер.
  4. Что мониторить? Circuit breaker state, outbox lag, saga timeout rate, DLQ depth.

Что дальше

В этом разделе мы подробно разберём implementation каждого значимого паттерна с кодом на PHP и Go: saga (оркестровка и хореография), outbox + publisher worker, fan-out стратегии, materialized views, два-стадийная обработка, алгоритмы rate limiter, генераторы ID, распределённые локи с fencing, resilience-паттерны, DLQ, и решения dual-write проблемы.

Выводы

  • Паттерны -- не догма, а набор инструментов под конкретные проблемы
  • Четыре категории: data, reliability, scaling, coordination
  • Большинство реальных систем комбинируют 5-10 паттернов одновременно
  • Каждый паттерн имеет цену: сложность, латентность, новые точки отказа
  • Применяйте, когда проблема доказана метриками, а не "на всякий случай"