0
votes

I've been reading about DDD and am still confused about aggregate root. Imagine that I have a situation similar to a blog, where people can create posts and add comments to other posts.

Rules: -Everybody needs to have an account to add post or comment -Users are able to delete their own comments only

With that in mind, I would need the following objects: -Post -PostComment -User

So, I created only the Post object as aggregate root and added some business logic to it

    public class User : EntityBase
    {
        public string Name { get; set; }
        public string Avatar { get; set; }
    }
    public class Post : EntityBase, IAggregate
    {        
        public string Title { get; set; }
        public string Content { get; set; }
        public User Creator { get; set; }
        private IList<PostComment> Comments { get; set; }

        public void AddComment(PostComment comment)
        {
            this.Comments.Add(comment);
        }
        public void DeleteComment(PostComment comment, int userId)
        {
            if (comment.Creator.Id != userId)
                throw new Exception("You cannot delete a comment that is not yours. blablabla");
            this.Comments.Add(comment);
        }
        public IList<PostComment> GetComments()
        {
            return this.Comments;
        }

    }
    public class PostComment : EntityBase
    {        
        public string Comment { get; set; }
        public User Creator { get; set; }
    }

Am I doing this correctly? I mean, is the business logic in the right place? Or I should've made PostComment as aggregate root too and added the logic of add/delete in it?

2

2 Answers

2
votes

Warning: it's difficult to reason about DDD using toy problems. Especially in your core domain, the point of all of this work is that you can customize things to meet your local needs. If you didn't need a bespoke solution, you'd just buy some off-the-shelf solution, integrate and get on with it.

Or I should've made PostComment as aggregate root too and added the logic of add/delete in it?

Maybe. Aggregates are best thought of as atoms, you load the entire aggregate, make your changes, save the results.

So if you find yourself with many concurrent attempts to modify the same aggregate, then you have to deal with a bunch of contention issues. Alice can't change her comment while Bob is changing his; we have to do them one at a time (to avoid losing changes).

On the other hand, if each comment is an aggregate of its own, then Bob and Alice can make their changes in parallel, without needing to rerun the "business logic" because the other person's change happened first.

Which is great, when it is free. But it isn't free -- the cost you pay is that the information is now distributed, and you have to deal with the fact that the changes have different timing. You'll sometimes see "eventual consistency" used here -- because the authoritative information is distributed, there will be times where not all of the observers have the same sets of changes.

In most domains, this is fine: race conditions don't exist. But trying to perform an all or nothing change across distributed data is a nightmare.

On the other hand, if you are willing to accept that changes happen at different times, then separating the aggregates out is fine.

Example: Twitter. Bob tweets something dumb. Alice tweets that Bob is dumb, with a link to his tweet. Bob deletes his tweet. And that's all fine, because we're comfortable with the fact that Alice's tweet has a link to something that is no longer available.

It is often the case that information that comes from the outside world can be its own aggregate, because what we are really doing at that stage is caching data, which is already stale by the time we receive it.

You may also want to review Mauro Servienti's talk All Our Aggregates Are Wrong, which discusses the heuristics for breaking down an aggregate into smaller pieces.

1
votes

Am I doing this correctly? I mean, is the business logic in the right place? Or I should've made PostComment as aggregate root too and added the logic of add/delete in it?

Partially! I consider the logic is in the right place and PostComment should not be an aggregate root. But if you wants to take off more about DDD I consider that there are some another points to review before continue. I hope I can help you some way in the explanations bellow.

I have reviewed the code and refactored it to explain some points you can reconsider. Try to read it, compare and understand before read my explanation below.

// you can simplify your DomainModel removing the IAggregate plus adding generics
public abstract class Entity<T>
{
    public T Id { get; set; }
}

// this is an Aggregate Root
public class Person : Entity<int>
{
    public string Name { get; set; }
    public string Avatar { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

//this is an Aggregate Root
public class Post : Entity<int>
{
    private List<Comment> _comments = new List<Comment>();

    public string Title { get; set; }
    public string Content { get; set; }
    public Person Author { get; set; }
    public IReadOnlyList<Comment> Comments => _comments;

    public void Reply(Comment comment)
    {
        _comments.Add(comment);
    }
    public void Delete(Comment comment, int personId)
    {
        if (!AreSame(comment.Author, personId))
            throw new Exception("You cannot delete a comment that is not yours. blablabla");

        _comments.Add(comment);
    }

    private bool AreSame(Person author, int personId)
    {
        return author.Id == personId;
    }

    public override string ToString()
    {
        return Title;
    }
}

// this is a Value Object part of Post Aggregate
public struct Comment
{
    public DateTime Date;
    public string Text;
    public Person Author;

    public Comment(DateTime date, string text, Person author)
    {
        Date = date;
        Text = text;
        Author = author;
    }

    public override string ToString()
    {
        return $"{Date} - {Author}: {Text}";
    }
}

If the PostComment is part of Post Aggregate, it can't be an EntityBase, because each Aggragate should have only one root (Id). You're modeling a domain where a Post may have N Comments. You can consider the PostComment as a Value Object instead an Entity removing his Id.

You should pay attention about the names you are using. Try to sound more natural. It is called, ubiquitous language, the words everybody speak.

User is a description that just have a sense in system's context, in other words, you should have a User if you dealing with Security or Authentication contexts, in a Blog Context you have a Person acting as Author.

Increase readability using terms your users says. Reply may be more natural than AddComment.

    public void Reply(Comment comment)
    {
        _comments.Add(comment);
    }

Increase readability adding names for your conditions:

    public void Delete(Comment comment, int personId)
    {
        if (!AreSame(comment.Author, personId))
            throw new Exception("You cannot delete a comment that is not yours. blablabla");

        _comments.Add(comment);
    }

    private bool AreSame(Person author, int personId)
    {
        return author.Id == personId;
    }