How to define a repository interface
eddiemf opened this issue · comments
Hello there Bill.
First I would like to thank you for this awesome video and repo you came with, it opened my mind in ways I can't even express and I'm learning a lot with it. Now to the question.
I'm building a more complex app using a very similar approach but using typescript.
Simplifying the whole thing, I have an entity User
which declares a dependency on a UserRepository
that will be used to check if the given email is unique across the database on creation (which is an enterprise business rule, therefore needs to be here). This UserRepository
interface has a findByEmail
method that returns a simple user data structure (name, email and password).
So far so good, but I have one use case ShowUser
which will be used to display the user information with some extra data. This use case should use the same UserRepository
(defined in the entity), but the return value of findByEmail
now should return a slightly different user data structure, because now I want it to include the user boards, which you can think of as a join of 2 tables.
Now comes the whole problem, with a lot of solutions which seem bad to me.
Solution 1: Easily fix the problem by going to the User
entity and altering the user data structure to include a boards
field. But now I modified the data structure with an application business rule in a place that should only be concerned with enterprise business rules.
Solution 2: Create another method in the interface called maybe findByEmailWithBoards
, which sounds horrible just by the name (what if I need more fields?) and also has the same problem as solution 1.
Solution 3: Make the repository simpler, returning a simple user data structure and creating a new BoardsRepository
to create a "manual join" of both data structures. This sounds a bit better but I completely lose the speed and convenience of joining multiple tables (or even having embedded documents in mongodb) that databases offer, leading me to have to make multiple queries (which can grow a lot) instead of 1, which will probably have a huge impact on performance in certain cases.
Solution 4: Having one UserRepository
interface in the User
entity and another for the use case, which then the implementation would implement both, satisfying both needs. But that sounds very weird for me, especially on how would I name them to differentiate between them.
There are probably more solutions that I came about but all of them seem to have a big drawback like these, so I'm not sure if this is gonna have to be a trade off or maybe I'm missing something.
This approach is a bit different than yours since you don't use the repository/database inside your entities, but there's definitely gonna be a point where you'll have to if you want to have more complex business rules.
So what to do when your application business rules conflict with your enterprise ones like here?
Thanks again and sorry for the long question! I hope you have the time to discuss this because I'm kinda out of ideas for now and I'm really hopping to get to something solid.
Hi @eddiemf thanks for taking the time to provide some feedback and for asking an interesting question. Sorry for the delay in responding; holidays have been hectic.
If I understood correctly, there are 2 rules/scenarios that we want to deal with in a "Clean" way.
- Ensure that a user's email is unique across the entire user data store.
- Have the option to return a collections of "boards" along with a user.
Here's how we could handle each:
Ensure that a user's email is unique across the entire user data store.
The User
Entity does not need any dependency on the UserRepository
for this. This rule belongs in the Use Case layer since it deals with the interaction between two or more entities (or in this case, several instances of a single Entity type). We should create a Use Case called AddUser
in which we would inject our user repository and query for a matching email before inserting a new user. Something like:
// make-add-user.js
const User = require('../entities/user')
module.exports = function makeAddUser({userRepo}) {
return function addUser({id, email, ...userInfo}) {
// other code
const foundUsers = await userRepo.findByEmail({email})
if(foundUsers && foundUsers.length > 0) {
throw new Error('User must have a unique email address') // this could be a custom error instead
}
// more code...
}
}
Have the option to return a collections of "boards" along with a user.
Assuming that your use case is only about reading boards for a given user there are typically 2 options here.
The first is what you described as "Solution 3" above. In my experience, especially in a Monolithic style app this type of solution will not cause any noticeable drag on performance (assuming we've enabled connection pooling and we have a decent DB server that is sitting close to our app). Under that scenario we have a BoardRepository
that exposes a findByUser
method and we have a Use Case called getUserWithBoards
that first queries for a user and then queries for all of that user's boards. This is what I would go with first, especially if there is the potential to want to paginate boards (e.g. if a user can have 100s of associated boards).
If it turns out that this is indeed too slow for our needs (after profiling the app and proving that it really is a bottleneck), the second option is to create what is sometimes called a "Read Model" or "Query Model" which is an idea that comes from the CQRS architecture pattern. A Read Model is a special entity designed to support a use case where we need multiple entities to be combined for read-only purposes and we want to take advantage of DB performance. So, now we create a new entity that composes together a User
and a collection of Boards
depending on our business context this new entity can be called UserDashboard
, UserProfile
, ExpandedUser
or whatever adequately describes the reason for this use case. Then in our UserRepository
we add a findUserDashboard
method (or findUserProfile, or findExpandedUser or whatever). This pattern fits especially well with Document Databases where we can easily persist the Read Model in a single Document (JSON or BSON).
Hope that helps.