3

I am building an app that integrates Plaid API to access user bank info (logins, accounts, transactions, etc.). I'm trying to follow DDD principles.

Here is a general idea of how the Plaid API flow works:

  1. A user provides his email/password for some bank institution. If valid, a plaid Item is created. This object associates a user to a set of bank credentials and contains an access token which can be used to further interact with the API.
  2. Every plaid Item has access to a certain set of bank Accounts.
  3. Every bank Account has a set of Transactions

So far, I created 3 entities in my domain layer: Item, Account and Transaction. I created a repository with basic CRUD operations for each.

public class Item
{
    public string Id { get; set; }
    public string AccessToken { get; set; }
    public string UserId { get; set; }
    ...
}

public class Account
{
    public string Id { get; set; }
    public string ItemId { get; set; 
    ...
}
public class Transaction
{
    public string Id { get; set; }
    public string AccountId { get; set; 
    ...
}

As you can see, the relationship between these entities is:

User HAS Item -> Item HAS Accounts -> Account HAS Transactions

My question is, what happens when I need to find an entity by an indirect parent? For example: GetTransactionsByItemId or GetAccountsByUserId. Based on DDD, where should this logic go?

Because of how my data is structured (No-SQL chain of 1-many relations) I know I have to do these sort of queries in multiple steps. However, I've read that a Repository should only be concerned about it's own entity so I suspect that injecting the ItemsRepository and AccountsRepository to the TransactionsRepository to add a GetTransactionsByItemId method might not be a good idea.

I also read about injecting many repositories to a Service and managing all these "joins" from inside. However, I can't come up with a name for this Service, so I'm worried that's because conceptually this doesn't make much sense.

I also read about Aggregates but I'm not sure if I recognize a root in these entities.

Another option I can think of is to try shortening relationships by adding an ItemId to every transaction for example. However, this would need to be a hack because of how I get the data from the api.

Pedro L
  • 86
  • 1
  • 5

2 Answers2

2

I would say your aggregation root would be an Item. If I got the structure right, Accounts cannot exist withoug Items and Transactions without account. So you could be ok just with ItemsRepository:

public class ItemsRepository
{
  public async Task<Item> GetById(long id, IncludesSpec includes)
  {
    return await this.context.Items
      .Where(c => c.Id == id)
      .Include(c => c.Accounts).ThenInclude(c => c.Transactions)
      .SingleOrDefaultAsync();
  }
}

Than you get an Item with all the loaded data in it. The IncludesSpec is up to you: it would contain which includes should be made and includes shall be added dynamically in the repository method.

As of .net ef core 5 you can do filtered Includes, like .Include(c => c.Accounts.Where(...)), so you could further narrow the actual include down based on your requirements. You could pass another parameter which would contain this filter information.

Also your Item should expose Accounts as read-only collection (use backing field for EF) and provide a method AddAccount() so that nobody can modify your DDD item as pure entity.

Maxim Zabolotskikh
  • 1,923
  • 15
  • 19
  • Thanks for your helpful response. With this approach, how would I do a query like _GetTransactionsInDateRange(string userId, DateTime startDate, DateTime endDate)_ ? Can an ItemRepository have a method that returns Transaction Entities or should it only return Items? – Pedro L Jan 17 '21 at 01:22
  • Well, in the long run it's up to you, rules are there to break them. Depends on requiement, speed, data.. I would put it into a service, which queries Item from repository and than returns a list of transactions. However if it's e.g. not performant enough, I'd put it directly into repository, coz performance outweights rules in the end. As for the time range, I'd go with *query objects* passed to the repository methods which build the right EF query. It's more work in the beginning, but once it's all set up, you end up with all your Wheres nicely encapsulated. – Maxim Zabolotskikh Jan 18 '21 at 07:38
2

What would make the most sense, I believe, would be to have a Service with multiple Repositories injected into it.

You could have one ItemRepository which returns Item objects, one AccountRepository which returns Accounts, one TransactionRepository returning Transactions and one UserRepository returning Users.

If your data model makes it cumbersome to do your query in one request, then have a function in your service which is transactional (ACID : either it all completes or it's all rollbacked) which does different queries to the each injected repository, then builds the objects and returns them.

If you do see a way to make it one query, you can hard-code that query inside the relevant repository. From Domain-Driven Design, Evans:

Hard-coded queries can be built on top of any infrastructure and without a lot of investment, because they do just what some client would have to do anyway.

On projects with a lot of querying, a REPOSITORY framework can be built that allows more flexible queries.[...]

One particularly apt approach to generalizing REPOSITORIES through a framework is to use SPECIFICATION-based queries. A SPECIFICATION allows a client to describe (that is, specify) what is wants without concern for how it will be obtained. In the process, an object that can actually carry out the selection is created.[...]

Even a REPOSITORY design with flexible queries should allow for the addition of specialized hard-coded queries. They might be convenience methods that encapsulate an often-used query or a query that doesn't return the objects themselves, such as a mathematical summary of selected objects. Frameworks that don't allow for such contingencies tend to distort the domain design or get bypassed by developers.

Chris Neve
  • 1,522
  • 15
  • 26