Why Late Binding? ================= Late binding (or lazy instantiation) is a core design principle of **hdmi**. This document explains why we chose this approach and how it benefits your applications. What is Late Binding? --------------------- **Late binding** means that services are instantiated only when they're first accessed, not when the builder is configured or the container is built. .. mermaid:: graph LR subgraph "Early Binding (Eager)" E1[Configure] --> E2[Build] E2 --> E3[Instantiate ALL] E3 --> E4[Use Services] end subgraph "Late Binding (Lazy)" L1[Configure] --> L2[Build & Validate] L2 --> L3[Use Service A] L3 --> L4[Instantiate A only] L2 --> L5[Use Service B] L5 --> L6[Instantiate B only] end style E3 fill:#ffcccc style L4 fill:#ccffcc style L6 fill:#ccffcc With early binding, all services are created upfront. With late binding, they're created on-demand. Benefits of Late Binding ------------------------- 1. Faster Startup Time ~~~~~~~~~~~~~~~~~~~~~~ Applications start faster because services aren't created until needed: .. code-block:: python import asyncio async def main(): # With early binding: ALL services created here (slow!) container = builder.build() # With late binding: only validation happens (fast!) container = builder.build() # Services created only when accessed service = await container.get(MyService) # <- instantiation happens here asyncio.run(main()) For applications with many services, this can significantly reduce startup time. 2. Lower Memory Footprint ~~~~~~~~~~~~~~~~~~~~~~~~~~ Services that are never used are never created: .. code-block:: python import asyncio async def main(): # Register many services builder.register(ServiceA) builder.register(ServiceB) builder.register(ServiceC) # ... 50 more services ... container = builder.build() # Only use one service service_a = await container.get(ServiceA) # ServiceB, ServiceC, and the other 50 services are never instantiated # Memory is conserved! asyncio.run(main()) This is especially valuable in: - CLI applications (not all commands use all services) - Testing (tests often use a subset of services) - Microservices (different endpoints use different services) 3. Conditional Service Creation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Services can be created based on runtime conditions: .. code-block:: python import asyncio async def main(): container = builder.build() if user_wants_feature_x(): # Service created only if feature is used feature_x = await container.get(FeatureXService) if environment == "production": # Different service for production monitor = await container.get(ProductionMonitor) else: # Different service for development monitor = await container.get(DevMonitor) asyncio.run(main()) 4. Better Error Isolation ~~~~~~~~~~~~~~~~~~~~~~~~~~ Errors in service constructors don't prevent the application from starting: .. code-block:: python import asyncio async def main(): # Build succeeds even if OptionalService has constructor issues container = builder.build() try: # Error only occurs if we actually try to use this service optional = await container.get(OptionalService) except Exception: # Handle gracefully - other services still work logger.warning("Optional feature unavailable") # Core services still work fine core = await container.get(CoreService) asyncio.run(main()) 5. Circular Dependency Detection Without Full Instantiation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Late binding allows us to validate the dependency graph without creating instances: .. code-block:: python # These services have a circular dependency builder.register(ServiceA) # depends on ServiceB builder.register(ServiceB) # depends on ServiceA # Validation catches the cycle immediately container = builder.build() # Raises CircularDependencyError # No services were instantiated! # The error is caught at graph-validation time, not instantiation time Early Validation + Late Instantiation -------------------------------------- The key insight of **hdmi** is that you can validate the dependency graph without instantiating services: .. mermaid:: graph TD Builder[ContainerBuilder
Configuration] --> Build{.build} Build -->|Validation| CheckCycles[Check for cycles] CheckCycles --> CheckTypes[Check types] CheckTypes --> CheckResolvable[Check all resolvable] CheckResolvable --> Container[Container
Validated Graph] Container -->|Later| Get{.get} Get --> Instantiate[Instantiate Service] style Build fill:#fff4e1 style Instantiate fill:#e1ffe1 This gives you: - **Safety**: All configuration errors detected at build time - **Performance**: Services created only when needed - **Flexibility**: Runtime decisions about which services to use When Late Binding Isn't Appropriate ------------------------------------ Late binding isn't always the right choice. Consider early binding when: 1. **Fail-Fast is Critical** If you need to know immediately that all services can be instantiated successfully: .. code-block:: python import asyncio async def main(): # With late binding, this succeeds even if services can't be created container = builder.build() # You might want to eagerly instantiate critical services await container.get(DatabaseConnection) # Fail immediately if DB is down await container.get(ConfigService) # Fail immediately if config is invalid asyncio.run(main()) 2. **Warm-Up is Beneficial** For some services, instantiation is expensive and you want to do it upfront: .. code-block:: python import asyncio async def main(): # Warm up expensive services during startup ml_model = await container.get(MachineLearningModel) # Takes 30 seconds cache = await container.get(CacheService) # Needs initialization # Now the services are ready for fast access asyncio.run(main()) 3. **Deterministic Resource Allocation** When you need to know upfront what resources will be allocated: .. code-block:: python # Late binding makes it hard to predict memory usage # Early binding (or explicit instantiation) gives predictability Comparison with Other Approaches --------------------------------- ====================== ==================== ==================== ===================== Approach Startup Speed Memory Usage Error Detection ====================== ==================== ==================== ===================== Early Binding Slow (all services) High (all services) All errors upfront Late Binding (hdmi) Fast (validation) Low (on-demand) Config errors early, instance errors late Factory Pattern Fast Low Deferred Service Locator Fast Low Runtime only ====================== ==================== ==================== ===================== **hdmi's approach** gives you the best of both worlds: - Configuration errors are caught early (like early binding) - Instantiation is deferred (like factory pattern) - Services are typed and validated (unlike service locator) Real-World Example ------------------ Consider a web application with these services: .. code-block:: python from hdmi import ContainerBuilder # Configure all services builder = ContainerBuilder() builder.register(DatabaseConnection) builder.register(CacheService) builder.register(EmailService) builder.register(PaymentService) builder.register(AnalyticsService) builder.register(LoggingService) builder.register(MonitoringService) # Build container (validates, but doesn't instantiate) container = builder.build() # < 1ms, all dependencies validated # Handle a simple GET request @app.get("/health") async def health(): # Only LoggingService is instantiated logger = await container.get(LoggingService) logger.info("Health check") return {"status": "ok"} # Handle a payment request @app.post("/payment") async def payment(): # Now PaymentService, DatabaseConnection are instantiated payment_service = await container.get(PaymentService) return payment_service.process() Benefits in this scenario: - Health check endpoint is very fast (doesn't initialize payment services) - Each endpoint only pays for the services it uses - All endpoints benefit from early validation of the dependency graph Conclusion ---------- Late binding provides: ✅ Faster startup ✅ Lower memory usage ✅ Better error isolation ✅ More flexibility ✅ Still catches configuration errors early The only tradeoff is that instantiation errors happen at first access, not at startup. For most applications, this is an excellent tradeoff.