Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 100 additions & 17 deletions .claude/rules/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -625,15 +625,20 @@ except Exception: # Generic catch-all last
Controllers check the result.error_code field and map to HTTP status codes:

```python
from typing import Annotated
from fastapi import APIRouter, HTTPException, Depends
from app.shared.infrastructure.container import ApplicationContainer, get_fastapi_app_container
from app.shared.infrastructure.middleware import get_current_user_id

@router.post("/accounts", status_code=201)
async def create_account(
request: CreateAccountRequest,
handler: CreateAccountHandlerContract = Depends(get_create_account_handler),
user_id: int = Depends(get_current_user_id),
app_container: Annotated[ApplicationContainer, Depends(get_fastapi_app_container)],
user_id: Annotated[int, Depends(get_current_user_id)],
):
"""Create a new user account"""
handler = app_container.get_create_account_handler()

command = CreateAccountCommand(
user_id=user_id,
name=request.name,
Expand Down Expand Up @@ -674,50 +679,128 @@ async def create_account(

## Dependency Injection

### Define Contract-Based Factories
**IMPORTANT**: This project uses the **ApplicationContainer** pattern for centralized dependency management.

### Define Factory Functions in Infrastructure Layer

Factory functions take `db: AsyncSession` and `logger: LoggerContract` as parameters and return fully configured handlers:

```python
# infrastructure/dependencies.py
def get_user_repository(
db: AsyncSession = Depends(get_db),
) -> UserRepositoryContract:
return UserRepository(db)
# infrastructure/dependencies/dependencies.py
from sqlalchemy.ext.asyncio import AsyncSession
from app.shared.domain.contracts import LoggerContract

def login_handler_factory(db: AsyncSession, logger: LoggerContract) -> LoginHandlerContract:
"""Factory for LoginHandler with all dependencies"""
from app.context.auth.infrastructure.repositories import SessionRepository
from app.context.auth.domain.services import LoginService
from app.context.auth.application.handlers import LoginHandler

# Private helper to get repository
session_repo = _get_session_repository(db)
# Private helper to get service
login_service = _get_login_service(session_repo, logger)

return LoginHandler(login_service, logger)

def get_login_handler(
service: LoginServiceContract = Depends(get_login_service),
) -> LoginHandlerContract:
return LoginHandler(service)
def _get_session_repository(db: AsyncSession) -> SessionRepositoryContract:
"""Private helper to get repository instance"""
from app.context.auth.infrastructure.repositories import SessionRepository
return SessionRepository(db)

def _get_login_service(
session_repo: SessionRepositoryContract,
logger: LoggerContract,
) -> LoginServiceContract:
"""Private helper to get service instance"""
from app.context.auth.domain.services import LoginService
return LoginService(session_repo, logger)
```

### Inject in Controllers
**Export factories in `__init__.py`:**

```python
# infrastructure/dependencies/__init__.py
from .dependencies import login_handler_factory

__all__ = ["login_handler_factory"]
```

### Register Factories in ApplicationContainer

Add handler getter methods to the ApplicationContainer:

```python
# app/shared/infrastructure/container/app_container.py
class ApplicationContainer:
def __init__(self, db: AsyncSession):
self._db = db
self._logger = get_logger()

@property
def logger(self) -> LoggerContract:
return self._logger

def get_login_handler(self):
"""Get login handler with all dependencies"""
from app.context.auth.infrastructure.dependencies import login_handler_factory
return login_handler_factory(self._db, self._logger)
```

### Use ApplicationContainer in Controllers

Controllers inject the ApplicationContainer and access dependencies through it:

```python
from typing import Annotated
from fastapi import APIRouter, Depends
from app.shared.infrastructure.container import ApplicationContainer, get_fastapi_app_container

@router.post("/login")
async def login(
request: LoginRequest,
handler: LoginHandlerContract = Depends(get_login_handler),
app_container: Annotated[ApplicationContainer, Depends(get_fastapi_app_container)],
):
# Get dependencies from container
logger = app_container.logger
handler = app_container.get_login_handler()

# Use handler
command = LoginCommand(...)
return await handler.handle(command)
result = await handler.handle(command)
return result
```

**Benefits of this pattern:**
- **Centralized dependency management** - All wiring happens in one place (ApplicationContainer)
- **Cleaner controllers** - Single container injection instead of multiple `Depends()`
- **Easier testing** - Mock the entire container instead of individual dependencies
- **Consistent across contexts** - Same pattern for all bounded contexts
- **Better separation of concerns** - Dependency wiring separated from business logic

### Authenticating Requests

**Rule:** Always inject the authenticated user ID using the shared middleware dependency. Pass user_id as a **primitive** (int) to commands/queries.

**Pattern:**

```python
from typing import Annotated
from fastapi import APIRouter, Depends
from app.shared.infrastructure.container import ApplicationContainer, get_fastapi_app_container
from app.shared.infrastructure.middleware import get_current_user_id

