gitaspen docs

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

  • domainsdtos, 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: фабрика эндпойнтов

Эндпойнты объявляются единой обёрткой, которая делает то, что иначе дублировалось бы в каждом обработчике:

  1. Конверт ответа

    Оборачивает результат в { status, data, details? }. Обработчик возвращает чистые данные.

  2. Каталог ошибок

    Стандартные коды (валидация, доступ, конфликт, лимит, внутренняя) с примерами подключаются автоматически; кастомные — мержатся поверх.

  3. Декларация форм ответа

    для авто-документации.

Поверх — общая обёртка ошибок: ожидаемые (доменные) поднимаются типовыми «подъёмниками» с машинным error_code; неожиданные перехватываются, логируются со стеком, наружу отдаются обезличенным кодом.

API: схемы и DI

Схемы. Базовые RequestSchema / ResponseSchema из shared. Примеры — рядом со схемой. Типизация — доменными enum. Бизнес-логики в схемах нет.

DI. Без глобального контейнера, явная композиция. Две роли имён:

  • get_* — в API-слое (точка входа DI обработчика);
  • build_* — в core и infrastructure.

Каскад: get_<domain>_solutionbuild_<domain>_solutionbuild_<domain>_servicebuild_<domain>_repo.

API: аутентификация и ID

Доступ объявляется требованием на входе: «нужен валидный пользователь» или «нужна роль X». Механизм проверяет токен, при провале — стандартный отказ, в обработчик передаёт уже проверенного пользователя. Авторизация единая для всех клиентов; шлюз пробрасывает токен, проверяет сам сервис.

ID ресурса: действие над конкретным ресурсом — в пути; массовые операции и фильтрация — в теле.

Три модели данных

МодельГдеРоль
DTOcore/domains/dtosобмен между слоями (core ↔ infrastructure)
Модель храненияinfrastructure/db/modelsтаблицы/документы в БД
Схема APIapi/.../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
Фабрики DIget_* в api, build_* в core и infrastructure

Идемпотентность

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

Повтор — не редкость, а норма распределённой системы: клиент нажал дважды, сеть оборвалась после выполнения, но до ответа, очередь доставила сообщение повторно.

Как обеспечивается:

  • ключ идемпотентности приходит от вызывающей стороны и хранится вместе с результатом. Повтор с тем же ключом возвращает сохранённый результат, а не выполняет операцию заново;
  • проверка текущего состояния перед изменением: переход выполняется, только если сущность находится в ожидаемом состоянии;
  • ключ сообщения в очереди — потребитель отбрасывает повтор.

Таймауты и повторы

У каждого внешнего вызова — база, кеш, очередь, чужой API — задан таймаут. Вызов без таймаута занимает поток до бесконечности: отказ соседа превращается в отказ этого сервиса.

Таймауты задаются в infrastructure, рядом с подключением, а не в прикладном коде.

Повторы применяются только к операциям, которые безопасно выполнять дважды — то есть к идемпотентным. Между попытками пауза растёт (экспоненциальная задержка), число попыток ограничено: бесконечные повторы превращают кратковременный сбой соседа в лавину запросов.

Транзакции

Границей транзакции владеет тот слой, который знает бизнес-операцию целиком, — сервис или сценарий, а не репозиторий. Репозиторий выполняет отдельные запросы и не решает, где фиксировать.

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

Версионирование API

Версионируется только внешний API — тот, которым пользуются другие. Внутренние ручки версии не требуют: их потребитель обновляется вместе с сервисом.

Новая версия добавляется рядом со старой, старая продолжает работать до тех пор, пока не отключены все её потребители. Ломающее изменение внутри текущей версии недопустимо.

Фоновые задачи

Долгая работа не выполняется внутри обработки запроса: он должен ответить сразу. Задача ставится в очередь или запускается фоном, а клиент получает идентификатор и отслеживает состояние отдельной ручкой.

Фоновая задача обязана быть готовой к повторному выполнению: процесс может быть остановлен посередине.

Инварианты

  1. api не ходит в БД и к клиентам — только в core через DI (включая internal).
  2. core не знает о транспорте (нет импортов транспортного фреймворка).
  3. Сервисы одной зоны не зовут другую напрямую — межзональное только через solutions.
  4. shared ни от кого не зависит.
  5. infrastructure не бросает транспортных ошибок; из вышележащего знает только DTO и доменные ошибки.
  6. Всё наружу — в конверте (success / error), голых моделей не возвращаем.
  7. Ошибки — через общий канал, не транспортными исключениями по месту.
  8. Доступ — проверкой роли, не группами роутеров.
  9. DI-нейминг: get_* в api, build_* в core и infrastructure.

Добавление домена

  1. Модель хранения + миграция.
  2. Репозиторий в infrastructure/db/repositories/<domain> + build_<domain>_repo.
  3. DTO и enum в core/domains.
  4. Сервис в core/services/<domain> + build_<domain>_service.
  5. Solution в core/solutions/<domain> — если сценарий многосервисный.
  6. API в api/.../routers/external/front/<domain> (или back/internal): handlers, schemas, dependencies; подключить в агрегатор.
  7. Mapper — при необходимости.

Наш стек

Стек реализации, на которой эта архитектура обкатана:

РольВыбор
ЯзыкPython (async)
RESTFastAPI + Uvicorn
СобытияKafka (через свою обёртку над клиентом)
БД + ORMPostgreSQL + SQLAlchemy 2 (async) + asyncpg
МиграцииAlembic
Схемы и настройкиPydantic 2 + pydantic-settings
Межсервисный HTTPhttpx
КешRedis
Авторизация, криптоPyJWT, cryptography
Загрузкиpython-multipart
Инструкция не помогла?

Откройте исходник документа по ссылке «Предложить правку» — там же видно, что и когда в нём менялось.