There's a sizable community of folks who use CQRS to implement their domains. My feeling is that, if the interface of your repository is analogous to the best practices used by them, that you won't go too far astray.
Based on what I've seen...
1) Command handlers usually use the repository to load the aggregate via a repository. Commands target a single specific instance of the aggregate; the repository loads the root by ID. There isn't, that I can see, a case where the commands are run against a collection of aggregates (instead, you would first run a query to get the collection of aggregates, then enumerate the collection and issue a command to each.
Therefore, in contexts where you are going to be modifying the aggregate, I would expect the repository to return the entity (aka the aggregate root).
2) Query handlers don't touch the aggregates at all; instead, they work with projections of the aggregates -- value objects that describe the state of the aggregate/aggregates at some point in time. So think ProjectionDTO, rather than AggregateDTO, and you have the right idea.
In contexts where you are going to be running queries against the aggregate, preparing it for display, and so on, I'd expect to see a DTO, or a DTO collection, returned, rather than an entity.
All of your getCustomerByProperty
calls look like queries to me, so they would fall into the latter category. I'd probably want to use a single entry point to generate the collection, so I would be looking to see if
getCustomersThatSatisfy(Specification spec)
is a reasonable choice; the query handlers would then construct the appropriate specification from the parameters given, and pass that specification to the repository. The downside is that signature really suggests that the repository is a collection in memory; it's not clear to me that the predicate buys you much if the repository is just an abstraction of running a SQL statement against a relational database.
There are some patterns that can help out, though. For example, instead of building the specification by hand, pass to the repository a description of the constraints, and allow the implementation of the repository to decide what to do.
Warning: java like typing detected
interface CustomerRepository {
interface ConstraintBuilder {
void setLastName();
void setFirstName();
}
interface ConstraintDescriptor {
void copyTo(ConstraintBuilder builder);
}
List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor);
}
SQLBackedCustomerRepository implements CustomerRepository {
List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
WhereClauseBuilder builder = new WhereClauseBuilder();
descriptor.copyTo(builder);
Query q = createQuery(builder.build());
//...
}
}
CollectionBackedCustomerRepository implements CustomerRepository {
List<CustomerProjection> getCustomersThatSatisfy(ConstraintDescriptor descriptor) {
PredicateBuilder builder = new PredicateBuilder();
descriptor.copyTo(builder);
Predicate p = builder.build();
// ...
}
class MatchLastName implements CustomerRepository.ConstraintDescriptor {
private final lastName;
// ...
void copyTo(CustomerRepository.ConstraintBuilder builder) {
builder.setLastName(this.lastName);
}
}
In conclusion: the choice between providing an aggregate and providing a DTO depends on what you are expecting the consumer to do with it. My guess would be one concrete implementation supporting an interface for each context.