Dependency Injection Pattern
File: app/core/dependencies.py
Status: ✅ Implemented and Working
Purpose: Centralized service registry with singleton pattern
Overview
The dependency injection pattern ensures:
1. Single instances of services (singletons) created at startup
2. Proper dependency wiring (e.g., RetrievalPipeline depends on GPTMiniService)
3. FastAPI integration via Depends() pattern
4. Testability (can mock dependencies in tests)
Service Initialization Order
Services are initialized in dependency order:
# 1. External Clients (no dependencies)
openai_client = OpenAI(api_key=settings.OPENAI_API_KEY)
supabase_service = create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY)
# 2. Base Adapters (depend on clients)
pinecone_adapter = PineconeAdapter(api_key=..., index_name=...)
cache_service = CacheService()
# 3. Core Services (depend on adapters)
embedding_service = EmbeddingService(
openai_client=openai_client,
supabase=supabase_service,
pinecone_adapter=pinecone_adapter
)
gpt_mini_service = GPTMiniService(
openai_client=openai_client,
model=settings.OPENAI_MINI_MODEL
)
# 4. Pipelines (depend on core services)
retrieval_pipeline = RetrievalPipeline(
openai_client=openai_client,
supabase=supabase_service,
pinecone_adapter=pinecone_adapter,
embedding_service=embedding_service,
gpt_mini_service=gpt_mini_service,
cache_service=cache_service
)
# 5. Feature Services (depend on pipelines)
quiz_generator = QuizGeneratorService(
openai_client=openai_client,
retrieval_pipeline=retrieval_pipeline
)
Dependency Graph
graph TD
OAI[OpenAI Client] --> ES[EmbeddingService]
OAI --> GPT[GPTMiniService]
OAI --> QG[QuizGeneratorService]
SB[Supabase Client] --> ES
SB --> WS[WalletReservationService]
SB --> IS[IngestionService]
SB --> SS[ScraperService]
SB --> RP[RetrievalPipeline]
PC[Pinecone Adapter] --> ES
PC --> RP
CS[CacheService] --> RP
ES --> RP
GPT --> RP
RP --> QG
TN[TextNormalizer] --> SS
DS[DeduplicationService] --> SS
QC[QualityChecker] --> SS
Usage in Routers
Pattern
# 1. Import singleton from dependencies
from app.core.dependencies import wallet_service, retrieval_pipeline
# 2. Use in endpoint with FastAPI Depends
@router.post("/ask")
async def ask_question(
request: AskRequest,
user: dict = Depends(get_current_user)
):
# 3. Call service methods
balance = wallet_service.get_balance(user["id"])
# 4. Use service
if balance["token_balance"] < estimated_cost:
raise HTTPException(402, "Insufficient balance")
reservation = wallet_service.reserve(user["id"], estimated_cost)
# ... rest of logic
Working Example (Wallet Router)
File: app/api/routers/wallet.py
from app.core.dependencies import wallet_service, supabase_service
@router.get("/balance", response_model=WalletBalanceResponse)
async def get_balance(user: dict = Depends(get_current_user)):
"""Get wallet balance."""
# Uses singleton wallet_service
res = supabase_service.table("wallet").select("*").eq("user_id", user["id"]).single().execute()
# Count pending reservations
pending_res = supabase_service.table("reservations").select("*", count="exact").eq("user_id", user["id"]).eq("status", "reserved").execute()
return WalletBalanceResponse(
user_id=UUID(user["id"]),
token_balance=res.data["token_balance"],
subscription_tier=res.data["subscription_tier"],
pending_reservations=pending_res.count or 0
)
TODO: Chat Router Integration
File: app/api/routers/chat.py (currently stub)
Needs:
from app.core.dependencies import (
retrieval_pipeline,
wallet_service,
gpt_mini_service
)
@router.post("/ask", response_model=AskResponse)
async def ask_question(
request: AskRequest,
user: dict = Depends(get_current_user)
):
# 1. Validate input
validation = gpt_mini_service.validate_input(request.question)
if not validation["safe"]:
raise HTTPException(400, "Unsafe content")
# 2. Estimate cost
from app.services.tier_config import TierConfig
balance = wallet_service.get_balance(UUID(user["id"]))
tier = balance["subscription_tier"]
estimated = TierConfig.calculate_estimated_cost(tier, len(request.question))
# 3. Reserve tokens
reservation = wallet_service.reserve(
user_id=UUID(user["id"]),
estimated=estimated,
request_id=UUID(get_request_id())
)
# 4. Execute retrieval
retrieval_result = retrieval_pipeline.retrieve(
query=request.question,
user_tier=tier,
namespace=f"grade-{request.grade}-{request.subject}",
filter={"language": request.language or "fr"}
)
# 5. Generate answer with GPT-4o
# ... (use retrieval_result["results"] as context)
# 6. Finalize reservation
wallet_service.finalize(
reservation_id=UUID(reservation["id"]),
actual=actual_tokens_used
)
# 7. Return response
return AskResponse(...)
Service Registry
All available singletons in app.core.dependencies:
| Service | Variable Name | Used By |
|---|---|---|
| OpenAI Client | openai_client |
embedding_service, gpt_mini_service, quiz_generator |
| Supabase Service | supabase_service |
All services needing database access |
| Pinecone Adapter | pinecone_adapter |
embedding_service, retrieval_pipeline |
| Cache Service | cache_service |
retrieval_pipeline |
| Embedding Service | embedding_service |
retrieval_pipeline, ingestion workers |
| GPT-mini Service | gpt_mini_service |
retrieval_pipeline |
| Retrieval Pipeline | retrieval_pipeline |
chat router, quiz_generator |
| Wallet Service | wallet_service |
wallet router, chat router, quiz router |
| Chunking Service | chunking_service |
ingestion workers |
| Ingestion Service | ingestion_service |
admin router, ingestion router |
| Text Normalizer | text_normalizer |
scraper_service |
| Deduplication Service | deduplication_service |
scraper_service |
| Quality Checker | quality_checker |
scraper_service |
| Scraper Service | scraper_service |
scraper router |
| Quiz Generator | quiz_generator |
quiz router |
Testing with Dependency Injection
Unit Tests
Mock the dependencies:
from unittest.mock import Mock
from app.services.wallet_reservation import WalletReservationService
def test_reserve_insufficient_balance():
# Mock Supabase client
mock_supabase = Mock()
mock_supabase.table().select().eq().execute().data = [
{"token_balance": 5} # Insufficient
]
# Create service with mock
service = WalletReservationService(supabase=mock_supabase)
# Test
with pytest.raises(InsufficientBalanceError):
service.reserve(user_id=UUID(...), estimated=10)
Integration Tests
Override dependencies:
from fastapi.testclient import TestClient
from app.main import app
from app.core.dependencies import wallet_service
# Create mock service
mock_wallet = Mock()
mock_wallet.get_balance.return_value = {"token_balance": 100, ...}
# Override dependency
app.dependency_overrides[wallet_service] = lambda: mock_wallet
# Test
client = TestClient(app)
response = client.get("/wallet/balance", headers={"Authorization": "Bearer ..."})
assert response.status_code == 200
Benefits of This Pattern
1. Singleton Pattern
- Services initialized once at startup
- Shared state (cache, circuit breaker) across all requests
- No repeated initialization overhead
2. Dependency Inversion
- Routers depend on abstractions (service interfaces)
- Easy to swap implementations (e.g., in-memory cache → Redis)
- Testable (inject mocks)
3. Clear Dependencies
- Explicit dependency graph
- Easy to understand service relationships
- Prevents circular dependencies
4. FastAPI Integration
- Works with
Depends()for automatic injection - Type hints provide autocomplete
- Validation at compile time
Configuration
Services read from app.core.config.Settings:
# From .env
settings.OPENAI_API_KEY → openai_client
settings.PINECONE_API_KEY → pinecone_adapter
settings.SUPABASE_URL → supabase_service
settings.SUPABASE_SERVICE_ROLE_KEY → supabase_service
settings.OPENAI_EMBEDDING_MODEL → embedding_service
settings.OPENAI_MINI_MODEL → gpt_mini_service
Future Improvements
1. Lifespan Management
Use FastAPI lifespan events:
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: Initialize services
from app.core import dependencies
yield
# Shutdown: Close connections
# (Close OpenAI client, Pinecone, etc.)
app = FastAPI(lifespan=lifespan)
2. Health Checks for Dependencies
@router.get("/health/dependencies")
async def check_dependencies():
return {
"openai": check_openai_health(),
"supabase": check_supabase_health(),
"pinecone": check_pinecone_health()
}
3. Dependency Injection Framework
Consider using dependency-injector library:
from dependency_injector import containers, providers
class Container(containers.DeclarativeContainer):
config = providers.Configuration()
openai_client = providers.Singleton(
OpenAI,
api_key=config.openai_api_key
)
wallet_service = providers.Singleton(
WalletReservationService,
supabase=supabase_service
)
Status: ✅ Dependency injection working for wallet, admin, auth routers TODO: Wire chat, quiz, ingestion, scraper routers
See implementation_status.md for current implementation state