Video summary

The Rails Delegated Type Pattern with Jeffrey Hardy

Main summary

Key takeaways

Technology

Overview

The episode explains how Base Camp (37signals / “3rd version chassis”) models many different content types—such as documents, messages, comments, uploads, and more—using the Rails delegated type pattern. It focuses on why this design scales and enables fast feature development.


Core concept: Recordings delegate to recordables

Most “content objects” in Base Camp share common behavior, including:

  • metadata
  • timestamps
  • creator information
  • parent/child relationships

They’re modeled using:

  • a primary table: recordings
  • plus one table per concrete content type called a recordable (e.g., message, comment, document, upload)

What a recording stores

A recording holds only common/shared data, such as:

  • recordable_id
  • recordable_type
  • creator and timestamps

It does not store type-specific content like:

  • message text
  • document body/content
  • file location/path

What a recordable stores

A recordable stores the “meat” that’s specific to that type (e.g., message content, document content, upload metadata).

Why recordings matter

The system is designed so recordings become the uniform interface for tasks like:

  • querying
  • caching
  • exporting
  • copying/moving
  • controllers/actions

Meanwhile, recordables store the type-specific data.


Why this matters vs common alternatives

The speaker contrasts delegated types with two common approaches:

  1. Single Table Inheritance (STI)

    • New types make the shared table wider with many nullable columns.
    • Migrations become harder and slower as the table grows.
  2. Separate tables + polymorphic associations

    • Useful when “things can belong to many parents.”
    • But it doesn’t provide the same uniform querying experience (single interface by a shared parent concept) that the team wants.

Mutability model: immutable recordables + mutable pointers (and events)

Base Camp uses a clear mutability split:

Recordables are treated as immutable

When content changes, the system creates a new recordable rather than editing the old one.

Recordings are mutable pointers

A recording points to the “latest” recordable. Updating a document’s content becomes:

  • creating a new document recordable
  • re-pointing the recording to the new recordable

Events track history

A separate events model stores history:

  • events reference a recording
  • and a recordable

This enables timeline/change-log features so the system can reconstruct how the record looked at a particular time.


Tree structure (hierarchy) built on recordings

Recordings form a parent/child tree:

  • containers (like message boards or buckets such as projects) have children recordings
  • examples mentioned:
    • message board → messages → comments → attachments (via additional recordable relationships)

Even if the conceptual tree mixes recordable types, Rails filtering conveniences allow querying children by:

  • recordable type

Rails delegated-type conveniences used by the product

Delegated types enable:

  • type-specific loading
    • e.g., recording.document vs generic recordable access
  • scoped queries/filters by delegated type
    • e.g., recordings.where(recordable_type: "Message")
  • generic controllers/views
    • one set of behavior can operate on recordings regardless of which recordable type they delegate to

Uniform querying across types (timeline / mixed feeds)

A major advantage is building mixed content feeds without querying multiple tables (messages, documents, uploads, etc.).

Instead:

  • query only recordings
  • filter by recordable_type only when needed

This supports:

  • pagination
  • efficient “one feed across content types” behavior

Generic product operations: one copier/controller/exporter

Operations run at the recording level and are delegated to recordables, enabling reuse across types:

  • Export
    • one exporter service can handle all recordable types
  • Copy/Move
    • one copier handles many content types
    • copying can be efficient because immutable recordables may be re-used
    • often, the system creates new recordings that point to existing immutable recordables
  • Controllers
    • fewer type-specific controllers/actions
    • e.g., a single “recordings” controller can trash/archive/restore diverse content types

Performance and storage considerations

Large recordings table

  • The recordings table grows very large (potentially billions of rows).
  • But it’s described as small on disk because it stores mostly:
    • metadata
    • foreign keys
    • no large text blobs

Heavier content stays in recordables

Content-heavy tables (like those storing large message bodies) are “bigger on disk,” so copying/indexing them would be more expensive.


Downsides / tradeoffs

The pattern introduces several tradeoffs:

  • Learning curve
    • developers can find delegated types less intuitive than typical Rails “rich model” patterns
    • with conventional Rails, you often open Message and see message-specific logic immediately
    • with delegated types, type logic tends to be more distributed/generic, with context flowing through the recording
  • Upfront abstraction cost
    • onboarding new developers can be harder initially

Despite this, the team argues it pays off:

  • once the generic capabilities pattern is understood, new features behave more like configuration
    • e.g., “make a type commentable/exportable/etc.”

Adapting to change without breaking

Adding new recordable types is described as relatively low-risk:

  • the mobile/API can expose a single recordings JSON API
  • clients can handle new types without separate builds
  • because recordings share fields/behavior

Migration guidance mentioned

Migration is possible but not trivial:

  • introduce the new recordings table design
  • create/rename/migrate existing concrete types into recordables under the new scheme
  • create new recordings that point to recordables during migration

The talk suggests this pattern is best adopted if you already anticipate such change.


Main speakers / sources

  • Jeffrey Hardy — principal programmer, 37signals / Base Camp team
  • Kimberly — host; product team, 37signals
  • Fernando — technical contributor; supports the technical aspects
  • Rails framework documentation — delegated type pattern referenced as a primary source of the pattern’s availability/behavior

Original video