# Как эта архитектура реализована в проекте ## Общая схема Проект собирается так: 1. `adapter/config/loader.py` читает YAML, `.env` и env vars 2. `adapter/di/container.py` строит единый контейнер 3. `adapter/observability/factory.py` создает logger, metrics и tracer runtime 4. repository и usecase создаются один раз при старте 5. `adapter/http/fastapi/app.py` создает FastAPI app и подключает router + middleware Это и есть composition root: все внешние зависимости собираются в одном месте и потом передаются внутрь. ## Как здесь реализована чистая архитектура ### `domain/` - хранит сущности и доменные ошибки - не зависит от framework и SDK ### `usecase/` - описывает сценарии приложения - задает интерфейсы через `Protocol` - работает только с абстракциями ### `repository/` - реализует интерфейсы доступа к данным - может использовать tracer, clients, DB drivers ### `adapter/` - оборачивает внешние инструменты - переводит HTTP/config/OTel в контракты приложения ## Как создать новый use case Рабочая последовательность: 1. Если нужна новая бизнес-сущность или ошибка, добавить ее в `domain/` 2. Если use case зависит от внешнего ресурса, описать контракт в `usecase/` 4. Создать класс use case 5. Внедрить зависимости через конструктор 6. Подключить use case в `adapter/di/container.py` Минимальный шаблон: ```py from usecase.interface import Logger, Metrics, Tracer, UserRepository from domain.user import User class CreateUser: def __init__(self, repository: UserRepository, logger: Logger, metrics: Metrics, tracer: Tracer) -> None: self._repository = repository self._logger = logger self._metrics = metrics self._tracer = tracer def execute(self, email: str) -> User: with self._tracer.start_span('usecase.create_user', attrs={'user.email': email}): user = self._repository.save(email) self._metrics.increment('user.create.total') self._logger.info('user_create', attrs={'user.email': email}) return user ``` Правила: - не импортировать FastAPI в use case - не читать env vars в use case - не работать с `Request`, `Response`, `HTTPException` ## Как работать с repository ### 1. Сначала описать интерфейс Контракт должен жить рядом с use case, который его использует. ```py class UserRepository(Protocol): def get(self, user_id: str) -> User | None: ... def save(self, user: User) -> None: ... ``` ### 2. Потом сделать реализацию Реализация уходит во внешний слой, например в `repository/`. ```py class InMemoryUserRepository(UserRepository): def __init__(self, tracer: Tracer) -> None: self._tracer = tracer self._users: dict[str, User] = {} ``` ### 3. Подключить в контейнере Именно `adapter/di/container.py` решает, какую реализацию дать use case. Это важно: use case не должен сам выбирать repository. ## Как добавить новую HTTP ручку Последовательность: 1. Создать или переиспользовать use case 2. При необходимости добавить схемы в `adapter/http/fastapi/schemas.py` 3. Добавить dependency, если нужен новый use case из контейнера 4. Добавить маршрут в `adapter/http/fastapi/routers/v1/router.py` 5. Преобразовать HTTP input в команду или query use case 6. Преобразовать результат use case в response model 7. Смаппить доменные ошибки в `HTTPException` Шаблон: ```py @router.post('/users', response_model=UserResponse) def create_user(payload: CreateUserRequest, usecase: CreateUser = Depends(get_create_user)) -> UserResponse: try: user = usecase.execute(payload.email) except UserConflictError as exc: raise HTTPException(status_code=409, detail=str(exc)) from exc return UserResponse(id=user.id, email=user.email) ``` Правило: router остается тонким. ## Как пользоваться логгером, метриками и трейсами ### Логгер - использовать для бизнес-событий и предупреждений - передавать данные через `attrs` - сообщения делать короткими и стабильными ### Метрики - использовать для счетчиков и длительностей - не создавать условные ветки `if metrics_enabled` - runtime сам подставит рабочую или noop-реализацию ### Трейсы - открывать span на границе важной операции - класть ключевые атрибуты в `attrs` - ошибки писать через `record_error`, если это делает смысл явным ## Как конфигурировать проект ### Базовая конфигурация Основной файл - `config/app.yaml`. Там лежат: - имя приложения - окружение - HTTP host/port - режим логирования - флаги метрик и трейсов - OTel endpoints - имя заголовка токена ### Секреты Обязательные секреты берутся из env: - `APP_API_TOKEN` - `APP_SIGNING_KEY` ### Переопределение через env Можно переопределять YAML через переменные окружения, например: - `APP_HTTP_HOST` - `APP_HTTP_PORT` - `APP_LOGGING_LEVEL` - `APP_LOGGING_OUTPUT` - `APP_LOGGING_FORMAT` - `APP_LOGGING_FILE_PATH` - `APP_METRICS_ENABLED` - `APP_TRACING_ENABLED` - `APP_OTEL_LOGS_ENDPOINT` - `APP_OTEL_METRICS_ENDPOINT` - `APP_OTEL_TRACES_ENDPOINT` ### Локальный запуск ```bash APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run ``` ### Запуск с OTel ```bash APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make run-otel ``` ### Docker Compose ```bash APP_API_TOKEN=local-api-token APP_SIGNING_KEY=local-signing-key make compose-up ``` ## Короткая памятка - новая бизнес-логика -> `usecase/` - новый контракт -> рядом с use case - новая реализация контракта -> `repository/` или внешний `adapter/` - новый HTTP маршрут -> `adapter/http/fastapi/` - новый infra/runtime code -> `adapter/` - зависимости связывать только в `adapter/di/container.py`