1

I am wondering how I would validate some actions in my domain when a parameter of an action needs to be validated, but that parameter's entity is actually in a different domain so I can't retrieve it.

Let's say I have the following situation:

// "Document" Domain
public class Document
{
    Guid Uuid {get;set;}
    Guid UserUuid {get;set;}
    // Other document properties..
}

// "User" Domain
public class User
{
   Guid Uuid {get;set;}
   UserType Type {get;set;}
   // Other user properties..
}

public enum UserType
{
   NormalUser = 0,
   Employee = 1
}

As you can see, these 2 classes are in 2 domains. In my situation, both of these are in the same service (DocumentService), but they are in different assemblies. Project.Domain.DomainA and Project.Domain.DomainB. They have no reference to eachother.

There are also classes like UserRepository and DocumentRepository which perform data actions. Furthermore, I use CQRS to issue commands. So, for example, creating a document through an api would be like this:

API -> CommandHandler -> Domain -> Persistence

Let's say I need to implement the following:

Allow employees to share a document by sending them to other employees. If the employee tries to share the document to a user who is a NormalUser, an error should occur.

The first part sounds easy enough. I would add a Share method to Document to implement the Sharing functionality. I pass the Uuid instead of the User, because the document domain has no knowledge of the domain where User is located.

void Share(Guid userUuid)
{
  // Share logic, events.. etc..
}

But now I need to validate that the user's type is Employee. Normally that would be easy enough: User.Type == UserType.Employee. But in this case, I have no access to the User type so I can't perform this test in my domain like this. So, the following isn't possible:

public void Share(User user)
{
    if (user.Type != UserType.Employee)
    {
        throw new Exception();
    }
}

I see a few options, but I would like to know what you guys think is best.

  • Validate this logic in the commandhandler:
    var document = documentRepository.GetDocumentByUuid(uuid);
    var user = userRepository.GetUserByUuid(userUuid);
    if (user.Type != UserType.Employee)
    {
        throw new Exception();
    }
    
    document.Share(user.Uuid);
    
    • To me, this is a bad idea. If another commandhandler or any other place in the codebase would try to share a document, this validation would not take place. Furthermore, I believe this validation should take place in my domain.
  • Validate this logic in a validator service of some kind:
    // New interface in the Document domain
    interface IDocumentSharingValidator
    {
        bool CanShareToUser(Guid userUuid);
    }
    
    // This is the Share method in the Document class
    void Share(Guid userUuid, IDocumentSharingValidator validator)
    {
        if(validator.CanShareToUser(userUuid))
        {
            // YAY!
        }
    }
    
    • This seems OK. But where would I place the implementation of IDocumentSharingValidator? It needs to talk to the data layer to retrieve the User, so I see 3 choices:
      • Put it in the Application layer and make it use the UserRepository to do validation.
        • To me, this is the best option right now. But it doesn't feel like good DDD for some reason? It is not limited to my Document domain anymore; it is just part of the application layer now.
      • Put it in the DocumentRepository (Which is in the Data layer) and make it talk to the User table (We use EF Core, so that would be dbContext.Users)
        • Ugly. We still talk to a User table in our Document Repository, so there is a bit of coupling.
      • Ok, so we put it in the UserRepository and make it talk to the User table.
        • Ugly because the UserRepository now uses an implements an interface from the Document domain.

Can you guys help me out? What would be a good idea? And how could I improve my DDD skills about this for the future? :)

Thanks!

2

