Управление транзакцией может брать на себя как сам юзкейс, так и вызывающий код. Для гибкости рекомендуется проектировать юзкейсы транзакционно-нейтральными: если выполнение уже происходит в рамках активной транзакции, используется она, иначе открывается новая. Благодаря этому бизнес-логика не зависит от того, кто отвечает за её жизненный цикл.

Одним из вариантов реализации такого подхода является использование TransactionManager. В приведённом ниже примере для Laravel различаются два состояния: isScoped() определяет, выполняется ли код внутри какой-либо транзакции базы данных, а isManaged() — находится ли код внутри области ответственности менеджера. Это позволяет разделять операции, которым достаточно любой активной транзакции, и операции, требующие управления именно им.

Если TransactionManager хранит внутреннее состояние (например, уровень вложенности или зарегистрированные callback’и), его следует регистрировать в контейнере Laravel как scoped(), а не singleton(). Это гарантирует, что каждый HTTP-запрос или задача очереди получат собственный экземпляр менеджера, а в долгоживущих окружениях, таких как Laravel Octane, внутреннее состояние не будет сохраняться между жизненными циклами приложения.

Пример кода: https://pastebin.com/F0rwBM9m

Управление транзакцией может брать на себя как сам юзкейс, так и вызывающий код | Сетка — социальная сеть от hh.ru