0

I'm going to ask this question using a concrete example. An optimal answer would take into account that this is just an example for the general problem.

I'd like to implement a user management system with a Clean Architecture approach.

  • My system has users and groups.
  • Group objects contain users.
  • Group objects have a contains_user function.
  • users and groups are part of the same bounded context.
Question

How would I implement the contains_user function in Clean Architecture? How can I cleanly divide domain code / core and non-domain code / infrastructure?

More abstract:

How can I separate an implementation detail (RDS) from domain code without loosing performance?

My own thoughts
  • Loading all users into the group might be bad performance-wise when there is a large number of users in a group.
  • Performance-wise contains_user would best be implemented directly as DB query inside the function. But that would break Clean Architecture.
9
  • Some additional context is needed. Are users and groups part of the same bounded context? What do you mean by "domain code" versus "non-domain code"?
    – Thomas Owens
    CommentedOct 20, 2024 at 11:15
  • The tactical DDD patterns are great for ensuring consistency when handling commands. A command does often not require the same level of performance as queries do. Are you sure your performance concerns are justified?
    – Rik D
    CommentedOct 20, 2024 at 11:36
  • @ThomasOwens edited: Yes, they are part of the same bounded context. Domain code refers to the core, while non-domain code refers to infrastructure code.
    – DarkTrick
    CommentedOct 20, 2024 at 11:59
  • 1
    If users and groups are within the same bounded context, how would a database query "break DDD"? If your software and data architectures correspond to your bounded context, each bounded context should have access to its own data. It seems like you've eliminated the obvious answer, but it's not clear why.
    – Thomas Owens
    CommentedOct 20, 2024 at 16:35
  • 1
    "But that would break DDD" - DDD (and rich domain models in general) do not require the actual means of doing something (the ultimate underlying implementation that gets the job done) to be fully implemented within the domain layer code (or even by the same piece of software). Instead, you want to try and capture the domain logic (business rules) in your domain code, meaning the core code should be written in a way that expresses/enforces those rules - but it should absolutely delegate the low level details of that to layers below that invoke infrastructure code or external services.CommentedOct 21, 2024 at 11:54

2 Answers 2

3

How would I implement the contains_user function in Domain-Driven-Design?

This is (subtly) the wrong question.

Domain Driven Design is a guideline that cares about how to design your domain. It does not care about how to minimize your memory footprint or IO bandwidth when fetching data from an external storage device. You've not found this information simply because that's not what that guideline is focusing on.

Purism of any approach is often an idealistic goal that tend to become very cost-ineffective the closer you try to get to that 100% purity goal. This is why developers often preach pragmatism over dogma, i.e. the ability to use your common sense to understand when pursuing an ideal becomes counterproductive to what you're actually trying to achieve.

Performance-wise contains_user would best be implemented directly as DB query inside the function. But that would break DDD.

Following on from the point I was making, a codebase shouldn't really be "DDD and nothing else". So what you call breaking DDD, I call having a pragmatic codebase that doesn't chase DDD purism as its sole goal. Let's compromise and agree to call it breaking away from DDD purism. The subsequent claim I'm going to make is that this is not problematic in and of itself, as long as you're making an informed decision in doing so.

So what do you do then, if not pure DDD?

It is hard to answer the question the right way for you personally, because I don't have a read on where you draw the line on what would be too much effort/load before you agree to deviate from the DDD ideal.

Most commonly, assuming this is a simple key check with no real business logic behind it, I would omit this from the domain, instead having the application layer talk to the persistence layer directly, so it can run a simple contains check without needing to load any data.
My justification for this is that this is an optimization that supplants the domain driven way of doing this, and the benefit (not having to load the entire list in memory) far outweighs the cost (a deviation from the DDD ideal).