@router.post("/accounts", status_code=201)
async def create_account(
request: CreateAccountRequest,
handler: CreateAccountHandlerContract = Depends(get_create_account_handler),
user_id: int = Depends(get_current_user_id), # ✅ Inject authenticated user
app_container: Annotated[ApplicationContainer, Depends(get_fastapi_app_container)],
user_id: Annotated[int, Depends(get_current_user_id)], # ✅ Inject authenticated user
):
"""Create a new user account"""
# Get dependencies from container
logger = app_container.logger
handler = app_container.get_create_account_handler()

command = CreateAccountCommand(
user_id=user_id, # ✅ Pass primitive to command
name=request.name,
Expand Down
67 changes: 52 additions & 15 deletions .claude/rules/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,18 @@ self._logger.critical("Unable to connect to authentication service", error=str(e

```python
# ✅ GOOD - Success logged only at controller
from typing import Annotated
from fastapi import Depends
from app.shared.infrastructure.container import ApplicationContainer, get_fastapi_app_container

@router.post("/cards")
async def create_credit_card(
request: CreateCreditCardRequest,
handler: CreateCreditCardHandlerContract = Depends(...),
logger: LoggerContract = Depends(get_logger),
app_container: Annotated[ApplicationContainer, Depends(get_fastapi_app_container)],
):
logger = app_container.logger
handler = app_container.get_create_credit_card_handler()

logger.info("Create credit card request", user_id=user_id, name=request.name)
result = await handler.handle(command)

Expand Down Expand Up @@ -211,15 +217,18 @@ class CreateCreditCardService:
- Internal implementation details (use handler/service for that)

```python
from app.shared.domain.contracts import LoggerContract
from app.shared.infrastructure.dependencies import get_logger
from typing import Annotated
from fastapi import Depends
from app.shared.infrastructure.container import ApplicationContainer, get_fastapi_app_container

@router.post("/login")
async def login(
request: LoginRequest,
handler: Annotated[LoginHandlerContract, Depends(get_login_handler)],
logger: Annotated[LoggerContract, Depends(get_logger)],
app_container: Annotated[ApplicationContainer, Depends(get_fastapi_app_container)],
):
logger = app_container.logger
handler = app_container.get_login_handler()

logger.info("Login attempt", email=str(request.email))

result = await handler.handle(LoginCommand(...))
Expand Down Expand Up @@ -395,30 +404,58 @@ class MyService:
self._logger = logger
```

### Step 2: Update Dependency Factory
### Step 2: Update Factory Function

Factory functions receive logger as a parameter:

```python
from sqlalchemy.ext.asyncio import AsyncSession
from app.shared.domain.contracts import LoggerContract
from app.shared.infrastructure.dependencies import get_logger

def get_my_service(
some_repo: Annotated[SomeRepositoryContract, Depends(get_some_repo)],
logger: Annotated[LoggerContract, Depends(get_logger)], # Add logger dependency
def my_handler_factory(db: AsyncSession, logger: LoggerContract) -> MyHandlerContract:
"""Factory for MyHandler with all dependencies"""
some_repo = _get_some_repository(db)
my_service = _get_my_service(some_repo, logger) # Pass logger to service
return MyHandler(my_service, logger) # Pass logger to handler

def _get_my_service(
some_repo: SomeRepositoryContract,
logger: LoggerContract,
) -> MyServiceContract:
"""Private helper to get service instance"""
return MyService(some_repo, logger)
```

### Step 3: Controllers Get Logger Directly
### Step 3: Register in ApplicationContainer

Controllers inject logger as a parameter (not passed through handlers):
Add handler getter method to ApplicationContainer:

```python
# app/shared/infrastructure/container/app_container.py
class ApplicationContainer:
def get_my_handler(self):
"""Get my handler with all dependencies"""
from app.context.my_context.infrastructure.dependencies import my_handler_factory
return my_handler_factory(self._db, self._logger)
```

### Step 4: Controllers Get Logger from Container

Controllers access logger through the ApplicationContainer:

```python
from typing import Annotated
from fastapi import Depends
from app.shared.infrastructure.container import ApplicationContainer, get_fastapi_app_container

@router.post("/endpoint")
async def my_endpoint(
request: MyRequest,
handler: Annotated[MyHandlerContract, Depends(get_my_handler)],
logger: Annotated[LoggerContract, Depends(get_logger)], # Inject logger
app_container: Annotated[ApplicationContainer, Depends(get_fastapi_app_container)],
):
logger = app_container.logger
handler = app_container.get_my_handler()

logger.info("Request received", some_field=request.some_field)
result = await handler.handle(...)
logger.info("Request completed", result_status=result.status)
Expand Down
Loading