Software Engineer's Blog

FastAPI Dependency Injection (DI) Guide

FastAPI Dependency Injection (DI) Guide

FastAPI’s dependency injection system is not just a way to pass objects around. It is a resource management engine built around the request lifecycle.

This guide explains how FastAPI DI actually works, how it differs from frameworks like Spring, and how to design clean, testable architectures using Depends().


1. Core Concept: Depends()

Depends() tells FastAPI to resolve a dependency before executing a path operation and inject the result as a function argument.

How this differs from Spring

  • Spring resolves dependencies at application startup and stores them in an application context.
  • FastAPI resolves dependencies per HTTP request, building a dependency graph dynamically for each request.
from fastapi import Depends

def get_query_param(q: str | None = None):
    return q

@app.get("/items")
async def read_items(query: str = Depends(get_query_param)):
    return {"query": query}

2. Dependency Chains

One of FastAPI’s most powerful features is that dependencies can depend on other dependencies. This allows you to express complex architectures in a clean, layered way.

Example: Clean Architecture with Dependency Chains

The following example reflects a common and practical rule:

  • Heavy resources → singleton-like, reused
  • Light objects → created per request
# 1. Infrastructure layer (Heavy – singleton)
async def get_vertex_ai_client() -> VertexAIClient:
    # Module-level singleton access
    return await VertexAIClient.get_instance()

# 2. Repository layer (Light – per request)
def get_user_repository() -> UserRepository:
    return UserRepository()

# 3. Service layer (dependency chain)
async def get_user_service(
    repo: UserRepository = Depends(get_user_repository),
    ai_client: VertexAIClient = Depends(get_vertex_ai_client)
) -> UserService:
    return UserService(repo, ai_client)

# 4. API layer
@app.post("/users/analyze")
async def analyze_user(service: UserService = Depends(get_user_service)):
    return await service.perform_analysis()

FastAPI resolves this chain from the bottom up, injecting fully constructed dependencies into higher layers.


3. Request-Scoped Caching

Within a single HTTP request, FastAPI caches dependency results.

  • If multiple services depend on the same function (e.g., get_db_session), it is executed only once.
  • The same instance is reused throughout the request.
  • This prevents unnecessary database connections and repeated computations.

You can disable this behavior when needed:

Depends(get_db_session, use_cache=False)

4. Resource Lifecycle Management with yield

FastAPI supports setup and teardown logic in dependencies using yield, similar to Java’s try-with-resources.

PhaseWhen it runsJava analogy
Before yieldBefore endpoint logicConnection conn = dataSource.getConnection()
Yielded valueInjected into endpointreturn conn
After yieldAfter response is sentconn.close() (finally)
async def get_db():
    db = DatabaseSession()
    try:
        yield db
    finally:
        await db.close()

This guarantees cleanup even if an exception occurs.


5. Practical Best Practices

Interface-based injection

While Python supports duck typing, defining clear interfaces using Protocol or ABC makes large codebases easier to reason about and refactor.

Dependency overrides for testing

FastAPI makes it trivial to replace real dependencies with mocks in tests.

app.dependency_overrides[get_vertex_ai_client] = lambda: MockAIClient()

This enables fast, isolated, and deterministic tests.

Control dependency depth

Deep dependency chains can become hard to debug. A common and effective guideline is:

API → Service → Repository / Client

Three layers are usually enough.


Summary

  1. Heavy resources (clients): async, reused, typically singleton-like
  2. Light objects (services/repositories): created per request
  3. Cleanup logic: use yield for guaranteed teardown
  4. Architecture: express dependencies as a directed acyclic graph (DAG)

Final Thoughts

FastAPI DI is not just a convenience feature.
It is a first-class architectural tool.

When used correctly, it enables:

  • Clear separation of concerns
  • Predictable resource lifecycles
  • Highly testable application design