The first repositories in cloudapps combined interface and implementation in one class. user_with_email wasn’t a contract - it was a filter expression backed by a concrete storage mechanism. A UserRepository defined what queries existed and how they ran, all in the same inheritance chain.
That worked until User.change_email needed to verify that no other account already held a given email address. The check is a repository lookup. But the repository lived in the persistence layer, and having an entity import from there would flip the dependency - domain depending on infrastructure.
So the interface moved. Not into a separate file in the same layer, but into the domain module itself, as a Protocol next to the entity it serves.
One direction
class UserRepository(Protocol):
def user_with_email_exist(self, email: str) -> bool: ...
def user_with_email(self, email: str) -> User: ...
def save(self, user: User) -> None: ...
def find(self, user_id: str) -> User: ...
Python
This lives in idm/domain/user.py, right below User. The method names are domain vocabulary - user_with_email is a concept the domain needs, not a query someone decided to expose. The concrete SQLiteUserRepository imports this Protocol from the domain and satisfies it. The domain doesn’t know the implementation exists.
With the Protocol available, User.change_email validates uniqueness without touching infrastructure:
def change_email(self, new_email: str, user_repository: UserRepository) -> None:
normalized_email = new_email.lower().strip()
if user_repository.user_with_email_exist(normalized_email):
raise DuplicateEmailError(normalized_email)
self.email = normalized_email
Python
The entity calls a repository it defined. Testing it means passing a stub - no database, no setup. Commands receive all their repositories through a context object typed against Protocols, wired by a factory they never see.
cloudapps has 25 repositories following this shape. The concrete implementations delegate to a shared base class that handles JSON serialization and optimistic locking. A smaller project handling email attachments reached the same structure independently with two hand-rolled repositories and inline serialization. The infrastructure is completely different. The boundary is identical: the domain defines the contract, the implementation satisfies it from the outside, and imports flow in one direction.