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.