Architecture overview¶
Короткий справочник о том, как устроена backend-часть проекта и какие паттерны мы используем. Прочитать за 10–15 минут — чтобы ориентироваться в коде и в разговорах команды.
Структура документа повторяет каталог паттернов с microservices.io/patterns. Для каждой категории — таблица со статусом «Используем / Не используем / Планируется» и коротким объяснением, как именно у нас реализован паттерн.
Карта системы¶
Верхнеуровневый срез: откуда трафик приходит, какие сервисы есть, чем они обмениваются.
flowchart LR
Client[Клиент<br/>web / mobile]
GW[API Gateway<br/>Traefik<br/>TLS, JWT verify,<br/>rate-limit]
subgraph K8s["Kubernetes кластер"]
USR[user-service<br/>auth + profiles]
REV[review-service<br/>отзывы + реакции]
MED[media-service<br/>загрузка + storage]
NOT[notification-service<br/>push/email/sms]
subgraph Infra["общая инфра"]
PG[(Postgres<br/>schema per service)]
RDS[(Redis<br/>cache, rate-limit,<br/>dedup)]
KFK[(Kafka<br/>kazmaps.*)]
S3[(S3 / MinIO<br/>user uploads)]
end
end
OBS[Observability<br/>Prometheus, Loki,<br/>Tempo, Grafana]
Client -->|HTTPS| GW
GW -->|/v1/*<br/>signed HMAC headers| USR
GW -->|/v1/*| REV
GW -->|/v1/*| MED
GW -->|/v1/*| NOT
USR <-->|/internal/*| REV
REV <-->|/internal/*| MED
USR -->|outbox| KFK
REV -->|outbox| KFK
MED -->|outbox| KFK
KFK --> NOT
KFK --> REV
KFK --> MED
USR --> PG
REV --> PG
MED --> PG
NOT --> PG
USR --> RDS
REV --> RDS
NOT --> RDS
MED --> S3
USR -.otlp.-> OBS
REV -.otlp.-> OBS
MED -.otlp.-> OBS
NOT -.otlp.-> OBS
Правила на диаграмме:
- Внешний трафик — всегда через gateway. Прямого доступа к сервисам извне нет.
- Между сервисами: синхронные запросы —
/internal/*(HTTP, HMAC внутренний токен), асинхронные факты — через Kafka и outbox. - Каждый сервис владеет своей схемой в Postgres. Cross-service JOIN запрещены.
Тип системы¶
Microservices architecture. Каждый сервис — отдельный git-репозиторий со своим жизненным циклом, pull request'ами, релизами и независимым деплоем. Межсервисное взаимодействие двумя способами:
- Event-driven через Kafka — основной способ сигнализировать о фактах
между сервисами (
review.created,user.banned,photo.uploaded). - Синхронные HTTP-вызовы через внутренние REST-endpoint'ы — когда одному сервису нужны данные другого «прямо сейчас» для ответа на запрос пользователя. Внешний трафик всегда приходит через API Gateway.
Каждый сервис владеет своей БД (Postgres), своей схемой и своим outbox'ом.
Decomposition¶
Границы сервисов = границы владения данными. Один сервис владеет одной доменной областью и её таблицами; всё, что нужно от другой области, получается через событие или internal HTTP-вызов.
| Паттерн | Статус | Как |
|---|---|---|
| Decompose by business capability | Используем | Сервисы нарезаны по capability'ям: auth+профили (user), отзывы (review), медиа (media), уведомления (notification). |
| Decompose by subdomain / DDD | Используем | Subdomain'ы совпадают с сервисами; внутри — стандартный DDD-слой domain/ с entity и sentinel-ошибками. |
| Strangler application | Не используем | Greenfield-проект, нет legacy-монолита, который надо душить. |
| Self-contained service | Используем частично | По возможности сервис отвечает за запрос полностью; для cross-service list-view используем API composition. |
Практическое правило: если две capability'и начинают постоянно запрашивать данные друг у друга синхронно — проверяй, правильно ли проведена граница. Событийная связь допустима, плотная RPC-связь — повод пересобрать границы.
Deployment¶
Unit деплоя — Docker-образ одного сервиса. Образ собирается в CI репозитория сервиса, пушится в registry, Helm/kustomize раскатывает в Kubernetes. Каждый сервис деплоится своим пайплайном, независимо от остальных.
| Паттерн | Статус | Как |
|---|---|---|
| Service per container | Используем | Один сервис = один Docker-образ (multistage, non-root, CGO_ENABLED=0, -ldflags="-s -w"). |
| Single service instance per host | Используем | В Kubernetes каждый pod — один контейнер сервиса. |
| Multiple services per host | Не используем | |
| Serverless deployment | Не используем | |
| Service deployment platform | Используем | Kubernetes как платформа деплоя; Helm/kustomize для манифестов. |
| Microservice chassis | Планируется | Шаблон-генератор будущего сервис-репо (kazmaps-service-template) со стандартными cmd/server/main.go, middleware stack, Makefile, Dockerfile. |
| Service template | Планируется | То же, что chassis — единый скелет нового сервиса. |
| Sidecar | Не используем |
Cross-cutting concerns¶
Горизонтальные вопросы, которые касаются всех сервисов одинаково: конфигурация, секреты, общий скелет.
| Паттерн | Статус | Как |
|---|---|---|
| Externalized configuration | Используем | Вся конфигурация через env; секреты — через SOPS / Vault, в образ не попадают. Загрузка — internal/config/ + валидация на старте (fail-fast, если обязательное поле пустое). |
| Microservice chassis / service template | Планируется | См. Deployment. |
.env.example с placeholder'ами коммитится в сервис-репо; реальный .env —
в .gitignore. Продовые секреты в контейнер попадают как env-переменные из
Kubernetes Secret'ов, заполняемых из Vault.
Communication style¶
Правило по умолчанию: асинхронно через события. HTTP используется только тогда, когда абоненту нужен ответ внутри одного запроса пользователя.
| Паттерн | Статус | Как |
|---|---|---|
| Messaging | Используем | Kafka + Watermill. Publisher/Subscriber, Router, DLQ — всё через watermill. Напрямую sarama/kafka-go не используем. Сериализация — JSON. |
| Remote Procedure Invocation — HTTP REST | Используем | Для internal endpoint'ов между сервисами (/internal/*). Chi-router на сервере, стандартный net/http на клиенте. |
| Remote Procedure Invocation — gRPC | Не используем | |
| Domain-specific protocol | Не используем | |
| Idempotent Consumer | Используем | Watermill Deduplicator middleware (Redis SETNX + TTL по Message.UUID/Event-Id) плюс идемпотентность на БД-уровне (unique constraint / upsert). |
Имена Kafka-топиков — kazmaps.<service>.<entity>.<action>. Envelope
каждого сообщения содержит Event-Type, Schema-Version, Correlation-Id,
Source-Service, Published-At, traceparent. Подробности — в
conventions/events.md.
External API¶
Внешний трафик всегда приходит через API Gateway. Напрямую до сервиса с интернета ходить нельзя — порт сервиса не выставляется наружу.
| Паттерн | Статус | Как |
|---|---|---|
| API Gateway | Используем | Traefik на edge: TLS termination, routing по host/path, rate-limit, JWT verification перед проксированием в сервис. |
| Backend for Frontend | Не используем | Один общий REST API для web и mobile; разведение нагрузок делаем на уровне endpoint'ов, не отдельным BFF-слоем. |
Сервисы различают два типа endpoint'ов: /v1/* — публичные (идут через
gateway), /internal/* — только для других сервисов (блокируются на gateway,
защищены middleware InternalToken).
Service discovery¶
| Паттерн | Статус | Как |
|---|---|---|
| Client-side discovery | Не используем | |
| Server-side discovery | Используем | Kubernetes DNS: сервис доступен по имени <svc>.<ns>.svc.cluster.local. Клиент ничего не знает про instance'ы — маршрутизацией занимается kube-proxy. |
| Service registry | Не используем отдельный | Роль registry играет Kubernetes API / etcd. |
| Self-registration | Не используем | |
| 3rd party registration | Не используем |
Reliability¶
Сервис должен уметь корректно переживать падение соседа: не залипать на retry бесконечно, не превращать одну точечную ошибку в каскад.
| Паттерн | Статус | Как |
|---|---|---|
| Circuit breaker | Используем | Для internal HTTP-вызовов — per-pod gobreaker в клиентской обёртке. Подробно — patterns/retry-and-circuit-breaker.md. Для платных внешних провайдеров (SMS, push) — shared state через Redis, чтобы все pod'ы видели один счётчик. |
| Retry | Используем | Watermill Retry middleware на Kafka-handler'ах: 5 попыток, exponential backoff 500ms×2 с jitter. Для HTTP — cenkalti/backoff/v4 с MaxElapsedTime ≤ 5s. Полные правила — patterns/retry-and-circuit-breaker.md. |
| Timeout | Используем | Все I/O-вызовы под context.WithTimeout. HTTP-сервер имеет read/write/idle timeout. БД-пул имеет ConnTimeout. |
| Bulkhead | Используем | Ограничение concurrency в upload pipeline: семафор на параллельную обработку chunks, чтобы media-сервис не съел весь pool соединений БД. |
| Fail fast | Используем | На старте сервис валидирует конфиг, пингует БД/Kafka. Redis-ping — только в /readyz сервисов с fail-closed-операциями (см. how-to/handle-redis-outage.md). Что-то обязательное не отвечает — os.Exit(1). |
| Graceful degradation | Используем | Cache — fail-open при падении Redis (continue to DB). Rate-limit — fail-open для default-endpoint'ов, fail-closed для критичных. Stale cache как fallback при открытом CB. Детали — how-to/handle-redis-outage.md. |
Security¶
| Паттерн | Статус | Как |
|---|---|---|
| Access token | Используем | JWT. На edge Traefik проверяет подпись и прокидывает user-id в подписанных internal-заголовках в сервис. Внутри сервиса middleware GatewayAuth читает их и кладёт user-id/role в context.Value. |
Сервис не проверяет JWT сам для каждого запроса — доверяет подписанным
заголовкам от gateway. Прямой запрос в сервис, минуя gateway, не пройдёт
сетевую политику кластера. Internal endpoint'ы дополнительно защищены
статическим токеном (InternalToken middleware).
Observability¶
Три основных сигнала — логи, метрики, трейсы — идут из каждого сервиса и
связываются по correlation_id / trace_id. Чтобы отладить запрос,
который прошёл через 3 сервиса, достаточно одного id.
| Паттерн | Статус | Как |
|---|---|---|
| Health check API | Используем | Каждый сервис выставляет /healthz (liveness — без проверки зависимостей) и /readyz (readiness — проверяет БД, Redis, Kafka). |
| Log aggregation | Используем | log/slog с JSON handler в stdout. Сбор — Loki, запросы — через Grafana. Все логи структурированные, request-id/correlation-id во всех записях запроса. |
| Distributed tracing | Используем | OpenTelemetry SDK, экспорт в Tempo. W3C trace context пробрасывается в Kafka metadata (traceparent) и в HTTP заголовки. |
| Exception tracking | Планируется | Единый exception tracker (Sentry/эквивалент) для группировки ошибок из prod — подключается отдельно от логов. |
| Application metrics | Используем | Prometheus endpoint /metrics. Регистрируются counters, histograms, gauges с low-cardinality лейблами. Алерты — на уровне Prometheus Alertmanager. |
| SLO / error budget | Используем | Availability + latency SLI per endpoint, multi-window burn-rate alerts (fast 14.4× / slow 6×). Formulas и шаблоны — conventions/slo-and-budget.md. |
| Audit logging | Используем | Каждый сервис имеет таблицу audit_log в своей схеме; пишется в бизнес-транзакции рядом с основной записью. Отдельный retention — 180 дней обычных событий, 1 год security-событий (см. conventions/data-retention.md). |
Data management¶
Каждый сервис — единственный владелец своей БД. Другие сервисы видят данные только через API или через события. Никаких совместных записей в одну таблицу двумя сервисами.
| Паттерн | Статус | Как |
|---|---|---|
| Database per service | Используем | У каждого сервиса своя схема в Postgres. Cross-schema JOIN между сервисами — запрещены. |
| Shared database | Не используем | Запрещено. Внешний ID хранится как BIGINT без REFERENCES. |
| Event sourcing | Не используем | Store-of-truth — обычные реляционные таблицы. События производные, а не первичный журнал. |
| CQRS | Используем частично | Через watermill/components/cqrs — подключаем, когда в сервисе становится >3 команд/событий на одну доменную модель и inline-switch становится каша. Пока большинство сервисов обходятся прямыми handler'ами. |
| Saga | Не используем | Планируется к подключению, когда появятся cross-service операции с откатом. Пока таких операций нет. |
| API composition | Используем | Для list-view, которые требуют данных из нескольких сервисов (например, «отзыв + автор + аватар»): фронт/gateway параллельно запрашивает нужные сервисы и склеивает ответы. Никакой cross-service join в БД. |
| CQRS read model | Не используем | Для list-view используем API composition, не отдельную read-model-БД. |
| Transactional outbox | Используем | Таблица outbox в схеме сервиса, запись в outbox идёт в той же транзакции, что и бизнес-операция. Публикация — через watermill-sql + components/forwarder. Retention acked-строк — 7 дней (см. conventions/data-retention.md). |
| Polling publisher | Не используем | Роль publisher'а выполняет components/forwarder. |
| Data retention | Используем | Soft-delete (deleted_at) для бизнес-сущностей, TTL + hard-delete через k8s CronJob для технических таблиц (outbox, sessions, audit). Детали — conventions/data-retention.md. |
| Transaction log tailing (CDC) | Не используем | Debezium/аналоги не подключены. |
| Domain event | Используем | Первичный способ сигнализации между сервисами. Имя события — факт в прошедшем времени (review.created, photo.uploaded). |
| Event-driven architecture | Используем | Все cross-service эффекты (уведомления, обновление денормализованных счётчиков и т.п.) — реакция на события, а не прямой RPC-вызов. |
Testing¶
Пирамида тестов: много unit-тестов service-слоя, умеренно integration-тестов
через testcontainers (реальные Postgres и Kafka), единичные end-to-end
прогонки. Unit Watermill-handler'ов — через gochannel (in-memory pub-sub).
| Паттерн | Статус | Как |
|---|---|---|
| Service component test | Планируется | Тестирование сервиса целиком, поднятого in-process с in-memory зависимостями. Пока каждый сервис покрыт unit-тестами + integration через testcontainers. |
| Consumer-driven contract test | Планируется | Pact или Schemathesis для проверки, что producer не ломает консьюмеров (актуально для event payload и internal REST). |
| Consumer-side contract test | Планируется | Параллельно с CDC — валидация, что консьюмер умеет читать все версии схем producer'а. |
UI patterns¶
Не применимо — handbook описывает backend. UI-паттерны с microservices.io здесь не рассматриваются.
Что мы ЯВНО не делаем¶
Список вещей, которых у нас нет — специально, чтобы новички, пришедшие из других стеков, не искали их в коде:
- Shared database между сервисами. У каждого своя схема, cross-schema
JOIN запрещены. Внешний ID хранится как
BIGINTбезREFERENCES— FK через границу сервиса не делаются. - Event sourcing как store-of-truth. События — производные от состояния в реляционных таблицах, а не первичный журнал. Восстановление состояния из Kafka не поддерживается и не гарантируется.
- gRPC для internal communication. Только HTTP REST + Kafka.
.proto-файлов и codegen-пайплайнов в проекте нет. - Service mesh (Istio, Linkerd). Маршрутизация — Kubernetes + Traefik, наблюдаемость — OpenTelemetry-SDK напрямую из кода, retry и timeout — в клиентской библиотеке сервиса.
- BFF слой. Один REST API обслуживает и web, и mobile. Разница в потребностях решается параметрами endpoint'а, а не отдельным сервисом на каждую клиентскую платформу.
- Monolith. Никаких «сложим пока в один сервис, потом разделим». Каждая новая capability = новый сервис-репо с начала.
- Client-side service discovery. Клиенты ходят по DNS-именам сервисов, balancing — Kubernetes kube-proxy. Никаких Consul/Eureka-клиентов в коде.
- 2PC (two-phase commit) / distributed transactions. Для cross-service согласованности будем подключать Saga, когда появится такая задача. Пока cross-service write-операций с откатом нет — все изменения локальны в пределах одного сервиса и публикуются через outbox.
- ORM. Работа с Postgres — напрямую через
pgx/v5+pgxpool. Никакогоgorm,ent,sqlxили собственной обёртки, скрывающей SQL. - Бизнес-логика в SQL (триггеры, stored procedures, БД-функции с
бизнес-правилами). Всё бизнес-правило — в Go, в
internal/service/. - Kafka напрямую из handler'а HTTP. Публикация события — только через
таблицу
outboxвнутри той же транзакции, что и основная запись. - Sync request-reply через Kafka. Для RPC используем HTTP internal endpoint, не round-trip через топики.
Связанные разделы handbook¶
Базовые conventions:
conventions/project-layout.md— каноническая структура одного сервис-репо.conventions/events.md— Watermill, Kafka, outbox, envelope, consumer middleware stack.conventions/db-pgx.md— pgx, миграции, advisory lock, expand-contract.conventions/http-api.md— chi, middleware, формат ошибок,/v1/*vs/internal/*.conventions/logging.md—log/slog, маскирование PII, correlation-id.conventions/security.md— секреты, валидация входов, internal-токены, rate-limit.conventions/observability.md— три сигнала, OpenTelemetry, alerting.conventions/slo-and-budget.md— SLI/SLO, error budget, burn-rate alerts.conventions/caching.md— Redis, TTL, stampede protection.conventions/data-retention.md— soft-delete, TTL, cleanup, archival.
Deep-dive паттерны:
patterns/outbox.md— Transactional Outbox.patterns/idempotent-consumer.md— защита от дублей at-least-once.patterns/cqrs.md—watermill/components/cqrs.patterns/api-composition.md— batch-endpoint'ы, parallel fetch, partial failure.patterns/retry-and-circuit-breaker.md— retry с jitter, CB на HTTP-клиентах, fallback.
Рецепты и runbook'и:
how-to/handle-redis-outage.md— fail-open/closed per operation.how-to/rollback-migration.md— откат миграции через forward-fix.how-to/onboard-new-dependency.md— vetting + govulncheck + SBOM.troubleshooting/— runbook'и по consumer stuck, rebalance-loop, slow query, memory-leak, Redis outage.
Ссылки¶
Публичные материалы, к которым отсылает этот документ:
- microservices.io/patterns — каталог паттернов Chris Richardson'а. По его категориям структурирован этот документ.
- microservices.io/book — книга Chris Richardson'а «Microservices Patterns».
- martinfowler.com/microservices — обзор Martin Fowler по микросервисам.