← Назад к вопросам

Как построить архитектуру на основе DDD?

2.7 Senior🔥 121 комментариев
#Архитектура и паттерны

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Архитектура на основе Domain-Driven Design (DDD)

DDD — методология проектирования сложных приложений, фокусирующая на бизнес-логике как центре архитектуры. Это особенно критично для больших систем.

Слои архитектуры DDD

┌─────────────────────────────────────────────┐
│         Presentation (FastAPI, Django)      │  Контроллеры, сериализация
├─────────────────────────────────────────────┤
│          Application (Use Cases)            │  Orchestration, транзакции
├─────────────────────────────────────────────┤
│     Domain (Business Logic, Entities)       │  Rules, algorithms (защищено)
├─────────────────────────────────────────────┤
│  Infrastructure (Database, External APIs)   │  Технические детали
└─────────────────────────────────────────────┘

Правило зависимостей: только от центра к периферии. Domain не знает о Database.

1. Domain слой (ядро приложения)

Entities (Сущности)

# domain/models/user.py
from dataclasses import dataclass
from datetime import datetime
from typing import Optional
from enum import Enum

class UserStatus(Enum):
    ACTIVE = "active"
    BANNED = "banned"
    DELETED = "deleted"

@dataclass
class User:
    """Domain Entity - не зависит от БД"""
    id: str
    email: str
    password_hash: str
    status: UserStatus
    created_at: datetime
    
    def is_active(self) -> bool:
        return self.status == UserStatus.ACTIVE
    
    def ban_user(self, reason: str) -> None:
        if not self.is_active():
            raise ValueError(f"User already {self.status.value}")
        self.status = UserStatus.BANNED

@dataclass
class Order:
    """Domain Entity"""
    id: str
    user_id: str
    items: list['OrderItem']
    total_price: float
    created_at: datetime
    
    def add_item(self, item: 'OrderItem') -> None:
        if item.quantity <= 0:
            raise ValueError("Quantity must be positive")
        self.items.append(item)
    
    def calculate_total(self) -> float:
        return sum(item.price * item.quantity for item in self.items)

Value Objects (Значимые объекты)

# domain/models/value_objects.py
from dataclasses import dataclass
from typing import Pattern
import re

@dataclass(frozen=True)
class Email:
    """Value Object - неизменяем, сравнивается по значению"""
    value: str
    
    def __post_init__(self):
        pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if not re.match(pattern, self.value):
            raise ValueError(f"Invalid email: {self.value}")

@dataclass(frozen=True)
class Money:
    """Value Object для денег"""
    amount: float
    currency: str
    
    def __post_init__(self):
        if self.amount < 0:
            raise ValueError("Amount cannot be negative")
        if self.currency not in ['USD', 'EUR', 'RUB']:
            raise ValueError(f"Unsupported currency: {self.currency}")
    
    def add(self, other: 'Money') -> 'Money':
        if self.currency != other.currency:
            raise ValueError("Cannot add different currencies")
        return Money(self.amount + other.amount, self.currency)

# Использование
email = Email("user@example.com")
price = Money(99.99, "USD")

Repository интерфейсы

# domain/repositories/user_repository.py
from abc import ABC, abstractmethod
from typing import Optional, List
from domain.models import User

class IUserRepository(ABC):
    """Interface - не зависит от реализации"""
    
    @abstractmethod
    async def get_by_id(self, user_id: str) -> Optional[User]:
        """Получить пользователя по ID"""
        pass
    
    @abstractmethod
    async def get_by_email(self, email: str) -> Optional[User]:
        """Получить пользователя по email"""
        pass
    
    @abstractmethod
    async def save(self, user: User) -> None:
        """Сохранить пользователя"""
        pass
    
    @abstractmethod
    async def delete(self, user_id: str) -> None:
        """Удалить пользователя"""
        pass

Domain Services

# domain/services/user_service.py
from domain.repositories import IUserRepository
from domain.models import User, UserStatus
from datetime import datetime

