Насколько сильно стоит полагаться на абстракции при проектировании и реализации репозитория?

Работая над проектом, дошёл до описания пула БД и задумался, имеет ли смысл полностью абстрагироваться от конкретной реализации базы данных. Что я имею в виду:
  • 23 июня 2026 г. 13:00
Ответы на вопрос 2
Короткий ответ
- Абстрагируйте бизнес‑операции (репозиторий как набор доменных методов), но не пытайтесь полностью скрыть БД под универсальным слоем, который «работает с любой базой» и гасит её особенности.  
- Пул подключений — инфраструктурная деталь: её конфигурация, жизненный цикл и поведение (таймауты, retry, max open) лучше держать в слое инфраструктуры и не делать предметом доменных абстракций. Экспортируйте только то, что реально нужно: контекст транзакции, интерфейс получения сессии/канала и семантику ошибок.

Почему это важно (плюсы/минусы полной абстракции)
- Плюсы абстракции:
  - Изоляция доменной логики от реализации БД → легче тестировать, менять реализацию.
  - Чистые интерфейсы для контрактов (инверсия зависимостей).
  - Возможность подменять реализацию в интеграционном тесте.
- Минусы «глубокой» абстракции:
  - Ликвидность (leaky abstractions) — низкоуровневые свойства БД всё равно просачиваются, и вы получаете сложный универсальный слой.
  - Потеря производительности и возможностей, если вы вынуждены работать с НОСД (наименьшим общим делителем) функций разных СУБД.
  - Избыточная сложность и поддержка множества адаптеров редко окупаются, если реально не нужна мульти‑СУБД поддержка.

Практические рекомендации
1. Абстрагируйте дело (что), а не способ (как).
   - Репозиторий должен предоставлять методы, нужные домену: getById, save, findBy..., списки/агрегаты. Не пытайтесь сделать generic API, которое генерирует SQL за вас.
2. Слой инфраструктуры реализует детали:
   - Конфигурация пула, драйвер, миграции, репликация — всё в infra. Доменный код не должен знать про maxConnections.
3. Экспортируйте минимально необходимую семантику:
   - Контекст транзакции, rollback/commit, таймауты, опционально retry. Например: withTransaction(ctx, func(tx Tx) error).
4. Не прячьте критические фичи СУБД, если они нужны:
   - JSONB, полнотекстовый поиск, window functions, upsert — если бизнес их использует, отражайте это в API.
5. Для тестирования:
   - Юнит‑тесты: мокируйте репозитории (интерфейсы). Интеграционные — прогоняйте на реальной СУБД (или на контейнере), т.к. in‑memory базы часто ведут себя иначе.
6. Если вы библиотека/фреймворк:
   - В таком случае абстракция нужна: библиотека должна поддерживать несколько реализаций через интерфейсы и адаптеры.
7. Если вы приложение:
   - Чаще всего проще выбрать одну СУБД и использовать её фичи; делать слои адаптации имеет смысл только при реальной необходимости смены движка.

Конкретная структура (пример архитектуры)
- domain/
  - model, service, repository interface (например UserRepository { Get(id) Save(user) FindByEmail(email) })
- infra/
  - db/pool — конфигурация и инициализация пула (параметры в конфигурации)
  - db/postgres_user_repository — реализация UserRepository, использующая SQL/ORM и пул
  - migrations, healthchecks, metrics
- app/
  - собирает зависимости (внедряет infra реализацию в доменные сервисы)

Про пул БД
- Сделайте пул единственным местом инициализации (singleton/di). Коды в приложении получают либо:
  - явный объект Session/Connection/Tx от пула, либо
  - контекст (ctx) в котором middleware/handler инжектирует сессию.
- Не пытайтесь «абстрагировать пул» в общем виде, чтобы он поддерживал любые драйверы. Лучше иметь адаптеры: PooledDB → PostgresPool / MySQLPool, и фабрику конфигурации.
- Экспонируйте поведение, которое потребуется приложению: BeginTransaction(ctx, opts), GetConn(ctx), Close() и параметры конфигурирования.

Поведение ошибок и семантика
- Не переворачивайте все ошибки в абстрактные. Часто нужно различать сетевую/таймаут/конфликт (unique violation) — переводите только те ошибки, которые важны домену (например, DuplicateEntityError), остальные оставляйте как транспортные или оборачивайте с сохранением кода.

Когда стоит сделать полную абстракцию
- Нужна реальная поддержка нескольких СУБД или ваша команда/продукт будет мигрировать между ними.
- Вы пишете публичную библиотеку/фреймворк.
- У вас очень жесткие требования к тестируемости, и вам важна возможность лёгкой подмены реализации во всех сценариях (но обычно достаточно моков на уровне репозитория).

Когда не стоит
- Вы разрабатываете приложение с одной СУБД, хотите её фичи и максимальную производительность.
- Абстракция будет стоить больше усилий, чем потенциальный выигрыш.

Короткий чек‑лист перед реализацией
- Что именно вы хотите скрыть и зачем? (производительность, тестирование, геморрой при смене СУБД)
- Нужны ли вам функции конкретной СУБД? Если да — не абстрагируйте их.
- Как будет выглядеть тестирование? Нужно ли реальное поведение транзакций/DDL?
- Будете ли вы поддерживать несколько реализаций «вечно»? Если нет — упрощайте.

Итог
Абстрагируйте интерфейсы на уровне домена и держите пул/конфигурацию/миграции в инфраструктурном слое. Не создавайте универсальную «сверх‑абстракцию», которая пытается скрыть всю семантику разных СУБД — это дорого и часто ненадёжно. Делайте адаптеры и ясные границы слоёв: домен пользуется понятным API, инфраструктура — конкретной реализацией.

Если хочешь, могу:
- привести примеры интерфейсов и адаптеров на твоём стеке (Go/Java/Node/Python), или
- помочь спроектировать конкретный API пула/транзакций для твоего проекта.
Интерфейс с pgx-типами — это не абстракция от БД, это просто обёртка. Реально нужно абстрагироваться выше: пусть репозиторий возвращает доменные структуры ( <code>GetUser(ctx, id) (User, error)</code> ), тогда pgx остаётся скрытым внутри. Делать DB-агностичный пул «на всякий случай» это YAGNI, смысл только если есть реальная вторая реализация.
Похожие вопросы