Search code examples
symfonydoctrine-ormtransactions

Doctrine Transactional Boundary


When calling this code, it will trigger the postPersist event of the Doctrine ORM. I have an event listener for that event, that will emit the events of the aggregate to a local (sync) event bus and store events that are flagged as integration events in an outbox table.

All of this must happen within one transaction. My question is if this code will work as I expect it? The actual aggregate should be persisted and the events in the outbox within the same transaction.

public function persist(Aggregate $aggregate): void
{
    $this->getEntityManager()->getConnection()->beginTransaction();

    try {
        $this->getEntityManager()->persist($aggregate);
        $this->getEntityManager()->flush();
        $this->getEntityManager()->getConnection()->commit();
    } catch (Exception $e) {
        $this->getEntityManager()->getConnection()->rollBack();
        throw $e;
    }
}

The diagram below shows the whole architecture. The state of the current transaction within the application must be guaranteed before the integration events are emitted.

enter image description here


Solution

  • The Doctrine postPersist event will be dispatched when you call the flush() method. If you're performing another persist/flush to store the events in the outbox table (a piece of code that you didn't show us, so I'm guessing here), then you'll trigger a nested transaction. In that case, the whole operation can be seen as a single transaction if you're not creating any savepoint in between.


    However, you should be careful about transitive operations being performed during the postPersist listener, because your original transaction is still open at that moment and it's wrapped in a try/catch. If any error occurs, the whole transaction will be rolled back (including the parent one in case of nesting transactions).

    I said that because I see you're using a domain event bus configured under an in-memory sync transport. By doing that, your domain event subscribers will be executed immediately in the same PHP thread, and they will be wrapped into the same transaction too. That's a design problem in my opinion, and you might face unexpected rollbacks due to any subscriber failure.

    To avoid that, I'd suggest changing the event bus configuration to use an async transport instead, decoupling your aggregate persistence operation from any domain event subscriber. If you really want to do that then it's probably useless to implement a bus pattern in this scenario, the Symfony EventDispatcher component could be enough.


    As an optimization note, the current function persist(Aggregate $aggregate) doesn't need a try/catch if you don't include any other persistence operations in between. The flush() method already creates a transaction and a try/catch for you. So, any failure happening in your Doctrine listeners will be caught, and the transaction will be rolled back.

    It behaves exactly like this:

    public function persist(Aggregate $aggregate): void
    {
        $this->getEntityManager()->persist($aggregate);
        $this->getEntityManager()->flush();
    }