A pragmatic repository design that protects domain operations and transactions without hiding SQLAlchemy behind a generic persistence imitation.
Use repositories for aggregate persistence and test seams; keep query-specific read models direct and let the unit of work own the transaction.
class GenericRepository:
def get(self, model, id): ...
def filter(self, model, **kwargs): ...
def update(self, model, values): ...
def commit(self): self.session.commit()class OrderRepository(Protocol):
def get_for_update(self, order_id: OrderId) -> Order | None: ...
def add(self, order: Order) -> None: ...
class SqlAlchemyUnitOfWork:
orders: SqlAlchemyOrderRepository
def commit(self) -> None:
self.session.commit()Model application needs
A generic repository exposes storage mechanics and encourages application code to assemble arbitrary filters. It adds a vocabulary layer without protecting any domain concept.
A focused repository exposes operations required to load and persist an aggregate consistently. Names such as get_for_update communicate concurrency intent that a generic get method cannot. The interface stays small because the use cases, not the ORM, define it.
Keep transactions above repositories
Repositories that commit independently make multi-step operations impossible to reason about. The application service should use a unit of work that owns one session and makes commit or rollback explicit across all repositories involved.
Flush can obtain database-generated values without ending the transaction. Reserve commit for the boundary that knows the full business operation has succeeded.
Do not force reads through aggregates
Dashboards and search endpoints often need joins, projections and pagination that do not reconstruct a domain aggregate. Use dedicated query functions or read models for these paths.
The production test is replaceability of the boundary, not the ability to switch databases overnight. Integration tests must still exercise real SQLAlchemy mappings, constraints and query behaviour.
Evidence to take into review
- Repository methods speak the language of use cases.
- Repositories never commit behind the caller's back.
- One unit of work owns the session lifecycle.
- Complex reads use explicit projections where appropriate.
- Database integration tests verify mappings and constraints.
Explore all engineering notes.
Use PRODUCTION-7 to connect this concern with the other dimensions of a trustworthy backend.
View all articles Get the checklist