PRO

Как выстроить архитектуру FastAPI по принципам чистой архитектуры / DDD?

В чистой архитектуре FastAPI остаётся внешним delivery-слоем: routers и schemas зависят от application use cases, а доменная логика не зависит от FastAPI, SQLAlchemy или конкретной базы. DDD помогает выделить bounded contexts, сущности, value objects, агрегаты и явные бизнес-инварианты.
Подробный ответ

Главный принцип

Зависимости должны быть направлены внутрь: инфраструктура зависит от application и domain, а domain не зависит от FastAPI, SQLAlchemy, Redis или HTTP. Это уменьшает связанность и позволяет менять фреймворк, хранилище или транспорт без переписывания бизнес-правил.

Пример структуры

app/
  domain/
    orders/
      entities.py
      value_objects.py
      repositories.py
      services.py
  application/
    orders/
      commands.py
      use_cases.py
      dto.py
  infrastructure/
    db/
      models.py
      repositories.py
    messaging/
    clients/
  presentation/
    api/
      routers/
      schemas/
      dependencies.py
  main.py

Роли слоёв

  • domain — сущности, value objects, инварианты и доменные события;

  • application — use cases, команды, транзакционные сценарии и порты;

  • infrastructure — SQLAlchemy repositories, кеш, брокер, внешние HTTP-клиенты;

  • presentation — FastAPI routers, Pydantic request/response schemas, аутентификация и HTTP-ошибки.

Use case и порты

Application layer зависит от абстракции репозитория, а инфраструктура реализует эту абстракцию. Router вызывает use case и преобразует HTTP-данные в command или DTO, не выполняя сам бизнес-логику.

from typing import Protocol


class OrderRepository(Protocol):
    async def get(self, order_id: int) -> 'Order | None': ...
    async def save(self, order: 'Order') -> None: ...


class CancelOrder:
    def __init__(self, repository: OrderRepository):
        self.repository = repository

    async def execute(self, order_id: int) -> None:
        order = await self.repository.get(order_id)
        if order is None:
            raise OrderNotFound(order_id)
        order.cancel()
        await self.repository.save(order)

DDD на практике

DDD стоит применять там, где есть сложные правила и богатая предметная область: платежи, биллинг, логистика, права доступа. Для простого CRUD чрезмерное количество слоёв создаёт только boilerplate. Архитектура должна соответствовать сложности домена и команды.

Транзакции и события

Use case обычно определяет транзакционную границу. Интеграционные события стоит публиковать надёжно, например через outbox pattern, а не отправлять сообщение во внешний broker до успешного commit транзакции.

Как ответить на собеседовании

В чистой архитектуре FastAPI — это слой доставки HTTP, а не место для бизнес-логики. Routers вызывают application use cases, use cases работают с портами репозиториев, а SQLAlchemy и внешние сервисы остаются в infrastructure. DDD применяю к сложным bounded contexts и не навязываю тяжёлую архитектуру обычному CRUD.

Оцени свой прогресс

Честно оцени своё понимание этого вопроса, чтобы мы могли построить твой учебный трек максимально эффективно.
Читать в блоге