class UserDomainService:
    """Pure бизнес-логика, без side effects"""
    
    def __init__(self, user_repo: IUserRepository):
        self.user_repo = user_repo
    
    async def create_user(self, email: str, password: str) -> User:
        """Создать пользователя с проверками"""
        # Проверяем уникальность
        existing = await self.user_repo.get_by_email(email)
        if existing:
            raise ValueError(f"User {email} already exists")
        
        # Создаём сущность с бизнес-правилами
        user = User(
            id=str(uuid4()),
            email=email,
            password_hash=hash_password(password),  # Безопасность
            status=UserStatus.ACTIVE,
            created_at=datetime.utcnow()
        )
        
        return user
    
    async def deactivate_user(self, user_id: str) -> None:
        """Деактивировать пользователя"""
        user = await self.user_repo.get_by_id(user_id)
        if not user:
            raise ValueError(f"User {user_id} not found")
        
        # Бизнес-правило
        if user.status == UserStatus.DELETED:
            raise ValueError("Cannot deactivate deleted user")
        
        user.ban_user("Deactivation")
        await self.user_repo.save(user)

2. Application слой (Use Cases)

# application/use_cases/create_order.py
from dataclasses import dataclass
from typing import List
from domain.models import Order, OrderItem
from domain.repositories import IOrderRepository, IUserRepository

@dataclass
class CreateOrderRequest:
    user_id: str
    item_ids: List[str]

@dataclass
class CreateOrderResponse:
    order_id: str
    total_price: float

class CreateOrderUseCase:
    """Orchestrator для бизнес-процесса"""
    
    def __init__(
        self,
        order_repo: IOrderRepository,
        user_repo: IUserRepository,
        product_service: 'IProductService'  # Еще одна абстракция
    ):
        self.order_repo = order_repo
        self.user_repo = user_repo
        self.product_service = product_service
    
    async def execute(self, request: CreateOrderRequest) -> CreateOrderResponse:
        # 1. Проверяем пользователя
        user = await self.user_repo.get_by_id(request.user_id)
        if not user or not user.is_active():
            raise ValueError("User not active")
        
        # 2. Получаем товары
        items = []
        for item_id in request.item_ids:
            product = await self.product_service.get_product(item_id)
            if not product:
                raise ValueError(f"Product {item_id} not found")
            items.append(OrderItem(product.id, product.price, 1))
        
        # 3. Создаём заказ (Domain Logic)
        order = Order(
            id=str(uuid4()),
            user_id=request.user_id,
            items=items,
            total_price=sum(i.price for i in items),
            created_at=datetime.utcnow()
        )
        
        # 4. Сохраняем (Infrastructure)
        await self.order_repo.save(order)
        
        # 5. Возвращаем ответ
        return CreateOrderResponse(
            order_id=order.id,
            total_price=order.total_price
        )

3. Infrastructure слой

# infrastructure/repositories/sqlalchemy_user_repository.py
from sqlalchemy.orm import Session
from sqlalchemy import select
from typing import Optional
from domain.models import User
from domain.repositories import IUserRepository
from infrastructure.database.models import UserORM

class SQLAlchemyUserRepository(IUserRepository):
    """Реализация Repository для PostgreSQL"""
    
    def __init__(self, session: Session):
        self.session = session
    
    async def get_by_id(self, user_id: str) -> Optional[User]:
        stmt = select(UserORM).where(UserORM.id == user_id)
        user_orm = self.session.execute(stmt).scalar()
        
        if not user_orm:
            return None
        
        # Маппинг ORM → Domain
        return self._to_domain(user_orm)
    
    async def save(self, user: User) -> None:
        # Маппинг Domain → ORM
        user_orm = self._to_orm(user)
        self.session.merge(user_orm)
        self.session.commit()
    
    def _to_domain(self, orm: UserORM) -> User:
        """ORM модель → Domain модель"""
        return User(
            id=orm.id,
            email=orm.email,
            password_hash=orm.password_hash,
            status=UserStatus(orm.status),
            created_at=orm.created_at
        )
    
    def _to_orm(self, user: User) -> UserORM:
        """Domain модель → ORM модель"""
        return UserORM(
            id=user.id,
            email=user.email,
            password_hash=user.password_hash,
            status=user.status.value,
            created_at=user.created_at
        )

