Python FastAPI Dependency Injection vs Global Singletons for Database Sessions
Quick answer
In FastAPI, create database sessions per request through a dependency, and keep only the engine or connection pool as a shared object. A single global session...
In FastAPI, create database sessions per request through a dependency, and keep only the engine or connection pool as a shared object. A single global session shared across requests is unsafe, because sessions are not designed for concurrent use. Dependency injection gives each request an isolated session with a clear lifecycle, and it makes testing much easier.
Why Is a Global Database Session a Problem?
A session tracks state: pending changes, loaded objects, and the current transaction. When several requests share one session, they share that state too. One request can commit or roll back another request's half-finished work, and concurrent access can corrupt the session's internal bookkeeping.
The symptoms are subtle and intermittent. Under light testing everything works, then under real concurrency you see stale data, unexpected rollbacks, or errors about a transaction being in an invalid state. These are hard to reproduce, which is why it is better to avoid the design entirely.
The distinction to keep in mind is between the engine and the session. The engine and its connection pool are meant to be created once and shared across the process. Sessions are cheap, short-lived units of work that should not be shared between requests or between threads and tasks.
How Does Dependency Injection Create Sessions Safely?
FastAPI dependencies can be generator functions. Code before the yield runs when the request starts, and code after it runs when the request finishes, which is the right place to close the session and release the connection.
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def read_user(user_id: int, db: Session = Depends(get_db)):
return db.get(User, user_id)
Each request gets its own session, and cleanup is guaranteed even when the handler raises an exception. The same pattern works with async engines and async sessions, but check the SQLAlchemy and FastAPI documentation for the exact API in your versions, since details have changed over time.
Dependencies also compose. A repository or service can depend on the session dependency, and a handler can depend on the service. FastAPI resolves the chain once per request and reuses the same session within it, so every layer in a single request works against the same unit of work without any global state.
How Should You Handle Transactions and Commits?
Decide where the transaction boundary lives. A common and clear choice is to commit explicitly in the service layer or handler when a unit of work succeeds, and let the dependency roll back on failure. Hiding commits in the dependency can surprise developers, because a read-only endpoint might commit unintentionally.
- Roll back on exceptions so the connection returns to the pool in a clean state.
- Keep sessions short-lived and avoid holding them across slow external calls.
- Do not pass ORM objects to background tasks after the session closes. Pass ids and open a fresh session instead.
- Use a separate session per background task, because the request session is closed when the response finishes.
Be aware of how dependency cleanup timing interacts with streaming responses and background tasks in your FastAPI version. The documentation describes when code after yield runs, and it has changed across releases, so verify before relying on a particular order.
Why Does Dependency Injection Help Testing?
FastAPI lets you override dependencies, so tests can substitute a session bound to a test database or an in-memory one without changing application code. With a global singleton, you would need patching tricks that are fragile and hard to maintain.
def override_get_db():
db = TestingSessionLocal()
try:
yield db
finally:
db.close()
app.dependency_overrides[get_db] = override_get_db
This also improves isolation. Each test can use its own transaction and roll it back at the end, keeping tests independent and fast. Teams that adopt this pattern usually find that their test suite becomes more reliable, because no test can leak state into another through a shared global.
Frequently Asked Questions
Should the database engine be a global?
Yes, creating it once per process is normal and recommended. The engine manages the connection pool. Only the session needs to be per request.
Is it safe to share a session between threads?
No. Sessions are not designed for concurrent use. Create one per unit of work and per thread or task.
Where should I commit?
Commit explicitly at the end of a successful unit of work, usually in the service layer. Avoid committing automatically inside the dependency.
Can I use dependency injection outside FastAPI routes?
Not directly. For scripts and workers, create sessions with a context manager, using the same session factory as your application.
Summary
Share the engine, never the session. Provide sessions through a generator dependency, define clear transaction boundaries, and use dependency overrides for tests. Check the FastAPI and SQLAlchemy documentation for details that differ between versions.