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¶
Organize Configuration Models - Group related settings into nested dataclasses - Use descriptive field names and metadata - Provide sensible defaults
Initialize Once at Startup - Call
setup_config()in your main entry point - Register globally usingset_global_config()- Handle errors gracefully with clear messagesUse Global Registry - Access configuration anywhere with
get_global_config()- No need to pass config objects around - Thread-safe and efficientSupport Multiple Environments - Use different config files for dev/staging/prod - Override with environment variables in containers - Use CLI arguments for debugging
Validate Configuration - Use
--check-variablesto diagnose issues - Add validation in__post_init__for complex rules - Fail fast with clear error messagesDocument Configuration - Use field metadata for descriptions - Provide
.env.examplefile - Document configuration files and their purposeTest 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¶
See Getting Started for a step-by-step tutorial
See Dynamic Updates for dynamic configuration updates
See Validation for configuration validation
See Global Configuration Registry for API reference