BMBP — Base Multi Backend Platform
Архитектура бэкенда: сервиса, API, монолита или бэкенда для приложения, любого размера и для любого транспорта (синхронные запросы, события, реалтайм).
12 минутАрхитектура бэкенда: сервиса, API, монолита или бэкенда для приложения, любого размера и для любого транспорта (синхронные запросы, события, реалтайм).
Слои
api → core → infrastructure → shared| Слой | Что внутри | Может импортировать |
|---|---|---|
api | приём запросов и событий, валидация, маршрутизация, ошибки; тонкий | core, shared |
core | бизнес-логика: services, solutions; доменная модель (dto, enums) | infrastructure, shared |
infrastructure | БД (модели, репозитории), внешние клиенты, события, безопасность | shared |
shared | преобразования моделей, логирование, базовые настройки | ничего |
Правила:
apiтонкий: принять, провалидировать, вызватьcore, отдать ответ. Бизнес-логики и прямых обращений к репозиториям нет.coreне знает о транспорте.infrastructureне содержит бизнес-логики и не бросает транспортных ошибок — только доменные.sharedни от кого не зависит.
Структура каталога
<service>/
├── app/
│ ├── config # настройки + имя/схема сервиса
│ ├── entrypoint # точка входа: поднять менеджеры транспортов
│ ├── api/
│ │ ├── _shared # базовый менеджер транспорта + супервизор
│ │ ├── rest/
│ │ │ ├── manager # менеджер транспорта (startup/run/shutdown)
│ │ │ ├── app-assembly # сборка приложения транспорта
│ │ │ ├── routers/
│ │ │ │ ├── base-router # корневой агрегатор
│ │ │ │ ├── external/
│ │ │ │ │ ├── front/<domain>/ # для фронтендов
│ │ │ │ │ └── back/<domain>/ # для других сервисов
│ │ │ │ └── internal/<domain>/ # отладка/админ, мимо шлюзов
│ │ │ └── _shared/ # фабрика эндпойнтов, декораторы, middleware
│ │ └── events/ # (опц.) события: manager · handlers/<topic>/
│ ├── core/
│ │ ├── domains/
│ │ │ ├── dtos/<domain> # DTO — формы данных
│ │ │ ├── enums/<x> # доменные перечисления
│ │ │ └── mappings # маппинги, константы (опц.)
│ │ ├── services/<domain>/ # service · dependencies — одна доменная зона
│ │ ├── solutions/<domain>/ # solution · dependencies — оркестрация сервисов
│ │ └── exceptions # доменные исключения
│ ├── infrastructure/
│ │ ├── db/
│ │ │ ├── models # модели хранения (общая база со схемой и таймстампами)
│ │ │ ├── repositories/<domain>/ # CRUD · dependencies
│ │ │ ├── connection # подключение, пул
│ │ │ └── lifecycle # инициализация схемы/таблиц, закрытие
│ │ ├── clients/<service>/ # клиенты соседних сервисов · dependencies
│ │ ├── event-clients/<topic>/ # (опц.) исходящие события
│ │ └── security # токены, подписи, лимиты
│ └── shared/
│ ├── mapper # DTO ↔ модель хранения ↔ схема API
│ ├── logging
│ └── settings-base # базы схем запрос/ответ
├── migrations/ # миграции БД (вне app/)
├── tests/
├── <манифест-зависимостей>
├── container # образ
├── compose # сервис + БД
└── .secrets/ # переменные окружения, ключи (вне VCS)Каждый домен (<domain>) — папка с фиксированным набором ролей: handlers, schemas, dependencies в
api; service, dependencies в core/services; repo, dependencies в infrastructure/db/repositories.
Что в каждом слое
api
Транспорт и приём запросов и событий: маршруты, обработчики, middleware, декораторы, модели
ошибок. Делегирует в core.
Маршруты делятся по адресату, а не по продукту. Сервис не знает про конкретные фронты — их агрегируют шлюзы:
| Группа | Префикс | Для кого |
|---|---|---|
external/front | /api/front | фронтенды |
external/back | /api/back | другие сервисы |
internal | /internal | отладка/админ, мимо шлюзов |
Разграничение доступа — проверкой роли в эндпойнте, не группой роутеров.
core
- domains —
dtos,enums,mappings. Доменная модель. - services — одна доменная зона. Работают с DTO и репозиториями. Сервисы разных зон не общаются напрямую — только через solutions.
- solutions — оркестрируют несколько сервисов в сценарий, используют внешних клиентов и события. К репозиториям напрямую не ходят.
- exceptions — доменные исключения.
infrastructure
- db — общая база моделей задаёт схему и таймстампы; модели, репозитории (CRUD), подключение,
инициализация. Репозитории используются
core-сервисами напрямую. - clients — клиенты соседних сервисов.
- event-clients — исходящие события.
- security — токены, подписи, лимиты.
Логики тут только для оптимизации запросов. Транспортных ошибок не бросает.
shared
Преобразования моделей (mapper), логирование, базы схем. Ни от кого не зависит.
API: иерархия маршрутов
Дерево из агрегаторов: домен экспортирует набор маршрутов → уровень-агрегатор подключает домены под своим префиксом → корневой агрегатор собирает уровни.
корень
├── /api → external
│ ├── /api/front → front → /api/front/<domain>
│ └── /api/back → back → /api/back/<domain>
└── /internal → internal → /internal/<domain>Модуль домена: handlers (обработчики), schemas (запрос/ответ + примеры), dependencies (сборка
зависимостей), опционально errors.
API: фабрика эндпойнтов
Эндпойнты объявляются единой обёрткой, которая делает то, что иначе дублировалось бы в каждом обработчике:
-
Конверт ответа
Оборачивает результат в
{ status, data, details? }. Обработчик возвращает чистые данные. -
Каталог ошибок
Стандартные коды (валидация, доступ, конфликт, лимит, внутренняя) с примерами подключаются автоматически; кастомные — мержатся поверх.
-
Декларация форм ответа
для авто-документации.
Поверх — общая обёртка ошибок: ожидаемые (доменные) поднимаются типовыми «подъёмниками» с машинным
error_code; неожиданные перехватываются, логируются со стеком, наружу отдаются обезличенным
кодом.
API: схемы и DI
Схемы. Базовые RequestSchema / ResponseSchema из shared. Примеры — рядом со схемой.
Типизация — доменными enum. Бизнес-логики в схемах нет.
DI. Без глобального контейнера, явная композиция. Две роли имён:
get_*— в API-слое (точка входа DI обработчика);build_*— вcoreиinfrastructure.
Каскад: get_<domain>_solution → build_<domain>_solution → build_<domain>_service →
build_<domain>_repo.
API: аутентификация и ID
Доступ объявляется требованием на входе: «нужен валидный пользователь» или «нужна роль X». Механизм проверяет токен, при провале — стандартный отказ, в обработчик передаёт уже проверенного пользователя. Авторизация единая для всех клиентов; шлюз пробрасывает токен, проверяет сам сервис.
ID ресурса: действие над конкретным ресурсом — в пути; массовые операции и фильтрация — в теле.
Три модели данных
| Модель | Где | Роль |
|---|---|---|
| DTO | core/domains/dtos | обмен между слоями (core ↔ infrastructure) |
| Модель хранения | infrastructure/db/models | таблицы/документы в БД |
| Схема API | api/.../schemas | запрос/ответ на границе |
Преобразования — через shared/mapper. Наружу всё уходит в конверте { status, data, details? } —
это контракт с BMFP.
Жизненный цикл
Каждый транспорт (REST, события, реалтайм) — менеджер с единым контрактом: startup (подготовка
ресурсов) → run (долгоживущая работа) → shutdown (корректное завершение с таймаутом). Над ними
супервизор: поднимает все менеджеры, гасит в обратном порядке по сигналу. Добавить транспорт =
добавить ещё один менеджер в список.
Точка входа:
старт:
настроить логирование
инициализировать БД (схема + таблицы)
[запустить фоновые задачи, если есть]
собрать список менеджеров
отдать супервизору
стоп (всегда):
закрыть БДМинимум для запуска с нуля — сервис + БД в compose. Всё вокруг (приватная сеть, прокси, брокер
событий) — обвязка развёртывания, не часть архитектуры.
Фоновые задачи (если есть) кладут рядом с сервисом домена и запускают после инициализации БД; они идемпотентны и корректно завершаются при остановке.
Изоляция по схемам БД
Каждый сервис получает свою схему БД, имя выводится из имени сервиса. Общая база моделей задаёт эту схему один раз; все таблицы создаются в ней. Это снимает конфликты имён, когда несколько сервисов делят один кластер БД, и даёт безопасный сброс (удаление схемы целиком).
Межсервисное взаимодействие
- Клиенты (
infrastructure/clients) — синхронный запрос-ответ к соседям; адреса в настройках. - События — асинхронная связь (pub/sub): исходящие через event-клиент, входящие —
обработчиками в
api/events. - Шлюзы — реверс-прокси под конкретный фронт, собирают ручки из нужных бэков. Подробно — BMGP.
Именование
| Что | Правило |
|---|---|
| Домен = папка | набор файлов с фиксированными ролями: routers/.../<domain>/{handlers, schemas, dependencies} |
| Агрегатор маршрутов | базовый агрегатор на каждом уровне: routers/base-router, external/front/base-router |
| Классы слоёв | XxxService, XxxSolution, XxxRepo, XxxModel |
| Схемы | <Verb><Noun>Request, <Noun>Response, ...List |
| Менеджер транспорта | XxxManager |
| Фабрики DI | get_* в api, build_* в core и infrastructure |
Идемпотентность
Повторный вызов операции с тем же ключом даёт тот же результат и не создаёт вторую сущность. Требование обязательно там, где повтор дорого стоит: оплата, создание заказа, списание, отправка сообщения.
Повтор — не редкость, а норма распределённой системы: клиент нажал дважды, сеть оборвалась после выполнения, но до ответа, очередь доставила сообщение повторно.
Как обеспечивается:
- ключ идемпотентности приходит от вызывающей стороны и хранится вместе с результатом. Повтор с тем же ключом возвращает сохранённый результат, а не выполняет операцию заново;
- проверка текущего состояния перед изменением: переход выполняется, только если сущность находится в ожидаемом состоянии;
- ключ сообщения в очереди — потребитель отбрасывает повтор.
Таймауты и повторы
У каждого внешнего вызова — база, кеш, очередь, чужой API — задан таймаут. Вызов без таймаута занимает поток до бесконечности: отказ соседа превращается в отказ этого сервиса.
Таймауты задаются в infrastructure, рядом с подключением, а не в прикладном коде.
Повторы применяются только к операциям, которые безопасно выполнять дважды — то есть к идемпотентным. Между попытками пауза растёт (экспоненциальная задержка), число попыток ограничено: бесконечные повторы превращают кратковременный сбой соседа в лавину запросов.
Транзакции
Границей транзакции владеет тот слой, который знает бизнес-операцию целиком, — сервис или сценарий, а не репозиторий. Репозиторий выполняет отдельные запросы и не решает, где фиксировать.
Внутри транзакции не делают внешних вызовов: HTTP-запрос или отправка в очередь внутри открытой транзакции удерживает соединение с базой на время чужого ответа, а при откате отменить уже отправленное невозможно. Порядок такой: зафиксировать изменение в базе, затем публиковать событие.
Версионирование API
Версионируется только внешний API — тот, которым пользуются другие. Внутренние ручки версии не требуют: их потребитель обновляется вместе с сервисом.
Новая версия добавляется рядом со старой, старая продолжает работать до тех пор, пока не отключены все её потребители. Ломающее изменение внутри текущей версии недопустимо.
Фоновые задачи
Долгая работа не выполняется внутри обработки запроса: он должен ответить сразу. Задача ставится в очередь или запускается фоном, а клиент получает идентификатор и отслеживает состояние отдельной ручкой.
Фоновая задача обязана быть готовой к повторному выполнению: процесс может быть остановлен посередине.
Инварианты
apiне ходит в БД и к клиентам — только вcoreчерез DI (включаяinternal).coreне знает о транспорте (нет импортов транспортного фреймворка).- Сервисы одной зоны не зовут другую напрямую — межзональное только через solutions.
sharedни от кого не зависит.infrastructureне бросает транспортных ошибок; из вышележащего знает только DTO и доменные ошибки.- Всё наружу — в конверте (
success/error), голых моделей не возвращаем. - Ошибки — через общий канал, не транспортными исключениями по месту.
- Доступ — проверкой роли, не группами роутеров.
- DI-нейминг:
get_*в api,build_*в core и infrastructure.
Добавление домена
- Модель хранения + миграция.
- Репозиторий в
infrastructure/db/repositories/<domain>+build_<domain>_repo. - DTO и enum в
core/domains. - Сервис в
core/services/<domain>+build_<domain>_service. - Solution в
core/solutions/<domain>— если сценарий многосервисный. - API в
api/.../routers/external/front/<domain>(или back/internal): handlers, schemas, dependencies; подключить в агрегатор. - Mapper — при необходимости.
Наш стек
Стек реализации, на которой эта архитектура обкатана:
| Роль | Выбор |
|---|---|
| Язык | Python (async) |
| REST | FastAPI + Uvicorn |
| События | Kafka (через свою обёртку над клиентом) |
| БД + ORM | PostgreSQL + SQLAlchemy 2 (async) + asyncpg |
| Миграции | Alembic |
| Схемы и настройки | Pydantic 2 + pydantic-settings |
| Межсервисный HTTP | httpx |
| Кеш | Redis |
| Авторизация, крипто | PyJWT, cryptography |
| Загрузки | python-multipart |
Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.