Надеюсь пригодится или для истории.
Вопрос с валидацией решил следующим образом. В нашем проекте фронтенд и бекенд взаимодействуют через команды (ф -> б). В любом проекте передача данных это некая структура и адреса приёма. В контроллере происходит сборка данных и на её основе формируется команда. Команда - это read-only объект после своего создания. Команда на данный момент отправляется сразу в сервис, но! по хорошему её нужно отправлять в сервис через шину, где на уровне шины делать авторизацию/аутентификацию, валидацию и любой другой аспект.
В сервисе первым делом валидируется команда. Проверяются те ли типы данных соответствуют полям, правильны ли поля с точки зрения бизнес-логики, обязательность заполнения и проч. Валидатор возвращает Result, который содержит массив найденных ошибок. Result также immutable. На этом этапе никаких эксепшенов нет и быть не должно, в этом суть валидации понять что не так и выдать наиболее полный ответ. Если result->isValid() == false, то тогда выбрасываем исключение, в которое передаём список ошибок result->getErrors(), но тут могут быть вариации, зависит от требований. Если валидация прошла успешно выполняем код сервиса (бизнес-логику запрошенного действия).
Во время выполнения кода в используемых методах сущностей и сервисов также должны быть проверки на корректность данных и если ошибка дошла до этого уровня (после валидации), то здесь уже выбрасывается исключение, тем самым сообщая нам, что обнаружился новый неучтённый кейс. И это является толчком для внесения изменений в валидатор, т.е. добавление нового проверочного правила.
На фронте обработку может проводить так. Если форма, то привязать ошибки к соответствующим полям. Если это некое действие (например, перерасчёт данных, импорт/экспорт), то сделать popup с выводом списка всех ошибок.
Итог. Валидация - это многогранный и многоуровневый процесс. Проверить все правила изнутри сущности не возможно. Может быть валидация данных на уровне сущности, на уровне коллекции, на уровне коллекции коллекций, всего проекта целиком и даже с внешними данными и проч. Отталкиваясь от этого одним из верных решений является внешний сервис (класс) валидации, который принимает все зависимости, и выполняет своё назначение. Одним из возможных минусов является дублирование кода, но этого можно обернуть в плюс, мы получаем те участки кода, которые стоит декомпозировать в отдельные функции или классы. Важный плюс отдельного класса валидации: в один момент над сервисом могут работать сразу 2 человека, один пишет саму бизнес-логику, а другой валидацию. Более того валидацию можно написать и позже, да код будет вылетать на исключениях, ну и ладно, для прототипов и начальных версий это вполне допустимо.