Skip to main content

ADR-004: Controller Architecture and Dependency Graph

Status

Accepted

Context

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:
However, this approach created several problems:
  1. Dependency Injection Limitations: Routes couldn’t be properly integrated into the dependency injection lifecycle
  2. Service Instance Naming: Variable names for services were auto-generated by regex minimization, making it hard for developers to identify and use them correctly.
  3. Service Coupling: Routes were tightly coupled to the flavor’s specific service instances
  4. Testing Complexity: Difficult to mock dependencies for individual route testing
  5. Flexibility Issues: Routes couldn’t easily use different services or configurations without interfering with the flavor’s core components
We needed an architecture that would:
  • 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 dependencies
Flavors: Compose and configure the complete dependency graph

Rationale

Why Decouple Controllers from Flavors?

The original approach of embedding routes directly in flavors created several architectural problems: Problem 1: Dependency Injection Lifecycle Issues
Problem 2: Inflexible Service Usage Routes couldn’t use different configurations or alternative service implementations without modifying the entire flavor. Solution: Controller Decoupling

Why Flavor as Pure Composition?

Flavors now serve a single, clear purpose: composing the complete dependency graph

Why Personal Container per Flavor?

Each flavor gets its own dependency injection scope:
  1. Isolation: Dependencies don’t interfere between flavors
  2. Flexibility: Each flavor can configure its dependencies independently
  3. Testing: Easy to override dependencies for specific flavors
  4. 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

References