Documentation

Configuration & Settings

Pydantic BaseSettings, adapter switching via environment variables, and the lru_cache singleton pattern.

Show:

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 fieldValuesUsed by
database_adaptersqlalchemy, inmemoryinfrastructure/database/factories.py
event_adapterinmemory, sns, celeryinfrastructure/events/factories.py
cache_adapterinmemory, redisinfrastructure/cache/factories.py
monitoring_adapterinmemory, cloudwatchinfrastructure/monitoring/factories.py
storage_adapterlocal_file, s3infrastructure/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

FilePurpose
.envLocal development overrides (gitignored)
.env.exampleCommitted template showing all required variables
Environment variablesSet 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/ or application/. Those layers are settings-agnostic.
  • Feature flags use bool settings fields — never string conditionals like if environment == "production".
  • @lru_cache on get_settings() ensures a single settings instance per process. Use this everywhere rather than instantiating Settings() directly.