Best Practices for Real-World Applications

This guide shows you how to structure and use Varlord in real-world applications, including file organization, initialization patterns, and using the global configuration registry.

Project Structure

Here’s a recommended project structure for a real-world application using Varlord:

myapp/
├── config/
│   ├── __init__.py          # Export config models and setup function
│   ├── models.py            # Configuration models (dataclasses)
│   └── setup.py             # Configuration initialization
├── src/
│   ├── __init__.py
│   ├── api/
│   │   ├── __init__.py
│   │   └── routes.py        # Uses global config
│   ├── services/
│   │   ├── __init__.py
│   │   └── database.py      # Uses global config
│   └── main.py              # Application entry point
├── config/
│   ├── app.yaml             # Application defaults
│   └── production.yaml      # Production overrides
├── .env.example             # Example environment variables
├── .env                     # Local development (gitignored)
└── requirements.txt

Configuration Models

Define your configuration models in config/models.py:

 1"""Configuration models for the application."""
 2
 3from dataclasses import dataclass, field
 4from pathlib import Path
 5from typing import Optional
 6
 7@dataclass(frozen=True)
 8class DatabaseConfig:
 9    """Database configuration."""
10    password: str = field(metadata={"description": "Database password"})
11    host: str = field(default="localhost", metadata={"description": "Database host"})
12    port: int = field(default=5432, metadata={"description": "Database port"})
13    name: str = field(default="myapp", metadata={"description": "Database name"})
14    user: str = field(default="postgres", metadata={"description": "Database user"})
15    pool_size: int = field(default=10, metadata={"description": "Connection pool size"})
16
17@dataclass(frozen=True)
18class RedisConfig:
19    """Redis configuration."""
20    host: str = field(default="localhost", metadata={"description": "Redis host"})
21    port: int = field(default=6379, metadata={"description": "Redis port"})
22    db: int = field(default=0, metadata={"description": "Redis database number"})
23    password: Optional[str] = field(default=None, metadata={"description": "Redis password"})
24
25@dataclass(frozen=True)
26class APIConfig:
27    """API server configuration."""
28    host: str = field(default="0.0.0.0", metadata={"description": "API server host"})
29    port: int = field(default=8000, metadata={"description": "API server port"})
30    debug: bool = field(default=False, metadata={"description": "Enable debug mode"})
31    cors_origins: list[str] = field(
32        default_factory=lambda: ["http://localhost:3000"],
33        metadata={"description": "Allowed CORS origins"}
34    )
35
36@dataclass(frozen=True)
37class AppConfig:
38    """Main application configuration."""
39    # Required fields first (no defaults)
40    secret_key: str = field(metadata={"description": "Secret key for encryption"})
41
42    # Nested configurations
43    database: DatabaseConfig = field(default_factory=DatabaseConfig)
44    redis: RedisConfig = field(default_factory=RedisConfig)
45    api: APIConfig = field(default_factory=APIConfig)
46
47    # Application-level settings (with defaults)
48    app_name: str = field(default="MyApp", metadata={"description": "Application name"})
49    log_level: str = field(default="INFO", metadata={"description": "Logging level"})
50    environment: str = field(default="development", metadata={"description": "Environment name"})

Configuration Setup

Create a setup function in config/setup.py:

 1"""Configuration setup and initialization."""
 2
 3import os
 4from pathlib import Path
 5
 6from varlord import Config, sources
 7from varlord.global_config import set_global_config
 8
 9from .models import AppConfig
10
11def setup_config() -> Config:
12    """Initialize and register application configuration.
13
14    This function should be called once at application startup.
15
16    Returns:
17        Config instance (also registered globally)
18    """
19    # Determine environment
20    env = os.getenv("ENVIRONMENT", "development")
21    app_dir = Path(__file__).parent.parent
22
23    # Build sources list with priority order
24    # Priority: CLI > Env > .env > User Config > App Config > Defaults
25    config_sources = [
26        # System/application defaults (lowest priority)
27        sources.YAML(str(app_dir / "config" / "app.yaml")),
28
29        # Environment-specific overrides
30        sources.YAML(str(app_dir / "config" / f"{env}.yaml")),
31
32        # User-specific configuration (if exists)
33        sources.YAML(Path.home() / ".config" / "myapp" / "config.yaml"),
34
35        # Environment variables (common in containers/CI)
36        sources.Env(),
37
38        # Local development .env file
39        sources.DotEnv(str(app_dir / ".env")),
40
41        # Command-line arguments (highest priority, for debugging/overrides)
42        sources.CLI(),
43    ]
44
45    # Create configuration
46    cfg = Config(
47        model=AppConfig,
48        sources=config_sources,
49    )
50
51    # Handle CLI commands (--help, --check-variables)
52    cfg.handle_cli_commands()
53
54    # Register globally for easy access throughout the application
55    set_global_config(cfg, name="app")
56
57    return cfg
58
59def get_config():
60    """Get the application configuration (convenience function).
61
62    Returns:
63        Loaded configuration object
64    """
65    from varlord.global_config import get_global_config
66
67    config = get_global_config(name="app")
68    return config.load()

