Архитектура сервисного слоя в FastAPI с SQLAlchemy async

    Проектирование сервисного слоя в FastAPI-приложении с асинхронной SQLAlchemy - важный этап, влияющий на тестируемость, производительность и сопровождаемость кода. Главные вопросы: где хранить сессию базы данных и как управлять жизненным циклом сервиса - синглтон или per-request. Рассмотрим оба подхода и определим лучшие практики.

    Где хранить сессию: конструктор или аргумент метода?

    Первый спорный момент - передача сессии через конструктор или как аргумент каждого метода.

    Вариант А: сессия в конструкторе

    Класс принимает сессию при создании:

    class MealService:
    def __init__(self, session: AsyncSession) -> None:
    self.session = session
    async def get(self, id: int) -> Meal: ...

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

    Вариант Б: сессия как аргумент каждого метода

    Каждый метод явно принимает сессию:

    class MealService:
    async def get(self, session: AsyncSession, id: int) -> Meal: ...

    Плюсы: гибкость - один сервис может работать с разными сессиями. Удобно для unit-тестирования. Минусы: дублирование кода в сигнатурах.

    Рекомендация: для большинства проектов лучше передавать сессию в каждый метод. Это делает сервис независимым от контекста и упрощает тестирование с моками. Если же сервис всегда работает в рамках одной транзакции (например, use-case), можно использовать конструктор.

    Синглтон или per-request?

    Второй вопрос - как создавать экземпляры сервисов: один на всё приложение или новый на каждый запрос.

    Вариант А: per-request через Depends

    Сервис создаётся в зависимости FastAPI:

    def get_meal_service(session: AsyncSession = Depends(get_session)) -> MealService:
    return MealService(session)

    Плюсы: изоляция запросов, легко внедрять зависимости. Минусы: небольшой оверхед на создание объектов.

    Вариант Б: синглтон

    Один экземпляр на всё приложение:

    meal_service = MealService()
    # в роутере
    async def get_meal(id: int, session: AsyncSession = Depends(get_session)):
    return await meal_service.get(session, id)

    Плюсы: минимальное потребление памяти. Минусы: если сессия хранится в конструкторе - проблемы с многопоточностью; сложнее тестировать.

    Рекомендация: используйте per-request. FastAPI спроектирован для такого подхода - он обеспечивает чистую архитектуру, легкость тестирования и масштабирования. Синглтон оправдан только для stateless-сервисов (например, утилиты, хэлперы), которые не хранят состояние.

    Лучшие практики для сервисного слоя

    • Инверсия зависимостей: сервисы не должны создавать сессии сами - получайте их через Depends.
    • Тестирование: используйте аргументы методов для лёгкой подмены сессии моками.
    • Транзакции: управляйте сессией на уровне роутера или middleware, а не внутри сервиса.
    • Разделение ответственности: сервис содержит бизнес-логику, сессия - только доступ к данным.

    Когда один вариант предпочтительнее другого?

    Выбор зависит от контекста:

    • Для микросервисов с простой логикой: сессия в конструкторе + per-request - быстро и понятно.
    • Для сложных доменов с множеством сервисов: сессия как аргумент + per-request - гибкость и тестируемость.
    • Для stateless-утилит: синглтон без сессии - оптимально.
    • Для high-load проектов: per-request с пулом сессий - лучший баланс производительности и надёжности.

    Часто задаваемые вопросы