Yiwei Yu

Posted on: 2026-01-08

How to Design & Build a Comment System

Comment system is probably the most complicated feature in my whole project, because it’s not just CRUD — it involves real data structures and performance trade-offs.

Let’s start with the frontend.

Comments naturally have replies, and indentation is the most intuitive way to represent them. However, screen width is always limited, especially on mobile devices. If every reply level were displayed using indentation, more than ten nested replies would be enough to consume the entire width of even a 27-inch monitor.

So my solution is: only indent one level of replies. All deeper replies are flattened and displayed by pointing to their parent comments with arrows.

The Real Complexity: Backend Data Organization

In the database, comments are essentially a self-referencing table: each comment may have a parent_id pointing to another comment.

@Entity
@Table(name = "yu_comment")
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "comment_seq")
    @SequenceGenerator(name = "comment_seq", sequenceName = "comment_seq", allocationSize = 1)
    private Long id;
    private String username;
    private String userId;
    private String avatar;
    @Column(columnDefinition = "TEXT")
    private String content;
    private String userEmail;
    @Column(nullable = false)
    private ZonedDateTime createTime;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "article_id")
    private Article article;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_comment_id")
    private Comment parentComment;

    @OneToMany(mappedBy = "parentComment", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<Comment> replyComments = new ArrayList<>();
}

This naturally forms a tree, not a simple list. And once a tree structure meets ORM frameworks, sorting requirements, and performance constraints, the complexity rises very quickly.

So the first principle of my backend design is: 

Never access the database inside recursion.

Step 1: Fetch Everything in One Query

The first thing I do is load all comments of the article with a single query:

List<Comment> all = commentRepository.findByArticleIdOrderByCreateTimeAsc(articleId);

This brings three benefits:

 • Avoids the N+1 query problem

 • All further processing happens in memory

 • The overall time complexity becomes predictable

From this point on, the comment system shifts from being “database-driven” to “data-structure-driven.”

Step 2: Keep Entities Clean, Use DTOs for the Frontend

Instead of sending entities directly to the frontend, I convert them into DTOs:

Map<Long, CommentView> viewMap = new HashMap<>(all.size());
        for (Comment c : all) {
            CommentView v = new CommentView();
            v.setId(c.getId());
            v.setUsername(c.getUsername());
            v.setUserId(c.getUserId());
            v.setAvatar(c.getAvatar());
            v.setContent(c.getContent());
            v.setUserEmail(c.getUserEmail());
            v.setCreateTime(c.getCreateTime().withZoneSameInstant(userZoneId)); // 不再 set 回 entity
            viewMap.put(v.getId(), v);
        }

(I’ll talk more about why DTOs are important in another article.)

So in memory I now have: commentId → CommentView

This is the foundation of the whole system, because later I can locate any DTO in O(1) time.

The frontend needs to know who this reply is replying to. So I do a second pass and attach a lightweight parentInfo (parent id/username/userId) to every non-top-level comment.

        for (Comment c : all) {
            if (c.getParentComment() != null) {
                Comment parent = c.getParentComment();
                CommentView v = viewMap.get(c.getId());
                v.setParentComment(new CommentView.ParentInfo(
                        parent.getId(),
                        parent.getUsername(),
                        parent.getUserId()
                ));
            }
        }

Step 3: Build an Adjacency List Instead of Recursing Immediately

Next, reorganize the structure in memory:

        Map<Long, List<CommentView>> childrenByParentId = new HashMap<>();
        List<CommentView> topLevel = new ArrayList<>();

        for (Comment c : all) {
            CommentView v = viewMap.get(c.getId());
            if (c.getParentComment() == null) {
                topLevel.add(v);
            } else {
                Long pid = c.getParentComment().getId();
                childrenByParentId.computeIfAbsent(pid, k -> new ArrayList<>()).add(v);
            }
        }

This converts scattered parent-child relationships in the database into a real graph structure in memory. At this point, the database schema has completely exited the stage.

Step 4: turn a tree into a controlled timeline

Since the frontend only indents one level, all deeper replies must be flattened into a timeline.

For each top-level comment, run a DFS traversal to collect all its descendants. After all replies are collected, I perform a final sort:

        for (CommentView top : topLevel) {
            List<CommentView> flattened = new ArrayList<>();
            collectAllReplies(top.getId(), childrenByParentId, flattened, new HashSet<>());
            flattened.sort(Comparator.comparing(CommentView::getCreateTime));
            top.setReplyComments(flattened);
        }

That's all.

The real challenge here is transforming:

a tree stored in the database → a graph built in memory → a timeline consumed by the frontend.

Although a comment system looks simple on the surface, it actually involves many important concepts: data structures (adjacency lists + DFS), ORM performance (avoiding N+1 queries), and frontend-backend contract design (two-level display structure).

This is a real test.




Comments (
)
Sign in to comment
0/500
Comment