По моему опыту, это единственный способ сделать что-то. Если вы напишите систему, которая пытается полностью скрыть или абстрагировать слой персистентности, то вы не сможете оптимизировать вещи, используя специфику слоя персистентности.
Недавно я столкнулся с этой проблемой и работал над решением, где постоянные уровни могут выбирать реализацию интерфейсов, представляющих оптимизацию. Я только что поиграл с ним, но, используя ваш пример ListAUsers, он выглядит следующим образом ...
Сначала напишите метод ListAllUsers, который делает все на уровне домена. Какое-то время это сработает, потом начнёт работать слишком медленно.
Когда модель расширенного домена становится медленной, создайте интерфейс под названием «IListActiveUsers» (или, возможно, что-то лучше). И пусть ваш код персистентности реализует этот интерфейс, используя подходящие методы (возможно, оптимизированный SQL).
Теперь вы можете написать слой, который проверяет эти интерфейсы и вызывает определенный метод, если он существует.
Это не идеально, и у меня нет большого опыта в подобных вещах. Но мне кажется, что ключ состоит в том, чтобы гарантировать, что, если вы используете абсолютно наивный метод персистентности, весь код все еще должен работать. Любая оптимизация должна быть сделана как дополнение к этому.