Лекция
Вопрос о том, где управлять транзакциями в архитектуре, построенной по принципам Domain-Driven Design, возникает практически у каждого разработчика, когда он начинает отходить от простого CRUD-подхода и строить модель предметной области. На первый взгляд может показаться, что транзакция — это просто техническая деталь работы с базой данных, и значит ей место в репозитории, ведь именно он занимается сохранением данных. Однако в DDD транзакция — это не только технический механизм, а прежде всего граница целостности бизнес-операции, и именно это определяет ее правильное расположение в архитектуре.
В DDD система делится на несколько уровней: доменная модель, инфраструктура и прикладной (сервисный) слой. Репозиторий относится к инфраструктуре и выполняет строго ограниченную роль — он предоставляет интерфейс для сохранения и получения агрегатов, скрывая детали хранения. Он не знает, что за бизнес-операция происходит, не понимает контекст use-case и не принимает решений о том, какие изменения должны быть атомарными. Его задача — взять агрегат и записать его в хранилище.
Сервисный слой, напротив, как раз и существует для описания сценариев использования системы. Именно здесь определяется, что пользователь делает с точки зрения бизнеса: оформляет заказ, переводит деньги, регистрируется, обновляет профиль. Эти сценарии часто включают несколько шагов, затрагивают несколько агрегатов и требуют, чтобы вся операция либо завершилась полностью, либо не произошла вовсе. Другими словами, именно здесь появляется понятие атомарности как бизнес-требования. Поэтому границы транзакции естественным образом совпадают с границами use-case, а значит управляться они должны на уровне сервисов.
Когда транзакция помещается внутрь репозитория, она начинает отражать не бизнес-операцию, а отдельное техническое действие — например, один вызов метода save. В простых случаях это не создает проблем, если речь идет о сохранении одного агрегата целиком. Представим, что пользователь меняет свой email. Вся операция ограничена одним агрегатом User, и сохранение этого агрегата действительно может быть выполнено атомарно внутри метода репозитория. В таком сценарии локальная транзакция внутри репозитория не нарушает модель, потому что граница агрегата и граница транзакции совпадают.
Однако как только use-case затрагивает более одного агрегата, ситуация меняется. Например, создание заказа и списание средств со счета — это уже две разные сущности предметной области, два агрегата, каждый из которых имеет собственный репозиторий. Если каждый репозиторий будет открывать и завершать транзакцию самостоятельно, то бизнес-операция распадется на две независимые транзакции. В случае ошибки между ними система окажется в неконсистентном состоянии: заказ может быть создан, а деньги не списаны, или наоборот. Это уже нарушение инвариантов предметной области, и исправить его задним числом намного сложнее, чем изначально правильно определить границы транзакции.

Именно поэтому в классическом DDD транзакции рассматриваются как часть прикладного слоя. Сервис открывает транзакцию, вызывает необходимые методы доменной модели и репозиториев и затем либо фиксирует изменения, либо откатывает их при ошибке. В такой схеме репозитории остаются «тупыми» с точки зрения бизнес-логики, а сервис управляет целостностью операции. Это делает систему предсказуемой и облегчает поддержку, потому что любой разработчик может посмотреть на сервис и сразу понять, где начинается и где заканчивается атомарная бизнес-операция.
Иногда путаница возникает из-за того, что современные ORM и библиотеки реализуют паттерн Unit of Work и сами управляют транзакциями при сохранении сущностей. Это может создавать ощущение, что транзакции «живут» в репозитории. На практике же это лишь техническая реализация механизма фиксации изменений, а решение о том, когда начинать и завершать транзакцию, по-прежнему должно приниматься на уровне сервиса или приложения. Инфраструктура может помогать, но не должна определять границы бизнес-операции.
В DDD (Domain-Driven Design) транзакции — это не про хранение данных, а про управление консистентностью бизнес-операций. Поэтому правильное место для них — это уровень приложения (Application / Service Layer), а не слой репозиториев.
Транзакции следует открывать и управлять ими на уровне сервисного слоя (Application Service)
Не в репозиториях
В DDD есть разделение ответственности:
Отвечает только за доступ к данным
Не знает ничего о бизнес-операциях
Не должен управлять транзакциями
Его задача: save(), find(), delete()
Оркестрирует бизнес-операцию
Вызывает несколько репозиториев
Управляет жизненным циклом транзакции
Именно здесь понятно:
где начинается бизнес-операция
где она заканчивается
что должно быть атомарным
Допустим, есть use-case:
“Создать заказ и списать деньги со счета”
Это одна бизнес-операция → одна транзакция.
class OrderService {
public function createOrder(CreateOrderCommand $cmd) {
$this->transactionManager->begin();
try {
$order = Order::create($cmd->data);
$this->orderRepository->save($order);
$this->paymentService->charge($cmd->userId, $cmd->amount);
$this->transactionManager->commit();
} catch (\Throwable $e) {
$this->transactionManager->rollback();
throw $e;
}
}
}
class OrderRepository {
public function save(Order $order)
{
$this->beginTransaction(); // плохо
// save
$this->commit();
}
}
Почему плохо:
если нужно сохранить 2 агрегата, будет 2 разные транзакции
нарушается атомарность
невозможно управлять целостностью бизнес-операции
В DDD есть правило:
Одна транзакция = один агрегат (в идеале)
Но на практике:
иногда операция затрагивает несколько агрегатов
тогда сервис координирует все в одной транзакции
Иногда допустимы:
реализуется инфраструктурно
но используется из сервисного слоя
тогда транзакция ограничена одним агрегатом
остальное через события

