> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tasteful.heka.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# ADR-004: Controller Architecture and Dependency Graph

> Separation of routes in controller classes and dependency graph management

# 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:

```python theme={null}
# Original approach - routes directly in flavor
class UserFlavor(BaseFlavor):
    def __init__(self):
        super().__init__()

    @Get("/users/{user_id}")
    async def get_user(self, user_id: int):
        return await self.user_service.get_user(user_id)
```

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

```python theme={null}
from tasteful.base_flavor import BaseController

class UserController(BaseController):
    def __init__(self, user_service: UserService, auth_service: AuthService):
        super().__init__(prefix="/users", tags=["users"])
        self.user_service = user_service
        self.auth_service = auth_service
    
    @Get("/{user_id}")
    async def get_user(self, user_id: int, request: Request):
        # Route logic with injected services
        user = request.state.user
        return await self.user_service.get_user(user_id, requesting_user=user)
    
    @Post("/")
    async def create_user(self, user_data: UserCreate, request: Request):
        user = request.state.user
        return await self.user_service.create_user(user_data, created_by=user)
```

**Flavors**: Compose and configure the complete dependency graph

```python theme={null}
class UserFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=UserController,
            services=[UserService, EmailService],
            repositories=[UserRepository],
            config=UserConfig
        )
```

## 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**

```python theme={null}
# Original problematic approach
class UserFlavor(BaseFlavor):
    def __init__(self):
        self.user_service = UserService()
    
    @Get("/users/{user_id}")
    async def get_user(self, user_id: int):
        # Route tied to flavor's specific service instance
        return await self.user_service.get_user(user_id)
```

**Problem 2: Inflexible Service Usage**
Routes couldn't use different configurations or alternative service implementations without modifying the entire flavor.

**Solution: Controller Decoupling**

```python theme={null}
# New approach - controllers with dependency injection
class UserController(BaseController):
    def __init__(self, user_service: UserService, auth_service: AuthService):
        super().__init__(prefix="/users", tags=["users"])
        self.user_service = user_service  # Explicitly declared variable name with injected dependency
        self.auth_service = auth_service  # Explicitly declared variable name that can use different implementation
    
    @Get("/{user_id}")
    async def get_user(self, user_id: int, request: Request):
        # Route uses injected services, not flavor's fixed instances
        return await self.user_service.get_user(user_id)
```

### Why Flavor as Pure Composition?

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

```python theme={null}
class UserFlavor(BaseFlavor):
    """
    A flavor is purely a composition of:
    - Controller (handles routes)
    - Services (business logic)
    - Repositories (data access)
    - Config (configuration)
    """
    def __init__(self):
        super().__init__(
            controller=UserController,
            services=[UserService, EmailService, NotificationService],
            repositories=[UserRepository, AuditRepository],
            config=UserConfig
        )
```

### 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

```python theme={null}
# Each flavor manages its own dependency graph
class OrderFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=OrderController,
            services=[OrderService, PaymentService, InventoryService],
            repositories=[OrderRepository, PaymentRepository],
            config=OrderConfig
        )

# Container automatically wires dependencies within flavor scope
def _register_flavor_dependencies(self, flavor: BaseFlavor) -> None:
    # Controller gets its dependencies injected from flavor's services
    controller_deps = self._resolve_controller_dependencies(
        flavor.controller, 
        available_services=flavor.services,
        available_configs=flavor.config
    )
```

## 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

* [Dependency Injector Documentation](https://python-dependency-injector.ets-labs.org/)
* [FastAPI Dependency Injection](https://fastapi.tiangolo.com/tutorial/dependencies/)
* [Clean Architecture Principles](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
