This is a continuation of part 1.

If we wanted to implement non-hierarchical comments, like one sees on an old-fashioned blog, or in a conventional forum, that’s easy; you group them all together by a common post id and sort by time.

Hierarchical comments, in which comments have replies recursively as a kind of tree, are inherently different.

The most obvious choice for storage here would allow modeling such a structure natively: a graph database, perhaps one like Neptune, which does have a serverless offering. But it’s not "true" serverless, since you’re still paying per discrete unit of compute, not by storage. Aurora Serverless is the same way (paying by ACU) compared to DynamoDB, which is by row.

The starting point for Neptune Serverless is 1 NCU, which offers ~2GiB of memory for $115 USD per month. For Aurora Serverless v2, the starting point is 0.5 ACU, which offers ~1GiB of memory for $43 USD per month.

Note
While it’s trickier, it is possible to model graph structures in relational databases using recursive CTEs, which is partly why Aurora Serverless is mentioned here.

What we have isn’t quite a graph, or at least not a many-to-many graph: it’s a tree, with the root being the post and direct descendants being top-level comments, and the next level of descendants being replies, and the next level after that being replies-to-replies, etc. The comments at each level have some kind of sorting within their parent, either by new or best/top.

One thing that’s easy to overlook is at least in Reddit, not many comments are actually loaded. Overall, maybe 20 comments are loaded, but often at multiple levels, i.e. the top comment chain down to 5, even 10ish levels. Another alternative is to load the top 10 comments, and the top 3 replies for each. Yet another possibility is identifying the top 30 comments for a post (no matter what depth) and then displaying them and the intermediate comments. There are many possible approaches here, so…​it makes sense to experiment. The solution below uses DDB, but in practice for a giant user base, another storage system might make more sense (e.g. Neptune).

One pattern to model this in DynamoDB is using an adjacent list — basically source-target tuples describing "edges" between nodes, and in our case, also the nodes.

Here’s one possible schema:

Table 1. Comments schema
Name Type Required? Description

parent (PK)

STRING

Required

The source/origin/parent side of this edge. For top-level comments, this will look like Post:$post_id and point to the post. For replies, this will look like Comment:$comment_id and point to the parent comment.

comment_id (SK)

STRING

Required

The unique id for the comment. Will always be some unique identifier for this comment.

children_ids

LIST<STRING>

Required (but can be empty)

user_id

STRING

Required

This is the user that created the comment.

text

STRING

Required

The body of the comment.

created_at

NUMBER

Required

The time this comment was created, in seconds since epoch.

For the sake of this design, let’s go with the first alternative mentioned — top 10, then top 3 replies to each of those.

Then to load comments, we need to perform the following queries in sequence:

  • Load top 10 top-level comments (one Query to DDB)

  • Load top 3 replies (one Query to DDB per top-level comment, but can be done in parallel)

Overall, this looks like 11 queries per initial load, which is a bit expensive, but good enough for now; as hinted at above, it’s nigh-impossible to know a priori what the optimal data model is here.

Voting

For upvoting, we want something similar to what we do for posts:

if (user.upvote(comment)) {
  comment.upvotes++
  comment.net_score++
}

And similarly, I think we’d want net_votes, upvotes, and downvotes as fields as well, along with a GSI (or LSI, possibly) with parent (PK) and net_votes (SK) for Sort by top.