Application Entry Point

Initialize configuration in your main application file:

 1"""Application entry point."""
 2
 3import logging
 4import sys
 5
 6from varlord import set_log_level
 7from config.setup import setup_config, get_config
 8
 9def main():
10    """Main application entry point."""
11    # Step 1: Setup logging (optional, but recommended)
12    logging.basicConfig(
13        level=logging.INFO,
14        format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
15    )
16    set_log_level(logging.DEBUG)  # Enable varlord debug logging
17
18    # Step 2: Initialize configuration (once at startup)
19    try:
20        cfg = setup_config()
21        app_config = get_config()
22    except Exception as e:
23        print(f"❌ Failed to load configuration: {e}")
24        print("\n💡 Tips:")
25        print("   - Check required fields: python main.py --check-variables")
26        print("   - See help: python main.py --help")
27        sys.exit(1)
28
29    # Step 3: Validate configuration (optional, but recommended)
30    if app_config.environment == "production" and app_config.api.debug:
31        print("⚠️  Warning: Debug mode is enabled in production!")
32        sys.exit(1)
33
34    # Step 4: Start your application
35    print(f"✅ Starting {app_config.app_name} in {app_config.environment} mode")
36    print(f"   API: http://{app_config.api.host}:{app_config.api.port}")
37    print(f"   Database: {app_config.database.host}:{app_config.database.port}")
38
39    # Your application code here
40    start_server(app_config)
41
42if __name__ == "__main__":
43    main()

Using Configuration in Business Code

Once configuration is initialized, you can access it anywhere in your application using the global registry:

Option 1: Using Global Config (Recommended)

 1"""API routes module."""
 2
 3from varlord.global_config import get_global_config
 4
 5def get_api_routes():
 6    """Get API routes configuration."""
 7    config = get_global_config(name="app")
 8    app_config = config.load()
 9
10    return {
11        "host": app_config.api.host,
12        "port": app_config.api.port,
13        "debug": app_config.api.debug,
14        "cors_origins": app_config.api.cors_origins,
15    }
16
17def setup_database():
18    """Setup database connection."""
19    config = get_global_config(name="app")
20    app_config = config.load()
21
22    db_config = app_config.database
23    # Use db_config.host, db_config.port, etc.
24    return create_db_connection(db_config)

Option 2: Using ConfigStore (For Dynamic Updates)

If you need dynamic configuration updates, use ConfigStore:

 1"""Service module with dynamic configuration."""
 2
 3from varlord.global_config import get_global_config
 4
 5def setup_service_with_watch():
 6    """Setup service with dynamic configuration updates."""
 7    config = get_global_config(name="app")
 8
 9    # Load as ConfigStore for dynamic updates
10    store = config.load_store()
11
12    # Subscribe to configuration changes
13    def on_config_change(new_config, diff):
14        print(f"Configuration updated: {diff}")
15        # Update service behavior based on new config
16        if "api.port" in diff.modified:
17            print(f"Port changed to {new_config.api.port}")
18
19    store.subscribe(on_config_change)
20
21    # Get current configuration (thread-safe)
22    current_config = store.get()
23    return current_config

Option 3: Dependency Injection (Alternative Pattern)

If you prefer explicit dependency injection, you can pass configuration as a parameter:

 1"""Service module with dependency injection."""
 2
 3def process_data(config: AppConfig):
 4    """Process data using configuration."""
 5    # Use config.database, config.redis, etc.
 6    pass
 7
 8# In your main code:
 9app_config = get_config()
10process_data(app_config)

Complete Example: Web Application

Here’s a complete example showing a web application structure:

config/__init__.py:

"""Configuration package."""

from .models import AppConfig, DatabaseConfig, RedisConfig, APIConfig
from .setup import setup_config, get_config

__all__ = [
    "AppConfig",
    "DatabaseConfig",
    "RedisConfig",
    "APIConfig",
    "setup_config",
    "get_config",
]

src/services/database.py:

 1"""Database service."""
 2
 3from varlord.global_config import get_global_config
 4
 5def get_db_connection():
 6    """Get database connection using global configuration."""
 7    config = get_global_config(name="app")
 8    app_config = config.load()
 9
10    db = app_config.database
11    return create_connection(
12        host=db.host,
13        port=db.port,
14        database=db.name,
15        user=db.user,
16        password=db.password,
17        pool_size=db.pool_size,
18    )

src/api/routes.py:

 1"""API routes."""
 2
 3from varlord.global_config import get_global_config
 4
 5def create_app():
 6    """Create Flask/FastAPI application."""
 7    config = get_global_config(name="app")
 8    app_config = config.load()
 9
