2

sorry for the long question, but I love DDD but there is a problem I can't seem to resolve.

We have a problem when adding business rules to the aggregate root.

Our architecture is mostly like this when we make a query from the front-end :

For a simple example, we have a Book we want

  1. to persist
  2. fetch it.

The book has

Name which has to be 10 characters and an Author which needs 15 characters.

Here the pseudo code in TypeScript:

class BookAggregateRoot {
   private _name: string;
   private _author: string;
   private constructor(name: string, author: string) {
       this._name= name;
       this._author= author;
   }

   static create(name: string, author: string): BookAggregateRoot {
       if(name.length < 10) {
           throw error; // not really throwing, its just to show we have an error
       }
       if(author.length < 15) {
           throw error; // not really throwing, its just to show we have an error
       }
       // all data is valid, create the Book
       return BookAggregateRoot(name, author);
   }
}

1st Scenario : save the book

Client -> DTO -> controllers -> useCases(1. calls create from Book class) -> calls repositories(persist the data) -> database -> data is returned, repositories(2. calls create from Book class) -> useCases -> mapperToDTO-> controller -> client.

2nd Scenario : fetch the saved book

Fetching data is in the same way but only, dont need to persist, only fetch the data : Client -> DTO -> controllers -> useCases(builds the query) -> calls repositories(query) -> database -> repositories(3. calls create from Book class) -> useCases -> mapperToDTO-> controller -> client.

As long as the business rules don't change, it's fine as the data stored and the data fetched has the same business rules.

The problem lies in the creating of the AggregateRoot. Let's say now my Book needs a publishedDate, all data that previously doesn't satisfy the data fetched from the database and will fail at step 3.

The class now becomes :

class BookAggregateRoot {
   private _name: string;
   private _author: string;
   private _publishedDate: Date;
   private constructor(name: string, author: string, publishedDate: Date) {
       this._name= name;
       this._author= author;
       this._publishedDate = publishedDate;
   }

   static create(name: string, author: string, publishedDate: Date): BookAggregateRoot {
       if(name.length < 10) {
           throw error; // not really throwing, its just to show we have an error
       }
       if(author.length < 15) {
           throw error; // not really throwing, its just to show we have an error
       }
       if(publishedDate.invalid) {
           throw error; // not really throwing, its just to show we have an error
       }
       // all data is valid, create the Book
       return BookAggregateRoot(name, author, publishedDate);
   }
}

the data will always fail at step 3. above as the create requires a date.

We could create a migration to fix all the data... but is that the right approach?

thanx in advance and let me know if there is anything to better the design. :)

2 Answers 2

1

If the business requires that you store an additional field/column of data, then there are essentially three possibilities:

  1. You modify the code to require the new field but don't touch the old data. This effectively renders the existing data inaccessible which is not likely to be accepted by the business.
  2. You write the reading code such that it can handle the missing field. This also means you have to mark the column in the database itself as optional (nullable) and you will have to assume that the field will become empty also for data entered after the change.
  3. You migrate the existing data to include (a sensible default for) the new field. This would be a one-time operation that does not complicate the normal flow of the application.
4
  • Yes, that's what I thought. You can modify the code or do the migration. I was wondering if there was another way. Kind of when systems are backward compatible. If I fetch the old books that didn't have the publishedDate, they were still valid at the time of creation and only invalid after I added the publishedDate so I'd like to be able to query them. Right now, if I query them, my domain model will define it as invalid.
    – bachibusuc
    Jan 25, 2021 at 3:02
  • 1
    @bachibusuc Domain driven design is focused on domain modelling, it seems like you're too concerned about the limitations of the language and therefore focus on the code. That is wrong. Domain Driven Design's first priority should always be the business - their decisions. You should ask them what the right approach is. If they want to distinguish between old and new books, you will need to add a field to distinguish between the two.
    – Andy
    Jan 25, 2021 at 7:21
  • 1
    @bachibusuc, systems that need to be backward compatible with old data typically implement option 2 or they might have an option to do option 3 when encountering the old data. Jan 25, 2021 at 9:06
  • @Andy I was asking about iterations and versions. When you start a project, you know the business logic but as the project advances, business logic is added. That is what I was referring to. It's not about having an old or new book. It's about adding business rules and how usually people address this problems.
    – bachibusuc
    Jan 25, 2021 at 19:33
0

I don't know the structure of your domain, or what other domain objects you have, but have you thought deeply about how you are structuring your code.

At some level it makes sense that you or I could create a book. Creating a book isn't the same as publishing one. For example, I could write a memoir and never get it published.

With that in mind, would it be possible to restructure the code so that the published date is optional?

public void PublishBook(DateTime datePublished){
   _publishedDate = publishedDate;
}

Your constructor the doesn't need to check if the published date is set. Also, I've found that creating a object that represents the data record that I can pass to the constructor of my aggregate, transforming it as necessary to fit the aggregate is a nice pattern that makes it easier to update aggregates when refactoring as you don't always need to change the constructor.

Check the premise of the book aggregate. At a broader level maybe there should be a publisher that accepts a book record and does the publishing? Like I said depends on the rest of your domain. Regardless, passing an object instead of individual values to your aggregate root will make life easier in the future.

2
  • Yes, it can be optional. What I meant is more like something like an iteration/version. So now i'm at version 1.0 of the system, it's a proof of concept. Now in version 2.0, I add a publisher(which a book necessarily has and must have but not in version 1.0 as it wasn't required for the proof of concept). It can be optional in some cases, but the questionning was more about when I can't make the subsequent field optional, what do people usually do?
    – bachibusuc
    Jan 25, 2021 at 3:09
  • 1
    If the field can't be optional then you'd have to create a migration and set an appropriate default value.
    – GetFuzzy
    Jan 25, 2021 at 13:54

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.