Video summary
The Rails Delegated Type Pattern with Jeffrey Hardy
Main summary
Key takeaways
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_idrecordable_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:
-
Single Table Inheritance (STI)
- New types make the shared table wider with many nullable columns.
- Migrations become harder and slower as the table grows.
-
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.documentvs generic recordable access
- e.g.,
- scoped queries/filters by delegated type
- e.g.,
recordings.where(recordable_type: "Message")
- e.g.,
- 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_typeonly 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
recordingstable 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
Messageand 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
recordingstable 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