# Reactions
The premise of this RFC is around users being able to react with emojis to a given post. Just like users can react to a given post in Slack.
In our case, the "post" will initially be an `Accomplishment` db record. And needs to be scaleable in that we can use the same reaction pattern off of other types of db records (i.e. `Comment`, `Reward`, etc.), in the future.
# Use case 1 - Reactions from multiple people, to multiple record types
A given `Accomplishment`, `RedeemedPrize`, `Comment`, and/or `Reward` db record could have:
- 4 '❤️' from John, Jack, and Sue
- 3 '👍' from John and Jane
- 1 '😎' from John
Another given `Accomplishment`, `RedeemedPrize`, `Comment`, and/or `Reward` db record could have:
- 1 '❤️' from John
- 1 '😂' from Jack
## Solutions
### Option 1 - Polymorphic `Reaction` table with jsonb `:reactions` column
jsonb `:reactions` column contains a hash of emoji unicode keys, each set to an array of `User#ids`.
**Reaction** table
| Column | Type | example |
| --------| -----| --------|
| reacted_to_type | string | "Accomplishment" |
| reacted_to_id | integer | 1 |
| emojis | jsonb | { "\u{1F970}" => [1, 4], "\u{1F933}" => [3] } |
**Create**
```ruby
emojis = {
"\u{1F970}" => [1, 2, 3],
"\u{1F933}" => [1, 4],
"\u{1F988}" => [1]
}
reaction = Reaction.create(reacted_to: accomplishment, emojis: emojis)
```
**Association**
```ruby
## will use a has_one: :belongs_to
reaction = accomplishment.reaction
```
**Fetch emojis**
```ruby
reaction.emojis
#=>
{
"\u{1F970}" => [1, 2, 3],
"\u{1F933}" => [1, 4],
"\u{1F988}" => [1]
}
```
**Update**
```ruby
## after we've confirmed key is present, and user is not already present in array
reaction.emojis["\u{1F970}"] << user.id
reaction.save
reaction.emojis
#=>
{
"\u{1F970}" => [1, 2, 3, 8],
"\u{1F933}" => [1, 4],
"\u{1F988}" => [1]
}
```
**Aggregate view data**
Here is the problem. There is no efficient way to query all of the reaction's users, without an `n + 1` query. Reason being, there is no db association for the varied user ids that reside in the json hash, in varied keys. Unless there is a way to do this (I was not able to find a suitable way)?
You would have to aggregate the data by looping over the collection, querying the `User` table in each iteration, like this:
```ruby
accomplishments = Accomplishment.includes(:reaction, :user).approved.where(users: { company_id: company_id })
def aggregate(accomplishments)
accomplishments.map do |accomplishment|
reactions = accomplishment
.reaction
.emojis
.map { |emoji, user_ids| { emoji => User.where(id: user_ids).to_a.uniq } }
{
accomplishment: accomplishment,
reactions: reactions
}
end
end
```
Note the `n + 1` call of `User.where(....)`. It is technically more than `n + 1`, b/c it is happening multiple (varied) times, per `Accomplishment` record, not just once. It would be happening once per emoji, per `Accomplishment`.
### Option 2 - jsonb column of `:reactions` on each model
We would add this same jsonb `:reactions` column instead to each relevant db table.
And add a model `concern` that encapsulates all of the emoji logic, that can be `included` into each model using it.
### Option 3 - Polymorphic `Reaction` table
**Reaction** table
| Column | Type | example |
| --------| -----| --------|
| reacted_to_type | string | "Accomplishment" |
| reacted_to_id | integer | 1 |
| emoji | string| "\u{1F970}" |
| user_id | integer | 1 |
**Create**
```ruby
Reaction.create(
reacted_to: accomplishment,
user: user_1,
emoji: "\u{1F970}"
)
Reaction.create(
reacted_to: accomplishment,
user: user_2,
emoji: "\u{1F970}"
)
```
**Aggregate view data**
```ruby
accomplishments = Accomplishment
.includes(reactions: :user)
.where(users: { company_id: 1 })
.map { |accomplishment| { accomplishment: accomplishment, reactions: accomplishment.reactions.group_by(&:emoji) } }
```
## Analysis
Here is some analysis of the three options, broken out by use cases.
_The initial `create` case_
- all three options require a db call
_The `update` case_
- options 1 and 2 will require two db hits, one to fetch the reaction, and one to update it, with custom logic
- option 3 requires one hit, to create a new record
_The `destroy` case_
- options 1 and 2 will require one db hit, with custom logic
- option 3 will require one db hit
_** The `fetch` case when loading the activity feed, data aggregation (counts/users), etc._
- options 1 and 2 are a huge performance hit; requires worse than an `n + 1` query repeatedly
- option 3 we would eager load records, and use custom logic to display counts and user names
_** The case of a race condition with multiple people updating around a single `Accomplishment` record_
- options 1 and 2 are always operating on one shared, common `Accomplishment` or `Reaction` record
- option 3 should not have any conflicts here, as it will always be working with a unique `Reaction` record, per request
# Use case 2 - Multiple people reacting to the same record at the same time
We will need to be able to handle a given db `Accomplishment` record being updated by multiple people, all at once.
## Solutions
### Option 1 - Optimistically set UI count plus CRUD actions through background jobs
Optimistically set the resulting count of any CRUD actions in the UI, and execute those actions in a background job. For example:
- a goal has a "👍" that 3 people reacted with
- user clicks the "👍" to add their reaction
- we immediately, optimistically, increment the count in the UI to 4
- we immediately trigger the background job to persist the new reaction
- next time the page reloads, accurate counts are displayed, should there be any question as to the count
### Option 2 - Normal synchronous actions
Make all calls and processes inline, syncronously.
# Recommendation
For the first use case I recommend option 3, the **Polymorphic `Reaction` table**. Reason being:
- The jsonb columns will have terrifically horrible performance when it comes to loading the activity feed, due to the varied `n + 1,2,3,4,5,...` queries per record. In option 3 we can eager load records, and use custom logic to display counts and user names.
- In the case of a race condition with multiple people updating around a single `Accomplishment` record, it is the only option that does not have any conflicts here, as it will always be working with a unique `Reaction` record, per request. Whereas options 1 and 2 are always operating on one shared, common `Accomplishment` or `Reaction` record.
- We can easily, conventionally validate that a given user cannot react more than once per record, per emoji.
- The create, update, and destroy cases are pretty much equal, and therefore do not carry much weight in the determination.
For the second use case I recommend option 1, the **Optimistically set UI w/background jobs**. Reason being:
- With social media, users have come to expect the UI for reactions to respond instantly. And with potentially multiple people all clicking at once, we cannot dependably count on our servers meeting there needs in a synchronous manner.
- Using background jobs in conjunction with the polymorphic `Reaction` table option, we will have a 1:1 ratio for a user's click to their unique db record. Thereby eliminating any race conditions or conflicts.
## Cons
The con with this approach is the number of `Reaction` db records being created. To that end, I spoke to a friend of mine at another company, and he said that in the last 2 months they have accrued ~1.3M reaction records, and this is only a fraction of their total db weight.
They have an account with Percona (https://www.percona.com/), a database performance company. Percona reports that they do not have a lot of data, and their db size is ~900GB.