This is the first installment of my chat building miniseries. In this article I will show some good practices and ideas how to handle likes and votes when you are building out your chat system.

THIS IS NOT A TUTORIAL, it is just a bunch of things I learned when I developed a chat system from scratch.

Why would you even do this?

Well this was the obvious question my coworkers asked when we decided to integrate a chat system in Studo back in 2016. The idea was to give our users a platform where they can exchange files and data. This project later became Studo Chat. Our goal was to replace the countless facebook and Whats-App group students use to discuss topics on ther various courses. Studo Chat has one channel where every student is automatically joined who takes said course.

But why make a chat system from scratch

So this is a very interesting question. Generally when there is a white-label solution on the market, it makes no sense financially to develop the same solution, because white-label solutions are made to integrate very well. So theoretically integrating a white-label chat system in Studo is faster and cheaper than developing one from scratch. The problem is that somehow at the time there was no good white-label chat system on the market, the ones we found were either not customizable enough or were crazy expensive.


Basic data model

Our basic chat data model consists of two classes, a Channel and a Message. At this point I should mention that this tutorial will use MongoDB-

// Channel
{
    id: "randomchannel-1",
    name: "Random Channel"
}

// Message
{
    id: "randommessage-1",+
    channelId: "randomchannel-1",
    text: "This is my first message!!"
}

The first obvious design decision is that a Message holds a reference (the unique ID) of a Channel and not the other way around. When a user creates a new message, there is no need to modify the Channel.

Now we add votes

At first this seems easy, the Message object just needs 2 counters like this:

// Message
{
    id: "randommessage-1",+
    channelId: "randomchannel-1",
    text: "This is my first message!!",
    upvotes: 1,
    downvotes: 0
}

Theoretically this would work, we can now easily show the user how many votes a message has, but sadly there is another necessary usecase.

Facebook not only shows how many likes this post has, it also shows that I already liked this post. With only 2 counters, it is impossible to do that, let’s try something else:

// Message
{
    id: "randommessage-1",+
    channelId: "randomchannel-1",
    text: "This is my first message!!"
}

// MessageVote
{
    id: "randomUniqueId",
    messageId: "randommessage-1",
    userId: "functionaldude",
    vote: "up"
}

Let’s introduce a new collection which holds all votes. In order to find out if a user voted we just query this collection like this:

{userId: "functionaldude", messageId: "randommessage-1"}

Of course for this query to work optimally, there needs to be a compound index containing these fields in this order!

The problem with this solution is that it doesn’t make it obvious how to display an accumulated count of the up- and downvotes. Luckily MongoDB has a really cool feature: aggregation. In order to get a result like this

{
  "id" : "randommessage-1",
  "up" : 1.0,
  "down" : 0.0
}

we just need to write an aggregation like this

[
    {$project: {
      _id: "$messageId",
      "up": {$cond: {if: {$eq: ["$vote","up"]}, then: 1, else: 0}},
      "down": {$cond: {if: {$eq: ["$vote","down"]}, then: 1, else: 0}}
    }},
    {$group: {
      _id: "$_id",
      up: {$sum: "$up"},
      down: {$sum: "$down"}
    }}
]

For optimal performance, this value can be cached inside of the Message object like this:

// Message
{
    id: "randommessage-1",+
    channelId: "randomchannel-1",
    text: "This is my first message!!",
    upvotes: 1, // Cached for optimal read performance
    downvotes: 0 // Cached for optimal read performance
}