0

I am trying to model a simple application using DDD.

Consider the following simplified code where the idea is to hide a Comment if its parent Post is hidden:

class Post {

    private $hidden;

    public function isHidden() {
        return $this->hidden;
    }

}

class Comment {
    private $post;

    private $hidden;

    public __construct(Post $post) {
        $this->post = $post;
    }

    public function isHidden() {
        if($this->hidden || $this->post->isHidden()){
            return true;
        }
    }

}

I am considering Comments and Posts as Aggregate Roots.

After reading about referencing Aggregate Roots by their IDs instead of reference I have changed the Comments reference to Post to the Post Id immediately catching an error in my Unit Test because of this line of code:

$this->post->isHidden()

Shouldn't this type of logic be in the Domain layer? Could this be a problem in the way I am designing my Aggregates?

Joyce
  • 57
  • 9
  • As per usual design principles in DDD, `Comment` should not have an object reference to `Post`. If you really need to make individual comments hidden if their parent `Post` is hidden, then make that explicit (e.g., by a long running process/saga). – Alexander Langer Jun 07 '15 at 09:54

2 Answers2

0

If the hidden property of Post and Comment must always be strongly consistent then you may want to model a large cluster aggregate. To reduce concurrency failures (e.g allow 2 comments added at the same time) you may tweak your persistence mechanism to allow collections to grow unbound. However, this is usually the kind of rule that can be made eventually consistent. Does it really matter if there's a small delay between the time a Post gets hidden and it's Comments are?

Choosing eventual consistency, Post and Comment are their own AR. When Post gets hidden, a PostHidden event is sent to a messaging mechanism and a subscriber will take care of making individual associated Comment ARs consistent.

Also, note that you may not have to sync Post.hidden and Comment.hidden at all. Since Comments are probably only seen in the context of a Post, I do not really see how the UI would allow to see Comments of an hidden Post. Avoiding to sync the hidden flag would actually allow to unhide a Post while bringing back it's Comments in the state they were before the Post got hidden without having to do anything.

plalx
  • 42,889
  • 6
  • 74
  • 90
0

First of all, I have to agree with plalx's last paragraph regarding the need to synchronise the hidden flags in both Post as well as Comment. One would assume that the UI would look at a Post and realise it's hidden and not bother fetching/showing the comments? But I appreciate that you may just be practicing DDD theories on a simple example.

I also agree with what he says regarding eventual consistency. However, for the point of this exercise I don't think its necessary to add the infrastructure required for it and a simpler approach can be taken.

I'd say there are two ways to do this, and the choice depends on how many comments each post is likely to have. Disclaimer: I'm a C# programmmer, so forgive me if the php syntax is wrong (I assume it is php?)

  1. Single aggregate design

If there aren't likely to be hundreds of comments per post, I would model Comment as a child entity of Post, where Post is the only aggregate root. This way the hidden comment invariant is easy to enforce:

class Post {

    private $hidden;
    private $comments;

    public function isHidden() {
        return $this->hidden;
    }

    public function hide(){
        $hidden = true;
        foreach ($comments as $comment){
            $comment.hide();
        }
    }   

    public function addComment($comment){
        $comments.add($comment);
    }
}
  1. Individual Aggregate Roots

If there are likely to be hundreds of comments being added to a post, then you'd need to model it as individual aggregates. Otherwise the Post aggregate will become too large, and perhaps more importantly (and as plalx points out) you would potentially get concurrency conflicts on the Post aggregate where multiple comments are being added at the same time.

Doing it this way would involve using a Domain Service to handle the logic, rather than the caller using methods on the aggregates themselves:

class PostService {

    private $postRepository;
    private $commentRepository;

    public function hidePost($postId) {
        $post = $postRepository.GetById($postId);


        $post.hide();
        $postRepository.save($post);

        //Method 1: update each comment
        $comments = $commentRepository.GetCommentsByPostId($postId);
        foreach($comments as $comment){
            $comment.hide();
            $commentRepository.save($comment);
        }

        //Method 2: create specific update method on repository with performant update query            
        $commentRepository.hideCommentsForPost($postId);
    }
}

Note that the hide() methods on the aggregate would not be available publicly. In C# these are called internal methods, meaning only code in the same assembly can call them: the point being that callers are forced to use the PostService to hide a post, not the $post.hide() AR directly.

Also note, you should never reference another AR directly in a AR. You should reference other AR's by Id instead. See this for more info.

Community
  • 1
  • 1
David Masters
  • 8,069
  • 2
  • 44
  • 75
  • I'm not so sure about your Domain Service example. If there's a transaction around all that then it's no better than a single AR in terms of concurrency conflicts. Every AR shall be modified in it's own transaction. Am I missing something? – plalx Jun 09 '15 at 21:17
  • Well if there was a single AR and people where adding loads of comments simultaneously then concurrency conflicts would be likely. This method has separate AR's so new comments can be added without concurrency conflicts. – David Masters Jun 10 '15 at 07:51
  • I guess I was expecting someone to tell me that there is no problem in holding the reference to the `Post` AR in the `Comment` since the logic would be enforced right in the core Domain. So the "reference AR by ID" is one of those rules I definitely shouldn't break. I will be using a Domain Service to handle this kind of problem or eventually move these validations for upper layers as suggested. Thank you @plalx and @david – Joyce Jun 10 '15 at 18:32