Domain Driven Design – Understanding Aggregates and Aggregate Roots

aggregatedomain-driven-design

I've stumbled upon a problem: "I can't split my domain models into aggregate roots".

I'm a junior developer and novice at DDD. I really want to understand it, but sometimes it's really confusing.

From this point I want to describe my domain briefly.

My poject dedicates to provide users opportunity to create any kind of documents by themselve. Users can create a new type of document. Each new type consists of its attributes. Then a user of this application can create a concrete document based on its type. User can also send the document for approval. An approval flow is different for each types.

So, we have the following models:

  1. DocumentType/ DocumentTemplate – acts as a template based on which
    concrete documents are created. It has one to many relationship with
    Document.
  2. DocumentsAttribute – represents an attribute of document.
    It has many to many relationship with DocumentType.
  3. AttributeValue – when a concrete document is created, It looks at
    its type and creates values for attributes, which has
    its type. Many to many relationship with Document and Attribute.
  4. Document – represents a concrete document that is created by users.

There are others models but I don't think that they make sense.

As you understand, here I apply Entity Attribute Value (EAV) pattern of data model. You can see a diagram that shows relationships in the database.

And my problems are:

I have a lot of entities in my model besides I have described.

I think that Document is definitely an aggregate root in my Domain. Because such things as ApprovalProcess which is aggregate cannot live out of it.

Here is the first question:

ApprovalProcess consists of its steps. Each step is an entity since it is mutable. A step has its state that can be changed. ApprvalProcess's state depends on its steps. Here we have a business invariant: "ApprovalProcess can be approved only if all its steps is approved".

I think that it is an aggregate root because it has the business invariant and contains entities that cannot live out of it. And we don't want to allow to have direct access to its steps in order to keep ApprovalProcess consistent.

Am I mistaken that ApprovalProcess is an aggregate root? May it is just an aggregate?
Can one aggregate root exist within another one as it's part? Does it mean that ApprovalProcess is just aggregate because Document is responsible for access to its parts? But when ApprovalProcess's step is approved, Document delegates an operation to ApprovalProcess.

For example:

Document doc = new Document(...);
doc.SendForAooroval(); //ApprovalProcess is created.

doc.ApproveStep(int stepId); // Inside the method Document delegates responsibility for approvement to ApprovalProcess.

Or I should leave Document and ApprovalProcess separately. Hence Document is going to refer to ApprovalProcess by Identity. And we have the following scenario:

Document doc = documentRepository.Get(docId);
doc.SendForAooroval();// A domain event "DocumentCreatedEvent" is raised.

DocumentCreatedEventHandler:

ApprovalProcess approvalProcess = new ApprovalProcess(event.DocId); // ApprovalProcessCreatedEvent is raised

approvalProcessRepository.Add(approvalProcess);
approvalProcessRepositroy.UnitOfWork.Save(); //commit 

But if ApprovalProcess's state changes, Document's state also changes. ApprovalProcess is approved then Document is also approved. Another word ApprovalProcess is kind of part of Document's state. Only thanks to it we can know that Document is approved.

And the biggest problem that I'm experiencing:

DocumentType is also an aggregate root. It consists of its attributes and ApprovalScheme. I haven't mentioned ApprovalScheme yet on purpose to keep my explanation as simple as possible. ApporvalScheme consists also from some entities. It's just an approval flow for DocumentType. ApprovalProcess is created according to ApprovalScheme of DocumentType which has Document. ApprovalScheme cannot exist without DocumentType. One to one relationship.

Document refers by identity to its DocumentType. Is it correctly?

At the begining of this task I thought that DocumentType should be a part of Document.

DocumentType has many Documents but in my domain It doesn't make sense. It doesn't represent the state of DocumentType. DocumentType can be marked as deleted but can't be deleted.

Document and DocumentType are two different aggregate roots. Am I right?

Thank you so much If you read it. Thank you a lot for you attention and help!
Sorry for my terrible English.

==========================================================================================================================================================

Problem №2

Now I want to provide you more details about my Domain.

There are: users, documents, types of documents, attributes of documents.

The documents in my application have dynamic fields. So I use EAV pattern.
A type of the Document defines set of attributes and an approval flow that consists of steps. If a document is created, it is created all values of attributes that its type contains for the document.

