Decoupling Polymorphic Types From Class Names in Rails

When a Rails model moves into a new module, its namespace changes — and so does the string Rails stores in the polymorphic_type column. That quietly breaks existing polymorphic associations. A team at Shopify, while relocating models under a new component, hit this exact problem and chose to stop storing class names in the database entirely. Here's how they did it.

The example throughout uses a Vehicle that has_one :key, where the Key belongs_to :vehicle. A Vehicle can be a Car or a Boat. Rails stores the parent's class name — "Car" or "Boat" — in the child's vehicle_type column.

Changing a polymorphic_type in Rails

The Problem: Namespace Changes Break Associations

When the team moved the Car and Boat models under the Garage module, new records began storing "Garage::Car" as the vehicle_type, while older records still had "Car". This split the data set into two incompatible groups.

The association stopped working in both directions. A Key with vehicle_type: "Car" wouldn't find its Garage::Car parent, because ActiveRecord looks up the target using the single value returned by polymorphic_name. Even an update to the association code wouldn't help unless it knew about both possible values.

Fixing this the conventional way would require two things: teaching the association to read from multiple polymorphic types and backfilling existing records to the new namespace. The team wanted a more durable approach — one that could tolerate future renames without touching the database.

Storing Arbitrary Strings Instead of Class Names

The core idea was to stop storing class names altogether. Instead of "Garage::Car" or "Car", they'd store a simple arbitrary string like "car". Rails provides a hook for exactly this: overriding the polymorphic_name method.

Rails' default implementation, taken from the gem's inheritance.rb, looks like this:

def polymorphic_name
  base_class.name
end

For the example, the override in Garage::Car (and similarly Garage::Boat) would map the class to its intended string:

def self.polymorphic_name
  "car"
end

New Key records now store vehicle_type: "car" instead of the class name.

The Transition Problem

The immediate issue: the database now contains both old and new values. A Key with vehicle_type: "Car" and another with vehicle_type: "car" both point to the same model, but the default association scope only searches for one value at a time.

The fix is to explicitly broaden the association scope to match both possibilities:

class Garage::Vehicle < ApplicationRecord
  has_one :key, -> { unscope(where: :vehicle_type).where(vehicle_type: ["car", "Car"]) }
end

With this scope, the generated SQL looks for keys having either "car" or "Car" as the vehicle_type.

Backfilling and Cleaning Up

Once both values are readable, the team can run a backfill to normalize the data. At Shopify, they used the MaintenanceTasks gem, but a migration or script works the same way. After the cleanup completes, only arbitrary strings remain in the column, and the .unscope can be removed from the association definitions.

The resulting code relies on a mapping table — a few hashes that connect class names to their arbitrary strings. For example:

CLASS_MAPPING = {
  "Garage::Car" => "car",
  "Garage::Boat" => "boat"
}

Renaming a class later means updating the keys and values in these hashes only. The database values don't change.

Is This Worth the Complexity?

The trade-off is real. This pattern adds indirection, and most applications will never need it. For Shopify's Payment Flexibility team, it was justified: they knew the module and class names were likely to change as the project's structure took shape. The added complexity was a reasonable price for decoupling the database from the Ruby namespace. This simplified version of the approach is presented here as the solution they wish they'd found earlier.