The Likes Table Problem: Why We Went Polymorphic.

Published: (February 21, 2026 at 11:26 AM EST)
3 min read
Source: Dev.to

Source: Dev.to

Introduction

A few days ago I was adding a Community section to an application. Users needed to be able to:

  • Create posts
  • Leave comments
  • Like posts
  • Like comments

We also had a separate News section, and the new requirement was that users should be able to like news articles as well.

Initial Approach: Separate Like Tables

If only one entity could be liked (e.g., News), the schema would be simple:

CREATE TABLE news_like (
    user_id   BIGINT   NOT NULL REFERENCES users(id),
    news_id   BIGINT   NOT NULL REFERENCES news(id),
    liked_at  TIMESTAMP NOT NULL DEFAULT now()
);

Because we had three entities—posts, comments, and news—we considered creating three separate tables:

  • post_likes
  • comment_likes
  • news_likes

Each table would have proper foreign‑key relationships, giving us:

  • Strong relational integrity
  • Easy joins
  • Explicit structure

While this is the most “pure relational” approach, it felt repetitive. Adding another likable entity in the future would require yet another table, causing the schema to grow horizontally.

Polymorphic Likes Table

Instead of multiple tables, we created a single polymorphic likes table that can reference any resource type.

CREATE TABLE likes (
    user_id       BIGINT      NOT NULL REFERENCES users(id),
    resource_id   UUID        NOT NULL,
    resource_type TEXT        NOT NULL CHECK (resource_type IN ('POST','COMMENT','NEWS')),
    liked_at      TIMESTAMP   NOT NULL DEFAULT now()
);

Columns

  • user_id – who liked the item (foreign key to users).
  • resource_id – the UUID of the liked item (no foreign key).
  • resource_type – the type of the item (POST, COMMENT, or NEWS).
  • liked_at – timestamp of the like.

The pair (resource_id, resource_type) uniquely identifies the liked entity. This design gives us a single, centralized table that is easy to extend and reuse across the system.

Trade‑offs

The main downside is the loss of direct foreign‑key enforcement on resource_id. PostgreSQL cannot enforce a foreign key that points to multiple tables, so referential integrity must be handled in application code.

Example query to count likes for a specific resource:

SELECT COUNT(*)
FROM likes
WHERE resource_id = 'some-uuid'
  AND resource_type = 'POST';

If we need the resource details together with its likes, we may have to issue separate queries or add application‑side logic. In our case this trade‑off was acceptable because we don’t perform heavy cross‑entity joins on likes.

Conclusion

The key insight was that “like” is a behavior shared across resources, not a feature tightly coupled to posts, comments, or news. Modeling it polymorphically treats “like” as a reusable system capability, reducing schema duplication and future refactoring effort as the application grows.

0 views
Back to Blog

Related posts

Read more »

Undefined vs Not Defined

Undefined undefined is a special keyword in JavaScript. It means the variable exists in memory, but no value has been assigned yet. During the creation phase o...