{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreib7ezfjulzamokg2rbluujlhcib4w2elfkcipe6q6f3dv2i2pybhi",
    "uri": "at://did:plc:nrjtliwbmlrgcf5b24n7efpt/app.bsky.feed.post/3mjljkoszaxe2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreic42tz7nfq3odrk3dhlxjv2556qwdr4vkz6tyvhhbp6pfaqxdq5ii"
    },
    "mimeType": "image/jpeg",
    "size": 536144
  },
  "description": "We hold a Ruby Campfire at the company I work for, bi-weekly. It's not a real campfire of course we work remotely! Recently, we talked about Ruby delegated types in one of those meetings. It was really interesting. I used to use STI (Single Table Inheritance) to connect multiple tables, but it turns out there is a different and more effective solution.\n\n\nBackground\n\nSingle-table inheritance looks simple at first. One table, one type column, done. Rails makes it easy to set up.\n\nBut then the prob",
  "path": "/why-i-stopped-using-sti-and-started-using-delegated-types/",
  "publishedAt": "2026-04-16T04:00:54.000Z",
  "site": "https://enderahmetyurt.com",
  "tags": [
    "ActiveRecord::DelegatedType — Rails API",
    "Add delegated type to Active Record — DHH's original PR #39341",
    "DHH on X — Delegated types and domain modeling at Basecamp",
    "The Rails Delegated Type Pattern — 37signals Dev Blog",
    "Delegated Types are an alternative to STI — DEV Community",
    "Delegated Types in Rails — Medium / NYC Ruby on Rails",
    "The delegated type pattern and multi-table inheritance — Mateus Guimarães"
  ],
  "textContent": "We hold a Ruby Campfire at the company I work for, bi-weekly. It's not a real campfire of course we work remotely! Recently, we talked about Ruby delegated types in one of those meetings. It was really interesting. I used to use STI (Single Table Inheritance) to connect multiple tables, but it turns out there is a different and more effective solution.\n\n## Background\n\nSingle-table inheritance looks simple at first. One table, one `type` column, done. Rails makes it easy to set up.\n\nBut then the problems start.\n\nI've been writing Rails apps for over 14 years. I've seen the STI trap many times in my own code too. You start with two or three similar models, put them in one table, and move on. Six months later, the table has thirty columns. Most of them are `NULL` for any given row. The `type` column is the only thing keeping it together.\n\nThere's a better option. Rails has had it since 6.1. It's called delegated types.\n\n## What's Wrong With STI\n\nSTI works fine when your models are very similar. When they share most columns and only differ a little in behavior. The Rails docs say it clearly:\n\n> \"STI works best when there's little divergence between the subclasses and their attributes.\"\n\nA simple example: you have `Message` and `Comment`. You want to show both in a feed and paginate them together. STI lets you do that. But a `Message` has a `subject` column. A `Comment` doesn't. With STI, the comment row gets a `subject` column anyway. It's just always `NULL`.\n\nDo this with enough models and enough columns, and your table becomes a mess. It's hard to query, hard to index, and hard to understand.\n\nPolymorphic associations fix the sparse table problem, but they bring other issues: no foreign key constraints, more complex queries, and it gets confusing fast.\n\n## A Third Option\n\nDHH added delegated types to Rails 6.1. The idea is simple: keep the shared columns in one **\"superclass\"** table. Give each subclass its own table for the columns that are specific to it.\n\nDHH said this pattern had _\"the most profound impact on how we do domain modeling at Basecamp\"_. They used it in Basecamp 3 and then in HEY.\n\nHere's how it looks. You have an `Entry` model. It holds the shared stuff: `creator_id`, `account_id`, timestamps. Then `Message` and `Comment` each have their own tables with their own specific columns.\n\n\n    class Entry < ApplicationRecord\n      delegated_type :entryable, types: %w[ Message Comment ]\n      delegate :title, to: :entryable\n    end\n\n    class Message < ApplicationRecord\n      def title\n        subject\n      end\n    end\n\n    class Comment < ApplicationRecord\n      def title\n        content.truncate(20)\n      end\n    end\n\nNow you can call `entry.title` on anything in the feed. `Message` returns its subject. `Comment` returns a short preview. The caller doesn't need to care which one it is.\n\nThe schema stays clean. The `entries` table has only shared columns. The `messages` and `comments` tables have only their own columns. No `NULL` columns, no `if record.type == \"Message\"` checks in your views.\n\n## How Is This Different From Polymorphic Associations?\n\nDelegated types are built on top of polymorphic associations. DHH called it **\"syntactic sugar\"** in some early discussions. But the intent is different.\n\nWith normal polymorphic associations, you go from parent to child: fetch a `Post`, then get its `images`. With delegated types, you go the other way. You query from `Entry` and let it delegate to the specific type. `Entry` is the main entry point. `Message` and `Comment` are details.\n\nThis means you can write `Entry.all` and paginate across all types with no `UNION` queries. You can eager-load associations on `Entry`. You can build controllers around `Entry` that work for both messages and comments.\n\nThe 37signals team talked about this on their dev blog. Their version of `Entry` is called `recordings`. It stays small because it only holds foreign key references. The actual content lives in the specific tables. A small table is easy to index and easy to query. The content tables can grow on their own without making everything slow.\n\n## When to Use It\n\nDelegated types aren't always the right choice. If your models are very similar and you don't need pagination across types, STI is fine.\n\nBut if you see yourself doing any of these things, delegated types are worth trying:\n\n  * Adding columns to an STI table that only one model uses\n  * Writing `if record.type == \"Message\"` in views or controllers\n  * Having trouble paginating across different model types\n  * Building a feed, timeline, or activity log with different kinds of content\n\n\n\nThe `Entry`/`Message`/`Comment` example in the Rails docs looks simple. But 37signals has used this pattern in production for over ten years, across two big products.\n\n## One Thing to Know\n\nWhen you need to filter by a subtype-specific column, you need a join. If you want to filter by `messages.subject`, you join `entries` to `messages`. This is usually fine, but it's good to know before you start.\n\nThe 37signals engineers said the small size of the `entries` table makes this less of a problem. A small table is cheap to index.\n\n## How to Set It Up\n\nCreating a record is simple:\n\n\n    Entry.create!(\n      entryable: Comment.new(content: \"Hello!\"),\n      creator: Current.user,\n      account: Current.account\n    )\n\nRails gives you scopes and helpers automatically: `Entry.messages`, `Entry.comments`, `entry.message?`, `entry.comment?`. Rendering is clean too:\n\n\n    <%# entries/_entry.html.erb %>\n    <%= render \"entries/entryables/#{entry.entryable_name}\", entry: entry %>\n\nEach type gets its own partial. `Entry` doesn't need to know which one to pick.\n\n## Why It Matters\n\nRails is opinionated. It bets that good conventions make code easier to work with over time. Delegated types feel like one of those conventions not a new abstraction for its own sake, but a name and a structure for something developers were already doing by hand, usually in a messier way.\n\nIt came from DHH's own work at Basecamp. It ran in production. It survived scaling. It got refined over years before it became part of the framework.\n\nIf you've been using STI out of habit, it's worth reconsidering. Not every model hierarchy needs delegated types. But the ones that do will be much cleaner for it.\n\nIf you've used STI or delegated types in your own projects, I'd love to hear about it. Which one did you go with? Did delegated types make things easier, or did you run into problems I didn't mention here? And if you're still using STI is it working well, or are you starting to feel the pain? Let me know in the comments.\n\n**References**\n\n  * ActiveRecord::DelegatedType — Rails API\n  * Add delegated type to Active Record — DHH's original PR #39341\n  * DHH on X — Delegated types and domain modeling at Basecamp\n  * The Rails Delegated Type Pattern — 37signals Dev Blog\n  * Delegated Types are an alternative to STI — DEV Community\n  * Delegated Types in Rails — Medium / NYC Ruby on Rails\n  * The delegated type pattern and multi-table inheritance — Mateus Guimarães\n\n",
  "title": "Why I Stopped Using STI and Started Using Delegated Types",
  "updatedAt": "2026-04-16T04:00:54.328Z"
}