<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Minseok Kim</title>
    <description>The latest articles on Forem by Minseok Kim (@em3s).</description>
    <link>https://forem.com/em3s</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3740723%2Fc2d97fb7-454a-4467-89e7-e62f450e2d9a.jpeg</url>
      <title>Forem: Minseok Kim</title>
      <link>https://forem.com/em3s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/em3s"/>
    <language>en</language>
    <item>
      <title>Actionbase for Postgres/MySQL: When Likes, Views and Follows Stop Scaling</title>
      <dc:creator>Minseok Kim</dc:creator>
      <pubDate>Tue, 10 Feb 2026 13:00:00 +0000</pubDate>
      <link>https://forem.com/em3s/actionbase-for-postgresmysql-when-likes-views-and-follows-stop-scaling-4e64</link>
      <guid>https://forem.com/em3s/actionbase-for-postgresmysql-when-likes-views-and-follows-stop-scaling-4e64</guid>
      <description>&lt;p&gt;If your likes/follows/views tables are hitting any of these, this post is for you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Shard by user? Counting likes on an item scatters across shards.&lt;/li&gt;
&lt;li&gt;Shard by item? Listing a user's likes scatters instead.&lt;/li&gt;
&lt;li&gt;Cache invalidation logic that keeps growing.&lt;/li&gt;
&lt;li&gt;Reverse lookups and counts becoming a consistency problem.&lt;/li&gt;
&lt;/ul&gt;

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

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

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  From a Likes Table to an Edge
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;user_likes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;item_id&lt;/span&gt; &lt;span class="nb"&gt;BIGINT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_user&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;user_likes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;idx_item&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;user_likes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Actionbase, the same data is an edge — &lt;strong&gt;who&lt;/strong&gt; did &lt;strong&gt;what&lt;/strong&gt; to which &lt;strong&gt;target&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;table&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user_likes&lt;/span&gt;    &lt;span class="c1"&gt;# (what)&lt;/span&gt;
&lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LONG&lt;/span&gt;         &lt;span class="c1"&gt;# user_id (who)&lt;/span&gt;
&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LONG&lt;/span&gt;         &lt;span class="c1"&gt;# item_id (target)&lt;/span&gt;
&lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LONG&lt;/span&gt;   &lt;span class="c1"&gt;# epoch millis&lt;/span&gt;
&lt;span class="na"&gt;indexes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;recent&lt;/span&gt;           &lt;span class="c1"&gt;# created_at DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100 → like → 200 (created_at: 1707300000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ftbqosbsyhtjahexwp6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7ftbqosbsyhtjahexwp6.png" alt="like example" width="800" height="397"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a &lt;strong&gt;unique edge&lt;/strong&gt; use case: one user, one like, one item. The edge key is &lt;code&gt;(source, target)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing Writes for Reads
&lt;/h2&gt;

&lt;p&gt;In a relational database, you serve new read patterns by adding indexes, caches, and tuning queries as the workload evolves.&lt;/p&gt;

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

&lt;p&gt;One write updates the edge, reverse lookup, counts, and sort indexes. Reads become lookups.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;OUT = user → items, IN = item → users&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  SQL → Actionbase
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Edge lookup&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;likes&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET source=100, target=200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;List (forward / reverse)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;likes&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;likes&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;
 &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SCAN start=100, direction=OUT, index=recent
SCAN start=200, direction=IN,  index=recent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Count&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;likes&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;likes&lt;/span&gt;
 &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;item_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;COUNT start=100, direction=OUT
COUNT start=200, direction=IN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No aggregation. No cache. No scatter queries across shards.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Moves, What Stays
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
Under the hood, Actionbase currently runs on HBase. A lighter alternative backed by SlateDB is in progress.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/kakao/actionbase" rel="noopener noreferrer"&gt;kakao/actionbase&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>database</category>
      <category>distributed</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Actionbase: One Database for Likes, Views, and Follows</title>
      <dc:creator>Minseok Kim</dc:creator>
      <pubDate>Sun, 08 Feb 2026 15:55:56 +0000</pubDate>
      <link>https://forem.com/em3s/actionbase-one-database-for-likes-views-and-follows-2od8</link>
      <guid>https://forem.com/em3s/actionbase-one-database-for-likes-views-and-follows-2od8</guid>
      <description>&lt;p&gt;Every social platform needs the same features — likes, views, follows, bookmarks. And every team builds them from scratch, hitting the same walls: fan-out writes, slow counts, inconsistent reads.&lt;/p&gt;

&lt;p&gt;Actionbase is an open-source database that treats user interactions as a first-class data model. It exposes a REST API — just HTTP calls to read and write. It's been serving 1M+ requests per minute in production at Kakao, the company behind KakaoTalk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;A simple "like" button touches more than you'd think:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Did this user already like this post? → edge lookup&lt;/li&gt;
&lt;li&gt;How many likes does this post have? → count&lt;/li&gt;
&lt;li&gt;Show all posts this user liked, newest first → scan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With a general-purpose database, you're computing these on every read. At scale, that means slow queries, cache invalidation headaches, and duplicated logic across teams.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Actionbase Works
&lt;/h2&gt;

&lt;p&gt;Every interaction is modeled as a graph edge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user-123 → like   → post-456
user-123 → follow → user-789
user-123 → view   → product-012
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Who&lt;/strong&gt; did &lt;strong&gt;what&lt;/strong&gt; to which &lt;strong&gt;target&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When a write comes in, Actionbase precomputes all derived data — forward edges, reverse edges, indexes, and counts — in a single operation. Reads are just lookups.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbietinpm6c5u8cbrwmmv.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbietinpm6c5u8cbrwmmv.gif" alt="Actionbase Quick Start CLI demo" width="760" height="345"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-it&lt;/span&gt; ghcr.io/kakao/actionbase:standalone
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs server (port 8080) in background, CLI in foreground. Load sample data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;load preset likes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates 3 edges:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice ── likes ──&amp;gt; Phone
Bob ──── likes ──&amp;gt; Phone
Bob ──── likes ──&amp;gt; Laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query — precomputed, just read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;get --source Alice --target Phone               # Alice → Phone
scan --index recent --start Bob --direction OUT  # Bob's likes
scan --index recent --start Phone --direction IN # Phone's likers
count --start Alice --direction OUT              # 1
count --start Phone --direction IN               # 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  See It in Action
&lt;/h2&gt;

&lt;p&gt;What does it look like in a real app? Here's a social media demo powered by Actionbase — likes, follows, feeds, all backed by precomputed reads.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqlmyztb8qtambcc42mr4.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqlmyztb8qtambcc42mr4.gif" alt="Actionbase Demo" width="600" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;This is the first post in the &lt;strong&gt;Actionbase Stories&lt;/strong&gt; series. Upcoming posts will cover patterns for adopting Actionbase into real running systems — from gradual migration to async processing to CQRS integration.&lt;/p&gt;

&lt;p&gt;Actionbase wasn't built complete from day one. It was deployed early and evolved under production pressure — surviving incidents, earning trust, and adding verification layers along the way. Those stories are coming too.&lt;/p&gt;




&lt;p&gt;GitHub: &lt;a href="https://github.com/kakao/actionbase" rel="noopener noreferrer"&gt;kakao/actionbase&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome — issues, discussions, or comments here.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>database</category>
      <category>webdev</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
