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.