7

I think I am struggling to model a relationship between 2 entities. I am trying to do so in a way that promotes DDD and captures business semantics. The following is my business context:

I am developing a memberships microservice. It is a service that is used to create organizations to which you can then invite members. An organization can have varying amounts of members throughout its lifecycle but needs to have at least one at all times.

So I have thought of multiple ways of trying to model this and am not sure which one is best and why.

Option 1: Making the Organization an Aggregate Root

class Organization {
  private id: OrgID;
  private name: Name;
  private members: Members[];

  constructor(initialMembers: Member[], name: Name, id: OrgId) {
    if(initialMembers.length < 1) {
     throw new Error("At least one member is needed");
    }
    this.members = members;
    this.name = name;
    this.id = id;
  }

  create

  addMember(member: Member){
   //..
  }
  removeMember(id: MemberID) {
   //..
   if (this.members.length === 1) {
     throw new Error("At least one member is needed");
   }
  }
}

Pros: Members are a concept that only exist within the context of an organization. The organization effectively acts as the aggregate root and enforces an invariant (Organization needs at least 1 member).

Concern: having an array of all members represented in this model is technically accurate but im afraid that I would not be able to hold all member sub entities in memory in the case I have a lot of them.

Option 2: Member has reference to org + injecting a repository to the Organization

interface Member {
  organizationId: OrgId;
  ...
}

class Organization {
  private id: OrgID;
  private name: Name;
  private memberRepo: MemberRepo;

  constructor(memberRepo: MemberRepo, name: Name, id: OrgID) {
    this.memberRepo = memberRepo;
    this.name = name;
    this.id = id;
  }

  addMember(memberName: string){
   const member = new Member(this.orgId, memberName);
   this.memberRepo.add(member)
  }
  
  removeMember(id: MemberID) {
   const numberOfMembers = memberRepo.sizeByOrg(this.orgId);
   if (this.members.length === 1) {
     throw new Error("organization should have at least one member");
   } else {
     //...
   }
  }
}

pros: This method is guaranteed to not exhaust application memory in the case where there is a lot of members in an organization.

concerns: I keep reading that I shouldn't pass a repository to an entity. I am still unsure whether this concern is warranted or not in this situation since I am only referencing a Repository interface here. The interface is technically part of the domain. But im not sure if passing the entire repo is overkill or not.

Option 3: Create a domain service that represents the interaction/relationship between organizations and members

class Organization {
  private id: OrgID;
  private name: Name;
  private orgMemberService: OrgMemberService[];

  constructor(orgMemberService: OrgMemberService, name: Name, id: OrgId) {
    this.orgMemberService = orgMemberService
    this.name = name;
    this.id = id;
  }

  addMember(member: Member){
   this.orgMemberService(this.id, member);
  }

  removeMember(memberId: MemberID) {
   this.orgMemberService(this.id, memberId);
  }
}

Pros: This better abides the by the Single Responsibility Principle as the relationship between an org and its members is cleanly encapsulated by the service.

Concerns: Similar to the situation above, this passes a whole class instance to the constructor to instantiate the Organization. The OrgMemberService would still be dependent on the repo interface.

Option 4: Add members to organization outside of Organization class

class Organization {
  private id: OrgID;
  private name: Name;

  constructor(name: Name, id: OrgId) {
    this.name = name;
    this.id = id;
  }
}

and then in a service

class OrgMemberService {
 private organization: Organization;

 constructor(organization: Organization) {
  this.organization = organization
 }

 addMember() {}
 removeMember() {}
}

Pros: The instantiation is much simpler and no heavy singleton business logic service is passed to the constructor.

Cons: The model has become rather anemic which is something we are typically advised against. In addition, the Organization class no longer responsible for the feasibility of its own existence as another service is now responsible for ensuring that at least 1 member exists.

Your input is very much appreciate here.

5
  • 1
    Keep in mind that you are equating persistence and domain models. An Aggregate Root is an In-Memory representation of a boundary. For example, Organization might not need the array of Member objects, but the array of Member IDs. O a small projection (representation) of Member. The minimum required for Organization to hold and ensure the invariants. If you put the ORM model in the middle, you are not doing DDD. You are doing ORM driven design
    – Laiv
    Mar 8, 2022 at 10:28
  • I don't think im mixing it up. I understand your point. But since there is no limit on the number of members an organization my have, your solution may not be representable in memory assuming some reason an organization has a huge amount of members (even if just referred to by their ID).
    – alaboudi
    Mar 8, 2022 at 13:13
  • The memory footprint of an array of longs is reasonable smaller than the footprint of the same amount of entries, but complex objects.
    – Laiv
    Mar 8, 2022 at 13:30
  • Yes I agree. And your suggestion is very practical. But I can see this organization entity have many 1 to n relationships. I don't feel comfortable going to down this way of modelling with each one.
    – alaboudi
    Mar 8, 2022 at 13:36
  • I'd question the requirement I think. These members all represent humans right? What if they all lose interest in the organization? You can't force them to participate in it's activities. You can make your system keep one of them on record as a member, but if the person refuses to do anything with or for the org that's just inaccurate information.
    – bdsl
    Aug 13, 2022 at 22:23

4 Answers 4

1

