Архитектура сервисного слоя в 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 с пулом сессий - лучший баланс производительности и надёжности.