Learn / Python and FastAPI Backend Engineering
Repository and Unit of Work
Own a subscription billing write flow with product-language repositories, one explicit unit-of-work transaction, rollback proof, and routes that never reach through ORM details.
Course: Python and FastAPI Backend Engineering. Level: Intermediate. Topic: Backend craft.
Stage: intermediate - Intermediate data ownership - Repositories, transactions, and unit-of-work ownership. Move persistence behind product-language boundaries and make transaction ownership reviewable.
Outcomes
- Design repositories around product questions instead of raw SQLAlchemy mechanics.
- Place commit and rollback ownership at a unit-of-work boundary for a complete write flow.
- Prove failure safety with tests that catch partial writes and lazy ORM leaks.
Concepts
- repository port
- unit-of-work boundary
- rollback proof
- ORM leak review
Concept flow
Show how a route command becomes one committed business transaction, or rolls back without partial billing state.
- Route command
- Billing service
- Unit of Work
- Customer repository
- Invoice repository
- Commit or rollback
Session flow
- Name persistence by product questions (concept, 12 min) — Design repository methods around billing decisions: load a customer for billing, check idempotency, stage an invoice.
- Repository names should survive query rewrites.
- Routes should not know joins, eager-loading strategy, or transaction state.
- Commit once at the unit-of-work boundary (walkthrough, 25 min) — Implement a subscription write where idempotency, aggregate changes, invoice creation, and commit happen as one action.
- A service can coordinate repositories without owning session plumbing.
- Commit belongs after all business invariants have been checked and staged.
- Prove rollback and ORM isolation (exercise, 15 min) — Write failure tests that prove no partial invoice survives and no route depends on lazy ORM access.
- Rollback tests protect the business promise, not only database mechanics.
- DTO mapping at the boundary prevents closed-session surprises in production.
Code example
Python in app/orders/service.py.
from dataclasses import dataclass
from typing import Protocol
from uuid import UUID
@dataclass(frozen=True)
class StartSubscription:
customer_id: UUID
plan_id: UUID
idempotency_key: str
class CustomerRepository(Protocol):
async def get_for_billing(self, customer_id: UUID) -> Customer: ...
class InvoiceRepository(Protocol):
async def add(self, invoice: Invoice) -> None: ...
async def exists_for_key(self, idempotency_key: str) -> bool: ...
class UnitOfWork(Protocol):
customers: CustomerRepository
invoices: InvoiceRepository
async def __aenter__(self) -> "UnitOfWork": ...
async def __aexit__(self, exc_type, exc, tb) -> None: ...
async def commit(self) -> None: ...
async def rollback(self) -> None: ...
async def start_subscription(uow: UnitOfWork, command: StartSubscription) -> Invoice:
async with uow:
if await uow.invoices.exists_for_key(command.idempotency_key):
raise DuplicateCommand(command.idempotency_key)
customer = await uow.customers.get_for_billing(command.customer_id)
subscription = customer.start_subscription(command.plan_id)
invoice = Invoice.for_subscription(subscription, command.idempotency_key)
await uow.invoices.add(invoice)
await uow.commit()
return invoice
Walkthrough examples
- Subscription billing write pack — A team is adding paid subscriptions. A failed invoice write must not leave a customer subscribed without a billable invoice.
- File: app/orders/service.py
- File: app/orders/repositories.py
- File: app/orders/unit_of_work.py
- File: tests/test_orders_unit_of_work.py
- File: docs/python-fastapi/repository-and-unit-of-work.md
- Create repository ports for customer billing lookup and invoice idempotency instead of exposing SQLAlchemy queries.
- Implement a UnitOfWork that opens the session, exposes repositories, commits once, and rolls back on exceptions.
- Call start_subscription from the route using a command DTO and map the returned invoice into a response DTO.
- Add tests for duplicate idempotency key, successful commit, rollback after staged invoice, and no ORM relationship access in the route.
Practice
- Rename one persistence method from a SQL-shaped name to a product question like get_for_billing.
- Move commit out of the route and repository so the unit of work owns the complete subscription write.
- Write one test that raises after invoice creation and proves rollback leaves no partial invoice.
Checklist
- Repositories are named in product language and hide SQLAlchemy query shape.
- The service owns the business decision but not the database session mechanics.
- Exactly one unit-of-work boundary commits the subscription write.
- Rollback and idempotency behavior have repeatable test evidence.
- Route responses are built from DTOs, not lazy ORM relationship traversal.
Quiz prompts
- Why should the repository method be named get_for_billing instead of select_customer_with_plan_join? — Repository APIs should communicate business intent; implementation details belong behind the boundary.
- Where should commit happen for a multi-step subscription write? — A unit of work lets the service treat related persistence changes as one business transaction.
- What does a rollback test need to prove? — Rollback evidence is about atomicity: either the whole business action is saved or none of it is.
- Which review smell suggests an ORM leak? — Lazy ORM access outside the transaction boundary creates fragile runtime behavior and unclear ownership.
Flashcards
- What should a repository method name reveal? The product question it answers, such as get_for_billing, not the SQL or ORM mechanics used today.
- What does the unit of work own? The transaction boundary: session lifetime, commit, rollback, and repository coordination for one business action.
- What is rollback proof? A repeatable test showing a failed multi-step write leaves no partial persisted state.
- What is an ORM leak in a route? Route or response code depending on lazy ORM behavior, session state, or relationship traversal instead of DTOs.
- Why check idempotency inside the transaction? The duplicate check and write decision must be protected by the same business boundary to avoid double billing.
Labs
- Ship a rollback-safe subscription write — Refactor a billing endpoint so route code sends a command, service code owns the business action, and the unit of work owns commit/rollback.
- Create CustomerRepository and InvoiceRepository ports named around billing decisions.
- Implement UnitOfWork as an async context manager that rolls back on exceptions and commits only when told.
- Move subscription creation into start_subscription and return a DTO-safe Invoice result to the route.
- Write tests for successful commit, duplicate idempotency key, rollback after staged invoice, and route DTO mapping.
- Test output proves commit happens once for the successful subscription path.
- Rollback test evidence shows no partial invoice remains after an injected failure.
- Review notes include the before/after route showing ORM/session details removed from the HTTP layer.
Challenge
- Review-ready billing transaction boundary (Core) — Turn a raw ORM write flow into a product-language repository and unit-of-work slice with rollback evidence, idempotency handling, and route DTO isolation.
- Repository methods are named after billing decisions instead of SQL shape.
- Only the unit of work commits or rolls back the write transaction.
- Tests prove success, duplicate idempotency, rollback on failure, and route DTO mapping.
- The review note names one ORM leak removed from the route layer.
Canonical lesson URL