Organisations have typically different kind of memberships, the typology being generally laid down in the organisation’s bylaws:

  • This implies that conceptually, the membership makes only sense in relation to the organization.
  • Moreover, some invariants of the organization may involve membership. In some countries for example, legal associations (sport clubs etc) must have a minimum number of members.

In this regard, option 1 seems the most appropriate, if the members are understood as memberships (i.e. only the “belonging” of a person/entity to that organization and not this person/entity itself).

The entities (persons, companies, organizations), that can become member should not be part of the organization aggregate, because they exist independently of the organisation and could have a membership in several organizations. They form an independent aggregate.

The relation between the two aggregates (organization+memberships on one side, the people on the other) could be managed by a domain service (option 3).

2
  • Yes you pretty much captured it nicely. Members only make sense as part of the org but users exist regardless. I have a few questions though. Would this service be injected into the organization or would the organization be injected into the service? In either case, how would you handle instantiating an organization with a huge list of members? Is it ok to inject a repository outside the app service layer?
    – alaboudi
    Mar 8, 2022 at 13:28
  • @alaboudi I’d inject the organization and the user into the service. Membership would reference user by ID. Depends on what you call huge, but wouldn’t lazy loading be an option? The last one deserves its own question ;-)
    – Christophe
    Mar 8, 2022 at 16:14
0

It’s a best practice that Aggregates only reference other aggregates by Id.

Assuming Member is also an AggregateRoot, including a collection of MemberId in Organization will allow you to enforce the business rule that every organization should have at least one member, without having to load all members into memory.

2
  • Yes but this solution may be an issue if the organization has a huge list of memberIds
    – alaboudi
    Mar 8, 2022 at 13:17
  • DDD is a good design technique to solve complex business problems. If the problems that need to be solved are more technical than business problems, you might need to look beyond what the DDD toolbox has to offer.
    – Rik D
    Mar 8, 2022 at 17:56
0

Option 2: Member has reference to org + injecting a repository to the Organization

The second step does not follow from the first.

If members are their own aggregate root, which I do agree is the right approach here due to not wanting to load all the members all the time (and what I suspect to be a heavier feature set on the members themselves), then they represent their own bounded context. Therefore, the members bounded context handles its own CRUD logic. Another bounded context (organisation) does not handle it for them, as that would directly contradict the independent nature of a bounded context.

Semantically rephrasing your approach, you don't then "add a member to the organisation", but you create a member (and fill in their organisation ID as part of the data). You don't "remove a member from an organisation", you delete the member (which inherently removes any reference they made to their organisation).

Do take note that how you model your domain does not necessarily reflect what your UI looks like. It's perfectly possible to create a single "organisation" web page which lists all of its members and allow you to add/remove them. To the end user, this looks like a single source of information, even though the backend logic actually relies on two separate bounded contexts to provide all of the data/functionality to the end user.

An organization can have varying amounts of members throughout its lifecycle but needs to have at least one at all times.

This can be handled on the service level instead of the domain level; by only exposing the "organisation create" logic in a way that includes the organisation's first member; and similarly for the "member delete" logic to validate that this is not the only member of the organisation (or to then also delete the organisation if it is the case).

2
  • An organization having at least one member is a domain level invariant. Im not sure I fully agree with this answer.
    – alaboudi
    Mar 8, 2022 at 14:19
  • Bounded Contexts are language barriers. If a named concept has a different meaning depending on which domain expert you ask, then you stumbled upon another Bounded Context. I don’t see how the limited information the question provided could lead to the conclusion that a separate Bounded Context is necessary.
    – Rik D
    Mar 8, 2022 at 23:14
0

In your specific use case, I would add a property called memberCount to Organization aggregate and use it to validate if you have at least one member in the organization when adding and removing them.

You also mentioned you don't want to keep a list of members in memory, so I would modify the addMember method to return a Member instance. In this case, your organization class becomes also a Factory of members.

Example:

interface Member {
    organizationId: OrgId;
    ...
}    

class Organization {
    private id: OrgID;
    private name: Name;
    private memberCount: int;
  
    constructor(name: Name, id: OrgID) {
      this.name = name;
      this.id = id;
    }
  
    addMember(memberName: string) : Member {
     const member = new Member(this.orgId, memberName);
     memberCount+=1;
     return member;
    }
    
    removeMember(member: Member) {
     if(member.organizationId !== this.id)
       throw new Error("Member is not part of the organization");

     if (this.memberCount === 1) {
       throw new Error("organization should have at least one member");

     this.memberCount-=1;
    }
  }

The responsibility of saving the members would be then delegated to an application service.

Example:

class OrgMemberService {
    private orgRepo: OrgRepo;
    private memberRepo: MemberRepo;
   
    constructor(orgRepo: OrgRepo, memberRepo: MemberRepo) {
        this.orgRepo: orgRepo;
        this.memberRepo = memberRepo;
    }
   
    addMember(orgId: OrgID, memberName: string) {
        org: Organization = this.orgRepo.findById(orgId);
        member: Member = org.addMember(memberName);
        
        this.orgRepo.save(org);
        this.memberRepo.save(member);
    }

    removeMember() {
        ....
    }
   }

To guarantee that the memberCount won't be modified by multiple different users at the same time, you can use optimistic locking or in the worst case scenario, pessimistic locking.

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