Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How would model a vote/like system in a DDD/CQRS/EventSourced project?

Here a brief explanation of my domain:

I've Article which are basically like any article (title, summary, and a body).

I need to allow votes on my articles, votes will be casted by anonymous users (no registration required, but a session will store votes, please don't focus on this).

In this domain, Article is my aggregate root.

I can't find a way to model my votes with the following requirements:

a Vote can be either I like or I don't like, it should be mutable (it can be changed over time or even canceled)

A guest user with associated session can only cast one vote per article.

So, should Vote be aggregate on its own?

something like

Class Vote { public function cast(ArticleId id, GuestSessionToken token, VoteValue value); }

But in this case how should I check for unicity? Using Eventual Consistency (it seems ok because I don't having some duplicate as far as they are rare).

Because if I add vote method to my Article aggregate, I'll have to replay history for each votes which will be casted which sounds fairly slow (given the fact I can have 100k votes per articles).

I know that performance and optimization should not be taken in account when designing the DDD way but here it is a specific problem I need to solve before to implement anything.

Any of you has done something similar before?

like image 693
Trent Avatar asked Nov 21 '14 09:11

Trent


1 Answers

Vote is an aggregate root on its own. If we think about associations "an article has many votes" then we are applying a relational approach and this make us incline towards the so criticized big aggregate approach consensual by the DDD community. Instead we want to focus on behaviour. We know already that an Article will not hold a collection of votes. Since a Vote will need its own lifecycle it will have its own global identity and so its own repository. An article is voted by users, it is a nice approach to give semantics to our domain model so playing with domain experts semantics we could say "an article is voted by the user"

anArticle.votedBy(aReader);

Remember that the user plays the role of a reader in this bounded context. The votedBy method is a factory method that is in a position of creating a Vote. Its implementation would be:

Article.votedBy(aReader) {
  return new Vote(this, aReader);
}

Always remembering that at the end a Vote will have the conceptual identifiers of an article and a reader, promoting the disconnected model instead of holding actual references to other aggregate roots. So the domain service would be the reader itself. Suppose that you model a rest interface

RestInterface.voteArticle(articleId) {
  reader = new Reader(articleRepository);
  reader.vote(articleId);
}

Reader.vote(anArticleId) {
  article = articleRepository.get(anArticleId);
  vote = article.votedBy(this);
  voteRepository.add(vote);
}

You should check for unique (making sure a user votes an article only once) by placing a composed unique constraint at the database level. This is the least intrusive way to check it, otherwise you should add another domain model that moderates votes. Maybe this new object makes more sense when different voting business rules are created, but for making sure a reader votes only once an article is enough and I think the most simplistic solution (that is to place the DB constraint). DDD is a learning process and as we learn new things about the domain we realize the user can hit Like or Dislike button on an article so we think about re-factoring what we did so far a little bit:

Article.likedBy(aReader) {
  return Vote.positiveByOn(aReader, this);
}

Article.dislikedBy(aReader) {
  return Vote.negativeByOn(aReader, this);
}

where both implementations are:

class Vote {

  readerId
  articleId
  typeId

  Vote(aReaderId, anArticleId, aType) {
    readerId = aReaderId
    articleId = anArticleId
    type = aType
  }

  public Enum VoteType { POSITIVE, NEGATIVE }

  Vote static positiveByOn(aReader, anArticle) {
    return new Vote(aReader.id, anArticle.id, POSITIVE);
  }

  Vote static negativeByOn(aReader, anArticle) {
    return new Vote(aReader.id, anArticle.id, NEGATIVE);
  }
}

Also, since the likedBy/dislikedBy factory method on the Article AR is creating Votes you could place a constraint saying an article cannot be voted more than N time or any other business scenario.

Hope it helps,

Sebastian.

like image 121
Sebastian Oliveri Avatar answered Nov 08 '22 23:11

Sebastian Oliveri