I don't understand your question entirely, but there seems to be some confusion on how to use repositories. Answering that may help you find the right way.
Let me answer your question in two parts: where do repositories fit in, and how to use queries represent domain concepts.
Repositories are not part of the Domain layer. They belong outside in the Application layer.
A typical transaction flow would be something like this:
- UI sends a request to API
- API Controller gathers request params and
invokes the Application Service
- Application Service gathers Repositories (Applications typically inject repositories at runtime based on configuration)
- Application Service loads Aggregates (domain objects) based on the request params with the help of Repositories
- Application Service invokes the methods on Aggregates to perform changes, if necessary
- Application Service persists the aggregates with the help of Repositories
- Application Service formats response and returns data to API Controller
So, you see, Application Service deals with repositories and aggregates. Aggregates, being in the domain layer, do not ever have to deal with Repositories.
A Query is best placed within the Repository because it is the responsibility of the Repository to interact with underlying data stores.
However, you should ensure that each Query represents a concept in the domain. It is generally not recommended to use filter params directly, because you don't capture the importance of the Query from the domain's point of view.
For example, if you are querying for, say, people who are adults (age > 21
), then you should have a Query object called Adults which holds this filter within it. If you are querying for, say, people are who are senior citizens (age > 60
), you should have a different Query object called Senior Citizen and so on.
For this purpose, you could use the Specification pattern to expose one GET API, but translate it into a Domain Specification Object
before passing it on to the Repository for querying. You typically do this transformation in your Controller
, before invoking the Application Service
.
Martin Fowler and Eric Evans have published an excellent paper on using Specifications: https://martinfowler.com/apsupp/spec.pdf
As the paper states, The central idea of Specification is to separate the statement of how to match a candidate, from the candidate object that it is matched against.
Note:
- Use the specification pattern for the Query side, but avoid reusing it in different contexts. Unless the Query represents the same domain concept, you should be creating a different specification object for each need. Also, DO NOT use a specification object on both the query side and command side, if you are using CQRS. You will be creating a central dependency between two parts, that NEED to be kept separate.
- One way to get the underlying domain concept is to evaluate your queries (getByAandB and getByAandC) and draw out the question you are asking to the domain (For ex., ask your domain expert to describe the data she is trying to fetch).
Repository Organization:
Apologies if this confuses you a bit, but the code is in Python. But it almost reads like pseudocode, so you should be able to understand easily.
Say, we have this code structure:
application
main.py
infrastructure
repositories
user
mongo_repository.py
postgres_repository.py
...
...
domain
model
article
aggregate.py
domain_service.py
repository.py
user
...
The repository.py
file under article
will be an abstract repository, with important but completely empty methods. The methods represent domain concepts, but they need to implemented concretely (I think this is what you are referring to in your comments).
class ArticleRepository:
def get_all_active_articles(...):
raise NotImplementedError
def get_articles_by_followers(...):
raise NotImplementedError
def get_article_by_slug(...):
raise NotImplementedError
And in postgres_repository.py
:
# import SQLAlchemy classes
...
# This class is required by the ORM used for Postgres
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
And this is a possible concrete implementation of the Factory, in the same file:
# This is the concrete repository implementation for Postgres
class ArticlePostgresRepository(ArticleRepository):
def __init__(self):
# Initialize SQL Alchemy session
self.session = Session()
def get_all_active_articles(self, ...):
return self.session.query(Article).all()
def get_article_by_slug(self, slug, ...):
return self.session.query(Article).filter(Article.slug == slug).all()
def get_articles_by_followers(self, ...):
return self.session.query(Article).filter(followee_id__in=...).all()
So in effect, the aggregate still does not know anything about the repository itself. Application services or configuration choose what kind of repository is to be used for a given environment dynamically (Maybe Postgres in Test and Mongo in Production, for example).