10    # Create app with configuration
11    app = create_web_app(
12        host=app_config.api.host,
13        port=app_config.api.port,
14        debug=app_config.api.debug,
15        cors_origins=app_config.api.cors_origins,
16    )
17
18    return app

src/main.py:

 1"""Main application entry point."""
 2
 3import sys
 4from config.setup import setup_config, get_config
 5from src.api.routes import create_app
 6from src.services.database import get_db_connection
 7
 8def main():
 9    """Main entry point."""
10    # Initialize configuration
11    try:
12        setup_config()
13        app_config = get_config()
14    except Exception as e:
15        print(f"Configuration error: {e}")
16        sys.exit(1)
17
18    # Initialize services
19    db = get_db_connection()
20
21    # Create and start application
22    app = create_app()
23    app.run(
24        host=app_config.api.host,
25        port=app_config.api.port,
26        debug=app_config.api.debug,
27    )
28
29if __name__ == "__main__":
30    main()

Configuration Files

config/app.yaml (Application defaults):

app_name: MyApp
environment: development
log_level: INFO

database:
  host: localhost
  port: 5432
  name: myapp
  user: postgres
  pool_size: 10

redis:
  host: localhost
  port: 6379
  db: 0

api:
  host: 0.0.0.0
  port: 8000
  debug: false
  cors_origins:
    - http://localhost:3000

config/production.yaml (Production overrides):

environment: production
log_level: WARNING

api:
  debug: false
  cors_origins:
    - https://myapp.com

.env.example (Example environment variables):

# Required
SECRET_KEY=your-secret-key-here
DATABASE__PASSWORD=your-database-password

# Optional
ENVIRONMENT=development
API__PORT=8000
DATABASE__HOST=localhost
REDIS__HOST=localhost

Best Practices Summary

  1. Organize Configuration Models - Group related settings into nested dataclasses - Use descriptive field names and metadata - Provide sensible defaults

  2. Initialize Once at Startup - Call setup_config() in your main entry point - Register globally using set_global_config() - Handle errors gracefully with clear messages

  3. Use Global Registry - Access configuration anywhere with get_global_config() - No need to pass config objects around - Thread-safe and efficient

  4. Support Multiple Environments - Use different config files for dev/staging/prod - Override with environment variables in containers - Use CLI arguments for debugging

  5. Validate Configuration - Use --check-variables to diagnose issues - Add validation in __post_init__ for complex rules - Fail fast with clear error messages

  6. Document Configuration - Use field metadata for descriptions - Provide .env.example file - Document configuration files and their purpose

  7. Test Configuration Loading - Test with different source combinations - Test validation and error handling - Test in different environments

Common Patterns

Pattern 1: Simple Application

# config/setup.py
def setup_config():
    cfg = Config(
        model=AppConfig,
        sources=[sources.Env(), sources.CLI()],
    )
    cfg.handle_cli_commands()
    set_global_config(cfg)
    return cfg

Pattern 2: Multi-Environment Application

# config/setup.py
def setup_config():
    env = os.getenv("ENVIRONMENT", "development")
    cfg = Config(
        model=AppConfig,
        sources=[
            sources.YAML(f"config/{env}.yaml"),
            sources.Env(),
            sources.CLI(),
        ],
    )
    cfg.handle_cli_commands()
    set_global_config(cfg)
    return cfg

Pattern 3: Application with Dynamic Updates

# config/setup.py
def setup_config():
    cfg = Config(
        model=AppConfig,
        sources=[
            sources.Env(),
            sources.Etcd(host="etcd.example.com", watch=True),
        ],
    )
    cfg.handle_cli_commands()

    # Use ConfigStore for dynamic updates
    store = cfg.load_store()
    set_global_config(store, name="app")
    return store

Pattern 4: Multiple Named Configurations

# config/setup.py
def setup_config():
    # App configuration
    app_cfg = Config(model=AppConfig, sources=[...])
    set_global_config(app_cfg, name="app")

    # Database configuration
    db_cfg = Config(model=DatabaseConfig, sources=[...])
    set_global_config(db_cfg, name="database")

    # Use in code:
    # app_config = get_global_config(name="app").load()
    # db_config = get_global_config(name="database").load()

Troubleshooting

Problem: Configuration not found

from varlord.global_config import get_global_config, has_global_config

if has_global_config(name="app"):
    config = get_global_config(name="app")
else:
    raise RuntimeError("Configuration not initialized. Call setup_config() first.")

Problem: Configuration changes not reflected

If using Config.load(), configuration is loaded once. For dynamic updates, use ConfigStore:

# Instead of:
config = get_global_config(name="app")
app = config.load()  # Static, loaded once

# Use:
store = get_global_config(name="app")
app = store.get()  # Dynamic, always current

Problem: Thread safety concerns

Global configuration uses thread-local storage, so each thread has its own registry. This is usually what you want, but if you need to share configuration across threads, pass the config object explicitly or use a shared ConfigStore instance.

Next Steps