The Likes Table Problem: Why We Went Polymorphic.
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_likescomment_likesnews_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 tousers).resource_id– the UUID of the liked item (no foreign key).resource_type– the type of the item (POST,COMMENT, orNEWS).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.