Идея у вас верная, только вместо защищенного onDelete, делают обычный публичный метод remove, который ничего не делает, а просто выбрасывает доменное событие. Этот метод как и все остальные просто вызывается внутри application сервиса, а затем удаляется из репозитория.
$catalog->remove();
$repository->remove($catalog);
Чтобы была атомарность, нужно события выбрасывать после того, как изменения были сделаны в базе данных. Само событие нужно также в базу сохранять, в одной транзакции с созданием/обновлением/удалением данных об агрегате. Какая-то штука затем должна удалять обработанные события. Какая-то штука должна проталкивать события из базы, которые почему-то там уж слишком долго живут - подписчикам. Обычно происходит при сбое, когда в базу событие записалось, а подписчикам не отправилось.
Какая-то сложная замороченная хрень с консистентностью по событию получается. Еще бывает, что тот кто принял событие не может его обработать, так как нарушается какой-нибудь инвариант в его доменной модели и шлет событие о неудаче, на которую должен отреагировать тот от кого пришло событие этому. Больше трех шагов и совершенно непонятно, какие события куда летают, кто на них реагирует и что вообще происходит в такой системе.
Поэтому я бы попробовал Saga Orchestration, это такая штука, в которой пошагово описывается какие сервисы нужно вызвать, чтобы успешно завершить бизнес-операцию, а также какие действия нужно выполнить на каждом шаге в случае сбоя одного из сервисов участвующих в бизнес-операции для того, чтобы вернуть всю систему в консистентное состояние.