ADR-004: Controller Architecture and Dependency Graph
Status
AcceptedContext
Initially, Tasteful implemented routes directly within flavor classes. This approach seemed logical - a flavor would contain its configuration, services, repositories, and routes all in one place:- Dependency Injection Limitations: Routes couldn’t be properly integrated into the dependency injection lifecycle
- Service Instance Naming: Variable names for services were auto-generated by regex minimization, making it hard for developers to identify and use them correctly.
- Service Coupling: Routes were tightly coupled to the flavor’s specific service instances
- Testing Complexity: Difficult to mock dependencies for individual route testing
- Flexibility Issues: Routes couldn’t easily use different services or configurations without interfering with the flavor’s core components
- Decouple routes from flavor lifecycle management
- Enable proper dependency injection for routes
- Allow routes to use different services or configurations independently
- Maintain clear separation between flavor composition and route logic
Decision
We adopted decoupled controller architecture where routes are separated from flavor classes and managed through dedicated controller classes with proper dependency injection.The New Architecture
Controllers: Handle routes and HTTP logic with injected dependenciesRationale
Why Decouple Controllers from Flavors?
The original approach of embedding routes directly in flavors created several architectural problems: Problem 1: Dependency Injection Lifecycle IssuesWhy Flavor as Pure Composition?
Flavors now serve a single, clear purpose: composing the complete dependency graphWhy Personal Container per Flavor?
Each flavor gets its own dependency injection scope:- Isolation: Dependencies don’t interfere between flavors
- Flexibility: Each flavor can configure its dependencies independently
- Testing: Easy to override dependencies for specific flavors
- Modularity: Flavors are truly self-contained modules
Consequences
Positive
- Proper Separation of Concerns: Controllers handle HTTP logic, flavors handle composition
- Dependency Injection Integration: Routes can participate fully in the DI lifecycle
- Flexibility: Controllers can use different service implementations without affecting flavor composition
- Testability: Easy to mock dependencies and test controllers in isolation
- Modularity: Flavors are truly self-contained with their own dependency scopes
- Maintainability: Changes to routes don’t interfere with flavor’s core components
Negative
- Additional Abstraction Layer: More classes to understand (Controller + Flavor)
- Learning Curve: Developers need to understand the controller/flavor separation
- Container Complexity: Each flavor manages its own dependency graph
Trade-offs
- Flexibility vs. Simplicity: More flexible dependency management but requires understanding two concepts (controllers and flavors)
- Decoupling vs. Directness: Better separation of concerns but less direct than having routes in flavors
- Testability vs. Setup: Much better testability but requires more initial setup
Migration Benefits
- Backward Compatibility: Existing flavors can be gradually migrated
- Incremental Adoption: Teams can adopt controller pattern flavor by flavor
- Clear Upgrade Path: Simple transformation from flavor routes to controller classes