DEV Community

Cover image for Actionbase for Postgres/MySQL: When Likes, Views and Follows Stop Scaling
Minseok Kim
Minseok Kim

Posted on

Actionbase for Postgres/MySQL: When Likes, Views and Follows Stop Scaling

If your likes/follows/views tables are hitting any of these, this post is for you:

  • Shard by user? Counting likes on an item scatters across shards.
  • Shard by item? Listing a user's likes scatters instead.
  • Cache invalidation logic that keeps growing.
  • Reverse lookups and counts becoming a consistency problem.

Actionbase doesn't solve all database problems. But scaling "who did what to which target" tables — likes, views, follows, wishlists, subscriptions — is exactly what it was built for. It's been serving 1M+ requests/min in production at Kakao.

This post shows how a likes table maps to Actionbase's edge model — and where it fits next to Postgres/MySQL.

If you haven't hit the wall yet, stick with your relational DB. A table, some indexes, a cache — that works for a long time.

From a Likes Table to an Edge

CREATE TABLE user_likes (
  user_id BIGINT NOT NULL,
  item_id BIGINT NOT NULL,
  created_at TIMESTAMP NOT NULL,
  PRIMARY KEY (user_id, item_id)
);

CREATE INDEX idx_user ON user_likes (user_id, created_at DESC);
CREATE INDEX idx_item ON user_likes (item_id, created_at DESC);
Enter fullscreen mode Exit fullscreen mode

In Actionbase, the same data is an edge — who did what to which target:

table: user_likes    # (what)
source: LONG         # user_id (who)
target: LONG         # item_id (target)
properties:
  created_at: LONG   # epoch millis
indexes:
  - recent           # created_at DESC
Enter fullscreen mode Exit fullscreen mode
100 → like → 200 (created_at: 1707300000)
Enter fullscreen mode Exit fullscreen mode

like example

This is a unique edge use case: one user, one like, one item. The edge key is (source, target).

Designing Writes for Reads

In a relational database, you serve new read patterns by adding indexes, caches, and tuning queries as the workload evolves.

Actionbase does it upfront: at write time, materialize the read-optimized structures you'll need for GET/COUNT/SCAN.

One write updates the edge, reverse lookup, counts, and sort indexes. Reads become lookups.

OUT = user → items, IN = item → users

SQL → Actionbase

Edge lookup

SELECT * FROM likes
 WHERE user_id=100 AND item_id=200
Enter fullscreen mode Exit fullscreen mode
GET source=100, target=200
Enter fullscreen mode Exit fullscreen mode

List (forward / reverse)

SELECT * FROM likes
 WHERE user_id=100
 ORDER BY created_at DESC

SELECT * FROM likes
 WHERE item_id=200
 ORDER BY created_at DESC
Enter fullscreen mode Exit fullscreen mode
SCAN start=100, direction=OUT, index=recent
SCAN start=200, direction=IN,  index=recent
Enter fullscreen mode Exit fullscreen mode

Count

SELECT COUNT(*) FROM likes
 WHERE user_id=100

SELECT COUNT(*) FROM likes
 WHERE item_id=200
Enter fullscreen mode Exit fullscreen mode
COUNT start=100, direction=OUT
COUNT start=200, direction=IN
Enter fullscreen mode Exit fullscreen mode

No aggregation. No cache. No scatter queries across shards.

What Moves, What Stays

Domain data — user profiles, product catalogs, orders — stays in Postgres/MySQL. The high-volume interaction tables move to Actionbase. Start with one table: the one causing the most pain. At Kakao, that was the Gift wish list.
Under the hood, Actionbase currently runs on HBase. A lighter alternative backed by SlateDB is in progress.

GitHub: kakao/actionbase

Top comments (0)