допустимо, если:
операция работает только с одним агрегатом
и это атомарный save()
например:
UserRepository::save(user)
НО как только:
несколько агрегатов
несколько репозиториев
бизнес-процесс
транзакция должна быть выше — в сервисе
Давай разберем на конкретном примере, чтобы стало максимально понятно.
В DDD агрегат (Aggregate) — это кластер сущностей с одним корнем (Aggregate Root), который гарантирует консистентность.
Пример агрегата:
Order (Aggregate Root) ├── OrderItem ├── ShippingAddress └── PaymentInfo
Все это — ОДИН агрегат Order
Это значит:
Ты сохраняешь один агрегат целиком
и это происходит одним действием (одной транзакцией)
Пользователь обновляет свой профиль
Это:
1 агрегат → User
одна операция → save(user)
class User {
private string $email;
private string $name;
public function changeEmail(string $email) {
// domain rules
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new DomainException("Invalid email");
}
$this->email = $email;
}
}
class UserRepository {
public function save(User $user) {
$this->db->transaction(function() use ($user) {
// UPDATE users SET ...
});
}
}
тут все нормально, потому что:
сохраняется один агрегат
операция атомарная
нет координации с другими репозиториями
class UserService {
public function changeEmail(int $userId, string $email)
{
$user = $this->repo->find($userId);
$user->changeEmail($email);
$this->repo->save($user);
}
}
Создать заказ и списать деньги
Это уже:
агрегат Order
агрегат Account
два агрегата
$orderRepo->save($order); // транзакция 1 $accountRepo->withdraw($money); // транзакция 2
если второе упадет — заказ уже создан
$this->transaction->begin(); $orderRepo->save($order); $accountRepo->withdraw($money); $this->transaction->commit();
транзакция на уровне сервиса
если:
затрагивается 1 агрегат
это одна операция сохранения
если:
несколько агрегатов
несколько репозиториев
бизнес-процесс (use-case)
UserRepository::save(user)
OrderRepository::save(order)
CartRepository::save(cart)
создать заказ + списать деньги
зарегистрировать пользователя + отправить email + создать профиль
перевести деньги между счетами
если операция — это просто сохранение одного агрегата как единого целого,
то транзакция может быть инкапсулирована внутри репозитория
Но:
если операция — это бизнес-процесс,
то транзакция должна быть на уровне сервисного слоя
✔ Транзакции в DDD размещаются на уровне сервисов (Application Layer)
✔ Репозитории должны быть тупыми (CRUD)
✔ Сервис управляет атомарностью бизнес-операции
Таким образом, ответ на вопрос о том, где размещать транзакции, зависит от того, что именно мы хотим защитить их границами. Если речь идет о простом сохранении одного агрегата, допустимо инкапсулировать техническую транзакцию внутри репозитория. Но как только появляется полноценный бизнес-сценарий, включающий несколько действий или агрегатов, транзакция должна подниматься на уровень сервисного слоя и охватывать весь use-case целиком. Это соответствует главному принципу DDD: технические детали подчиняются модели предметной области, а не наоборот.
В итоге можно сформулировать простое и практичное правило: транзакция должна совпадать с границей бизнес-операции. А место, где описывается бизнес-операция в DDD, — это сервисный слой. Поэтому именно там транзакции и должны управляться в большинстве реальных систем.
Комментарии
Оставить комментарий
Объектно-ориентированное программирование ООП
Термины: Объектно-ориентированное программирование ООП