4. Presentation слой (FastAPI)

# presentation/api/orders.py
from fastapi import APIRouter, Depends
from pydantic import BaseModel
from application.use_cases import CreateOrderUseCase
from infrastructure.repositories import SQLAlchemyUserRepository, SQLAlchemyOrderRepository

router = APIRouter()

class CreateOrderRequest(BaseModel):
    item_ids: list[str]

class OrderResponse(BaseModel):
    order_id: str
    total_price: float

@router.post("/orders", response_model=OrderResponse)
async def create_order(
    request: CreateOrderRequest,
    user_id: str = Depends(get_current_user_id),
    use_case: CreateOrderUseCase = Depends(get_create_order_use_case)
):
    result = await use_case.execute(
        CreateOrderRequest(user_id, request.item_ids)
    )
    return OrderResponse(
        order_id=result.order_id,
        total_price=result.total_price
    )

5. Dependency Injection

# main.py или app.py
from fastapi import FastAPI
from sqlalchemy.orm import Session
from infrastructure.database import get_db
from infrastructure.repositories import SQLAlchemyUserRepository
from application.use_cases import CreateOrderUseCase

app = FastAPI()

def get_create_order_use_case(db: Session = Depends(get_db)) -> CreateOrderUseCase:
    user_repo = SQLAlchemyUserRepository(db)
    order_repo = SQLAlchemyOrderRepository(db)
    product_service = ProductService()  # Может быть внешний сервис
    
    return CreateOrderUseCase(order_repo, user_repo, product_service)

# Все зависимости разрешаются фреймворком

Структура проекта

myproject/
├── domain/                    # ← Чистая бизнес-логика
│   ├── models/
│   │   ├── user.py
│   │   ├── order.py
│   │   └── value_objects.py
│   ├── repositories/          # ← Интерфейсы (Abstract)
│   │   ├── user_repository.py
│   │   └── order_repository.py
│   └── services/              # ← Domain Services
│       └── user_service.py
│
├── application/               # ← Orchestration, Use Cases
│   └── use_cases/
│       ├── create_order.py
│       ├── get_user.py
│       └── update_profile.py
│
├── infrastructure/            # ← Технические детали
│   ├── repositories/          # ← Конкретные реализации
│   │   ├── sqlalchemy_user_repo.py
│   │   └── sqlalchemy_order_repo.py
│   ├── database/
│   │   ├── models.py          # ← ORM модели (SQLAlchemy)
│   │   └── session.py
│   └── external_services/
│       └── payment_gateway.py
│
├── presentation/              # ← API контроллеры
│   ├── api/
│   │   ├── orders.py
│   │   ├── users.py
│   │   └── products.py
│   └── schemas.py             # ← Pydantic models
│
└── main.py

Преимущества DDD

  • Чистая архитектура — легко тестировать domain без БД
  • Бизнес-логика защищена — domain не зависит от infrastructure
  • Масштабируемость — легко добавлять новые use cases
  • Сотрудничество — разработчики и бизнес говорят на одном языке
  • Замена реализаций — смена БД или API провайдера не требует изменений domain

Когда DDD оправдан

  • Сложная бизнес-логика (финтех, e-commerce, кор-системы)
  • Большая команда (>5 разработчиков)
  • Long-term проект (не MVP)
  • Критичное качество и надежность

Когда DDD избыточен

  • Простой CRUD (блог, каталог)
  • One-man проект
  • MVP с неясными требованиями

DDD — не серебряная пуля, а инструмент для сложных систем. Используй когда чувствуешь боль от хаоса.