The two classes may be isolated in different assemblies, and that's fine, but that's not what we mean by domain (domain is just the overall problem space that you're working on). So in that sense, this is all within the same domain.

Now, the fact that these are in different assemblies doesn't really matter (except in the organizational sense). What you have here are two different aggregates. Based on your code, conceptually, Document is an aggregate root, and it references a User, another aggregate root.

It looks like you're following Vaughn Vernon's approach where you only reference other aggregates by an ID, to demarcate aggregate boundaries. This approach relies on repositories to obtain the actual objects via the ID. You can do it in two ways - (1) the entities can maintain a dependency to a repository so that they can internally look up the object (essentially lazy loading), and (2) you can push some of the logic a level up, into services, and look up dependent entities ahead of time. Vernon recommends (2) as that way entities and aggregates don't have to depend on the repositories.

The "only reference other aggregates by ID" rule applies to class fields; the reason is to be able to load an aggregate as a complete, usable object, without having to simultaneously load the dependencies when you don't actually need them (and also to make it clear where the aggregate boundaries are). Having a method like void Share(User user) is, on the other hand, perfectly fine, because it doesn't force you to load the User object when you create/reconstitute an instance of the Document aggregate - you just can't call the method. In fact, such a method would signal to developers that they need to obtain a user object before they can call it, which is in line with the style (2) - ahead of time dependency lookup.

Side note: If you are referencing other entities that you have designated to be in the same aggregate, you don't reference them by an ID, but directly. This is what I mean when I say that this is a technique that demarcates aggregate boundaries in code. Direct reference - same aggregate, a reference by an ID - different aggregate.

So, ahead of time dependency lookup would look something like this (adapted from Vernon's guide, part 2, link below):

public class DocumentSharingService 
{
  public void ShareDocumentWithUser(Guid documentUuid,  Guid userUuid) 
  {
    Document doc = documentRepo.GetDocument(documentUuid);

    // you know a user is needed, so you obtain the user object as well
    User user = userRepo.GetUser(userUuid);  

    // You could place additional logic here, especially logic that 
    // doesn't have a clear home in either Document or User.

    doc.Share(user);   // Share can now validate the user
  } 

  // Other methods...
}

// then you call this service from your commandhandler 
// or any other place in code. 
// You either have to arrange things so that you can't 
// update the underlying system (or DB) except through such services
// (if you need to enforce the additional logic above), 
// OR keep the design simple and rely on developer discipline 

You can find the aforementioned 3-part guide by Vernon here.

On a final note, keep in mind that this is an approach to DDD and aggregate design, and not the one an true way to do DDD. It's much more important to understand the concepts and reasoning behind them than it is to religiously follow these rules.

Also, if your current design feels more like it's getting in the way instead of helping, don't be afraid to rethink your aggregates. We don't organize the code into aggregates because it's a "best practice", but because we want to get some benefits from doing that (e.g., low coupling through aggregate roots, high cohesion, consistency boundaries, usability improvements, etc.)

| improve this answer | |
  • Hey @Filip, thank you so much for your great answer! One question I have after reading your post and part 2 of Vernon's guide is the true benefit of that service in my current situation. As you say, I don't need to religiously follow the rules of DDD. I currently do not see a benefit of this service and I want to discuss this. If my commandhandler uses repos to retrieve user and document and calls doc.share(), stuff seems fine to me. In my eyes introducing this service makes sense when the code will be used multiple times in the codebase, but not now. What do you think? – S. ten Brinke Oct 9 at 9:15
  • Another reason why I think that this service currently is unnecessary is because it seems to add a bit of complexity that doesn't benefit the codebase. This already seems like a strong enough argument for me. I do not want to criticize your answer, I just would like to learn more about your arguments and why the service would be a good idea. Again, my arguments are that the service would be a good idea if the code is used often. You say it could also contain complex logic, but if it is complex and only used once, why not keep that in the commandhandler too? – S. ten Brinke Oct 9 at 9:16
  • @S.tenBrinke - "service" is a sort of a generic term; in DDD a domain service is something that captures some domain logic that perhaps doesn't naturally fit elsewhere (e.g. in an entity). I wasn't sure what your commandhandler represents (architecturally), so I used this generic document service (adapting directly from Vernon) just to have a place to write some code, to demonstrate what I mean. I didn't mean to say that you must introduce a class called "SomethingService" into your codebase. If you feel commandhandler should play this role, then go for it. – Filip Milovanović Oct 9 at 18:16
  • That said, when it comes to decomposition, it's not all about separating out stuff that's used more than once. As things become more complicated, a method/class becomes bloated over time and coupled (internally, to its own different parts, as well as externally) in all kinds of ways - so some of the decomposition is done to keep that under control (and as a byproduct, some of the things you've extracted may become reusable). Maybe your commandhandler is not at that point yet, but it's just something to be aware of; and something to consider from time to time. – Filip Milovanović Oct 9 at 18:19
0

There are a couple of things here, some of it may not be related to DDD depending on interpretation.

If two domains are used in the same service those are by definition not different domains. There is no reason to separate two things that are used together or have to know each other in some form.

Can't help but notice that your "objects" are data-heavy. Domain objects should contain domain behavior, not data. These data-centric objects result in logic scattered all over the application, which is part of your current problems.

CQRS does not impact domain design. It is more of a persistence strategy that is sometimes pretty useful, in some circumstances. It should not be visible at all.

All in all it seems to me (and I obviously don't know all your requirements), that you are overthinking, over-patternizing this whole thing. Instead, what you should do is express clear intent in your design. If you believe (I think correctly), that Document.ShareWith(User) would best express your required functionality go with that. Always go with clear intent first. Worry about showing your architecture/DDD/CQRS prowess somewhere else.

| improve this answer | |
  • Hello! You say the following: > Can't help but notice that your "objects" are data-heavy. Domain objects should contain domain behavior, not data. These data-centric objects result in logic scattered all over the application, which is part of your current problems. Could you point out why you think this? I really do not see a problem. In my case, I would have a Document that has some properties, and some behavior like "Delete", "Share", "ChangeFileName", etc. (Just some examples). Why is this "bad"? What would be better? – S. ten Brinke Oct 9 at 8:59
  • Properties do not do anything themselves, they are used somewhere else to do some business behavior. Most of the time one property is used in multiple places, sometimes even copied out into other properties, those again used elsewhere, etc. So all of those places have to know what this property means, how to interpret it, how long it can be, what values it can have, in what relation it is with other pieces of data, etc. So a lot of knowledge gets scattered out across the whole application. It is much better to pull all those behavior back into the object. This is what OO used to mean. – Robert Bräutigam Oct 9 at 9:26
0

Other answers suggest you might be better off with a single Bounded Context. To be honest that might be true. This answer is for situations where there are two (or more) separate Bounded Contexts.

You could create an EmployeeRepository in the Document context. This repository maps to the same underlying persistence mechanism as the UserRepository in the User context. In the Document context, I would not call this a UserRepository, because Users are not relevant here, only Employees are.

If you follow this approach, you have Employees available in the Document context and you can create a

public void Share(Employee employee)

method.

Note that Employees don't have to have all properties Users have in the User context, perhaps a Name and Id are enough. Also note that the EmployeeRepository should not have a Save method. Employee (User) related changes should be done in the User context.

| improve this answer | |
  • Extending User into Employee is a great idea, but I would suggest either keeping the users and employees together in the same domain or putting them in their own domains. Grouping documents and employees makes little sense here unless the concept of an employee is only ever relevant for document (which I highly doubt). – Flater Oct 8 at 15:31
  • @Flater other contexts that are interested in data from a user, can do the same. In another context the user might be called a Manager, or also an Employee but with other data and/or behavior. – Rik D Oct 8 at 15:40
  • @Flater also note that the concept here is not extending. We create an new type in another context, which only loads some data from the same persistence mechanism. – Rik D Oct 8 at 15:43
  • 1
    Just for clarification: in my answer, I'm not suggesting a single Bounded Context (or a single domain), I'm actually saying that the OP is using the terms incorrectly. – Filip Milovanović Oct 8 at 16:07

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.