A Messenger middleware is where the transaction belongs

Wrapping a handler in a transaction inside the handler couples the persistence decision to the business logic; a middleware applies it uniformly.

// config/packages/messenger.yaml
framework:
  messenger:
    buses:
      command.bus:
        middleware:
          - doctrine_transaction
          - validation

// the ordering matters: validation before the transaction
// means a rejected message never opens one.

The ordering is the part that is easy to get wrong and hard to notice, because both orders work — one of them just holds a transaction open through validation, which on a busy bus is measurable lock time for no reason. A middleware also gives one place to add the retry classification, so the decision about which exceptions are retryable is not repeated in every handler.