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

Что такое DDD (Domain-Driven Design)?

2.8 Senior🔥 141 комментариев
#REST API и микросервисы#SOLID и паттерны проектирования

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

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

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

Что такое DDD (Domain-Driven Design)

DDD (Domain-Driven Design) — это методология проектирования архитектуры приложений, которая фокусируется на глубоком понимании бизнес-логики (домена) и отражении этой логики в коде. Концепция была введена Эриком Эвансом в его книге "Domain-Driven Design: Tackling Complexity in the Heart of Software" (2003) и остаётся актуальной и в 2025 году.

Основная идея

Вместо того, чтобы проектировать систему через призму технических слоёв (БД, фреймворки, UI), DDD предлагает обратный подход:

  1. Понимаешь бизнес — какие процессы, правила, сущности
  2. Проектируешь домен — моделируешь эти процессы в коде
  3. Обворачиваешь технологиями — БД, фреймворки становятся деталями реализации
Традиционный подход:              DDD подход:
Database                          Business Logic (Domain)
    ↓                                    ↓
Data Models                        Domain Models (Entities, Value Objects)
    ↓                                    ↓
Services                          Services (Use Cases)
    ↓                                    ↓
Controllers                        Controllers (Adapters)

Ключевые понятия DDD

1. Entity (Сущность)

Объект с уникальным идентификатором, жизненный цикл которого важен:

public class Order {
    private OrderId id;  // Уникальный идентификатор
    private CustomerId customerId;
    private List<OrderItem> items;
    private OrderStatus status;
    private LocalDateTime createdAt;
    
    public Order(OrderId id, CustomerId customerId) {
        this.id = id;
        this.customerId = customerId;
        this.status = OrderStatus.PENDING;
        this.items = new ArrayList<>();
    }
    
    public void addItem(Product product, int quantity) {
        if (this.status != OrderStatus.PENDING) {
            throw new OrderAlreadyConfirmedException();
        }
        this.items.add(new OrderItem(product, quantity));
    }
    
    public OrderId getId() {
        return id;
    }
}

2. Value Object (Объект-значение)

Объект без идентификатора, определяемый своими атрибутами. Неизменяемый (immutable):

public class Money {
    private final BigDecimal amount;
    private final String currency;
    
    public Money(BigDecimal amount, String currency) {
        if (amount.compareTo(BigDecimal.ZERO) < 0) {
            throw new InvalidMoneyException("Amount cannot be negative");
        }
        this.amount = amount;
        this.currency = currency;
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new CurrencyMismatchException();
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Money money = (Money) o;
        return amount.equals(money.amount) && currency.equals(money.currency);
    }
}

3. Aggregate (Агрегат)

Группа связанных объектов (Entity + Value Objects), которые рассматриваются как одна единица. Имеет корневую сущность (Aggregate Root):

public class Order {  // Aggregate Root
    private OrderId id;
    private CustomerId customerId;
    private List<OrderItem> items;  // Value Objects
    private Money totalAmount;
    private OrderStatus status;
    
    // Order отвечает за консистентность всего агрегата
    public void addItem(Product product, int quantity) {
        // Проверяем правила бизнеса
        if (this.status != OrderStatus.PENDING) {
            throw new OrderAlreadyConfirmedException();
        }
        OrderItem item = new OrderItem(product, quantity);
        items.add(item);
        recalculateTotalAmount();
    }
    
    private void recalculateTotalAmount() {
        this.totalAmount = items.stream()
            .map(OrderItem::getPrice)
            .reduce(new Money(BigDecimal.ZERO, "USD"), Money::add);
    }
}

4. Repository (Репозиторий)

Абстракция для доступа к данным. Работает с Aggregates, скрывая детали БД:

public interface OrderRepository {
    void save(Order order);
    Order findById(OrderId id);
    void delete(OrderId id);
}

@Repository
public class JpaOrderRepository implements OrderRepository {
    @Autowired
    private JpaOrderEntity repo;
    
    @Override
    public void save(Order order) {
        JpaOrderEntity entity = OrderMapper.toPersistence(order);
        repo.save(entity);
    }
    
    @Override
    public Order findById(OrderId id) {
        return repo.findById(id.getValue())
            .map(OrderMapper::toDomain)
            .orElseThrow(() -> new OrderNotFoundException(id));
    }
}

5. Service (Сервис)

Бизнес-логика, которая не относится к одному Entity или Aggregate:

@Service
public class CreateOrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Transactional
    public OrderId createOrder(CustomerId customerId, List<CreateOrderItem> items) {
        Order order = new Order(new OrderId(UUID.randomUUID()), customerId);
        
        for (CreateOrderItem item : items) {
            Product product = productRepository.findById(item.getProductId())
                .orElseThrow(() -> new ProductNotFoundException());
            
            order.addItem(product, item.getQuantity());
        }
        
        orderRepository.save(order);
        return order.getId();
    }
}

6. Domain Event (Доменное событие)

Событие, которое произошло в домене и важно для бизнеса:

public class OrderCreatedEvent {
    private final OrderId orderId;
    private final CustomerId customerId;
    private final LocalDateTime occurredAt;
    
    public OrderCreatedEvent(OrderId orderId, CustomerId customerId) {
        this.orderId = orderId;
        this.customerId = customerId;
        this.occurredAt = LocalDateTime.now();
    }
    
    public OrderId getOrderId() { return orderId; }
    public CustomerId getCustomerId() { return customerId; }
}

public class Order {
    private List<DomainEvent> events = new ArrayList<>();
    
    public void create() {
        // ... создание логики
        events.add(new OrderCreatedEvent(this.id, this.customerId));
    }
    
    public List<DomainEvent> getAndClearEvents() {
        List<DomainEvent> result = new ArrayList<>(events);
        events.clear();
        return result;
    }
}

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

┌─────────────────────────────────┐
│  Presentation (Controllers, Views)
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│  Application (Use Cases, DTOs)  ← Оркестрирует домен
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│  Domain (Entities, Services)    ← Бизнес-логика
└─────────────────────────────────┘
           ↓
┌─────────────────────────────────┐
│  Infrastructure (DB, External) ← Реализация деталей
└─────────────────────────────────┘

Когда использовать DDD

Используй DDD когда:

  • Проект большой и сложный
  • Бизнес-логика нетривиальна
  • Много правил и ограничений
  • Нужна долгосрочная поддерживаемость
  • Команда готова инвестировать время в понимание домена

Не используй DDD когда:

  • Маленький MVP
  • Простой CRUD-функционал
  • Один человек на проекте
  • Нет возможности общаться с бизнесом

Плюсы и минусы

Плюсы:

  • Код отражает бизнес-логику
  • Легче общаться с не-программистами
  • Проще поддерживать сложные системы
  • Естественная модульность

Минусы:

  • Много кода (Entities, Value Objects, Services)
  • Кривая обучения
  • Overkill для простых проектов
  • Требует дисциплины от команды

DDD — это не про выбор technology stack, а про способ мышления о проблеме. Это инвестиция в качество кода на долгосрочной перспективе.