If document is sent for approval, it is created a particular state for each step in an approval flow which is kept by the document type. This set of state relates to the document's approval process.

There are two types of users: Employee and Amin. The Employee can have differen roles like: manager, accountant and so on.

Each employee can:

  • Create a new document.
  • Send document for approval.
  • Edit document if it is not on approval.
  • Approve exactly one of the step of approval process that belongs to
    the document and user is responsible for.
  • Reject exactly one of the step of approval process that belongs to
    the document and user is responsible for.
  • Print it if it is approved.

Each admin can:

  • Create a new type of document.
  • Create new or set an existing attribute for the type of a document.
  • Create new or edit existing approval flow for the type of a document.

If the approval process is rejected, the user who is a creator of the document must make some corrects to the document and send it for approval again.

I have to option how to implement it.
The first option is I think that I have the following entities and Aggregates (I want to show their interface):

interface User {
  Document CreateNewDocument(int docType);
  Document SendDocumentForApproval(int docId);
  Document EditDocument(document, List<AttributesValues> attributesValues); // list of only attributes that must be changed.
  Document Approve(document, stepId);
  Document Reject(document, stepId);
  Document Print(document);
}

And I'm going to use it somehow like this:

CreateDocumentCommandHandler:

User user = userRepository.GetUser(userId);

Document doc = user.CreateNewDocument(docTypeId);

documentRepository.Add(doc);
documentRepository.UnitOfWork.Save();

SendDocumentForApprovalCommandHandler:

User user = userRepository.GetUser(userId);
Document doc = documentRepository.GetDocument(docId)

doc = user.SendDocumentForApproval(doc );

documentRepository.Update(doc);
documentRepository.UnitOfWork.Save();

But what is really tricky for me is what is hapenning inside SendDocumentForApproval method of the user:

Document SendDocumentForApproval(document)
{
   if(document == null) throw new ArgumentNullException(nameof(document));

   // When document is sent for approval, it needs to initialize a new
   // approval process and all states for the approval scheme's steps.
   // So we need to get a type of the document and ask it to provide us
   // ApprovalProcess that is based on ApprovalScheme.
   document.SubmitApprove(); // DocumentSendForApprovalDomainEvent is raised
   return document;
}

DocumentSendForApprovalDomainEventHandler:

Handle(dto)
{
   // It seems very stupid because either I need get one more time document
   // with the repository or I need to keep a reference to the instance of
   // the document as a field in the dto argument of this handler.
   Document doc = dto.Document;// looks terrible
   // or
   Document doc = documentRepository.GetDocument(dto.DocId);//looks also terrible because we have alredy used repository on this purpose in SendDocumentForApprovalCommandHandler

    DocType docType = docTypeRepositoty.GetDocType(doc.TypeId);

    ApprovalProcess approvalProcess = docType.InitNewApprovalProcessForDocument(doc.Id);


    doc.AssignApprovalProcess(approvalProcess)
}

Could someone correct the ApprovalProcess that I've modeled above please?
Or tell me please that I'm on the right way or not.

Best Answer

I think a lot of the confusion here is a result of focusing on data instead of behavior. The purpose of DDD is to model the functional requirements of a system to provide a useful abstract of your business domain. This means that we model according to behavior and let the data that supports our behavior become an implementation detail. So let's start there!

The system you are describing, in terms of behavior, is actually quite simple. Let's begin by listing your use-cases (gleaned from your question):

  • Create new Document
  • Change Document attribute value
  • Approve Document

With the above in mind, let us now create some command handlers:

CreateDocumentHandler

DocumentType dt = documentTypes.Find( cmd.DocumentTypeId );
Document doc = dt.CreateEmptyDocument( cmd.UserId ); // factory method
documents.Add( doc );

ChangeDocumentHandler

Document doc = documents.Find( cmd.DocumentId );
doc.ChangeAttribute( cmd.AttributeName, cmd.AttributeValue );
documents.Save( doc );

ApproveDocumentHandler

Document doc = documents.Find( cmd.DocumentId );
doc.Approve(); // approval process is part of document
documents.Save( doc );

From here I think you can fill in the blanks.

Related Topic