If you want to stick more to DDD, or there is more domain logic to performing this check (e.g. you need to also account for the date on which they were/n't part of the group, or their inclusion in the group is contingent on another condition that needs to be checked), then I would consider designing a separate domain object whose specific responsibility is to perform these checks. Call it a validator/checker or whatever makes the most sense contextually.

This also nudges you towards considering this "membership" as an aggregate root of its own, alongside users and groups. Whether or not this should be explicitly defined as a bounded context depends on whether you have sufficient use cases to justify the cost of creating it.

Personally, I tend to have my domain define my persistence interfaces (not implementations of course), and I don't think it's wrong for domain logic to interact with said interface. In that context, this means that the checker/validator (or membership aggregate root) is therefore perfectly capable of interacting with the persistence layer intelligently, so as to avoid loading more data than is strictly necessary for it to do its job.

2
  • I think you second paragraph should be somewhat more emphasized. Eye-opener. For me that was extremely helpful! I thought I was doing something wrong the whole time.
    – DarkTrick
    CommentedOct 23, 2024 at 12:14
  • Note to other help-seekers: Also read Ewan's answer and its comments. I find it provides additional views and their discussions.
    – DarkTrick
    CommentedOct 23, 2024 at 13:15
2

Group objects have a contains_user function

Almost certainly not. If you have a group object which is the Aggregate Root for groups and users. Then it will have all the user objects in it.. in it

public class Group { public List<User> UsersInGroup } 

I guess you might want to hide the actual list and make a load of functions, but likely the native list/dictionary/map has all the methods you need already. You can just use UsersInGroup.Any(i=>i.Id == id) or whatever

Or its not an aggregate root. In which case with no other data it's probably just a string.

What may have contains_user will be your repository

Repository { bool GroupContainsUser(userId, groupId); List<Group> GetGroupsForUser(userId); } 

Or more likely neither, as a user object is likely to contain its groups

User { List<string> Groups } 

And then, having the user in question you can simply check.

if(u.Groups.Contains(group)); 

Good domain design means that operations can be performed without recourse to a database. If you have some cross domain, or domain breaking function to perform, then you can make a Domain Service to implement it

TelephoneDirectory { Person FindPersonByNumber(number); } 
8
  • "Or more likely neither, as a user object is likely to contain its groups" There are separate use cases for needing to see all users of a group (i.e. group configuration), and all groups of a user (i.e. authorization checks, user configuration). It makes little sense to only have navigational properties one way, as (a) this is a textboox many-to-many scenario and (b) you would always struggle to implement one of the two use cases I just mentioned. The question here is about avoiding unnecessary data loads, i.e. not having to pull everything in memory to run a contains check.
    – Flater
    CommentedOct 20, 2024 at 23:20
  • 2
    "Good domain design means that operations can be performed without recourse to a database." It is correct that domain design guidelines focus on design, which generally uses in-memory as the simplest implementation; but that is rarely a realistic scenario. Some level of optimization is realistic. Just because the domain design guidelines don't explicitly point out this kind of optimization does not mean that it should be avoided. Real world software design is a matter of applying several approaches in order to leverage the most reasonable outcome. Not dogmatically applying one thing.
    – Flater
    CommentedOct 20, 2024 at 23:22
  • the case in point though does not require all the users in a group. Its unlikely to be possible to generate that list. That's why I give my TelephoneDirectory example.
    – Ewan
    CommentedOct 21, 2024 at 13:22
  • "Some level of optimization is realistic" I think you miss the point here. Design your domain so that the optimisation is not required. Following your pragmatic "its ok because its optimisation" loophole leads you down the wrong path here, ie injecting a repo into a domain object in order to facilitate a method, rather than realising that the method does not belong in the domain
    – Ewan
    CommentedOct 21, 2024 at 13:26
  • 2
    @Flater, Ewan: I think you are both right - in real world situations, one may either avoid the OPs problem by rethinking the model (Ewan's approach), or solve it directly by some kind of optimization (Flater's approach), or maybe a mixture of both. For an invented example like this one, however, there is currently not enough context to make a founded decision which approach fits "better" - I can think of different contexts where the either or the other approach works best.
    – Doc Brown
    CommentedOct 21, 2024 at 15:25

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.