Configuration & Settings
Pydantic BaseSettings, adapter switching via environment variables, and the lru_cache singleton pattern.
Framework implementations
All configuration lives in config/settings.py. It uses Pydantic BaseSettings which reads from environment variables and optional .env files.
Structure
# config/settings.py
from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# --- Service identity ---
service_name: str = "my-service"
environment: str = "development" # development | staging | production
# --- Database ---
database_adapter: str = "sqlalchemy" # sqlalchemy | inmemory
database_url: str = "postgresql+asyncpg://..."
database_direct_url: str | None = None # For Prisma-style direct connections
# --- Events ---
event_adapter: str = "inmemory" # inmemory | sns | celery
event_sns_topic_arn: str | None = None
event_publish_timeout: float = 5.0
# --- Cache ---
cache_adapter: str = "inmemory" # inmemory | redis
cache_redis_url: str | None = None
# --- Monitoring ---
monitoring_adapter: str = "inmemory" # inmemory | cloudwatch
monitoring_namespace: str = "Service/Name"
monitoring_batch_size: int = 20
monitoring_flush_interval: float = 60.0
# --- Storage ---
storage_adapter: str = "local_file" # local_file | s3
storage_s3_bucket: str | None = None
storage_local_dir: str = "storage/media"
# --- Auth ---
auth_jwt_secret: str
auth_jwt_algorithm: str = "HS256"
auth_jwt_access_token_expire_minutes: int = 1440
auth_jwt_refresh_token_expire_days: int = 30
auth_jwt_issuer: str = "" # OAuth 2.0 iss claim — e.g. "https://api.your-service.com"
auth_jwt_audience: str = "" # OAuth 2.0 aud claim — e.g. "your-service-api"
# --- CORS ---
cors_allowed_origins: str = "http://localhost:3000" # comma-separated
# --- Feature flags ---
feature_<name>_enabled: bool = False
# --- AWS ---
aws_region: str | None = None
@lru_cache(maxsize=1)
def get_settings() -> Settings:
return Settings()Adapter Switching Pattern
Each infrastructure concern declares its adapter via a settings field:
| Setting field | Values | Used by |
|---|---|---|
database_adapter | sqlalchemy, inmemory | infrastructure/database/factories.py |
event_adapter | inmemory, sns, celery | infrastructure/events/factories.py |
cache_adapter | inmemory, redis | infrastructure/cache/factories.py |
monitoring_adapter | inmemory, cloudwatch | infrastructure/monitoring/factories.py |
storage_adapter | local_file, s3 | infrastructure/storage/factories.py |
Local/test environments use inmemory for all adapters — no external dependencies required.
Production environments switch adapters via environment variables only — no code changes.
Accessing Settings
Settings are loaded once per process via @lru_cache. Access them via:
from config.settings import get_settings
settings = get_settings()In api/dependencies.py, pass settings into factory calls:
from config.settings import get_settings
from infrastructure.events.factories import build_event_publisher
from functools import lru_cache
@lru_cache(maxsize=1)
def get_event_publisher():
return build_event_publisher(get_settings())Environment Files
| File | Purpose |
|---|---|
.env | Local development overrides (gitignored) |
.env.example | Committed template showing all required variables |
| Environment variables | Set directly in CI/CD or container orchestration for staging/production |
The .env.example is the contract — every required setting must appear there with a comment explaining its purpose.
Rules
- Never hardcode values that differ between environments. Everything goes through settings.
- Never import settings directly in
domain/orapplication/. Those layers are settings-agnostic. - Feature flags use
boolsettings fields — never string conditionals likeif environment == "production". @lru_cacheonget_settings()ensures a single settings instance per process. Use this everywhere rather than instantiatingSettings()directly.