Skip to main content
In this tutorial, we’ll connect our custom Brand model with Spree’s core Product model. This is a common pattern when building features that need to integrate with existing Spree functionality.
This guide assumes you’ve completed the Model, Admin, and File Uploads tutorials.

What We’re Building

By the end of this tutorial, you’ll have:
  • Products associated with Brands
  • A brand selector in the Product admin form
  • Understanding of how to safely extend Spree core models

Understanding Decorators

When working with Spree, you’ll often need to add functionality to existing models like Spree::Product or Spree::Order. However, you shouldn’t modify these files directly because:
  1. Upgrades - Your changes would be lost when updating Spree
  2. Maintainability - It’s hard to track what you’ve customized
  3. Conflicts - Direct modifications can conflict with Spree’s code
Instead, we use decorators - a Ruby pattern that lets you add or modify behavior of existing classes without changing their original source code.

How Decorators Work

In Ruby, classes are “open” - you can add methods to them at any time. Decorators leverage this by:
  1. Creating a module with your new methods
  2. Using Module#prepend to inject your module into the class’s inheritance chain
  3. Your methods run first, and can call super to invoke the original method
# This is the basic pattern
module Spree
  module ProductDecorator
    # Add a new method
    def my_new_method
      "Hello from decorator!"
    end

    # Override an existing method
    def existing_method
      # Do something before
      result = super  # Call the original method
      # Do something after
      result
    end
  end

  Product.prepend(ProductDecorator)
end
The key line is Product.prepend(ProductDecorator) - this inserts your module at the beginning of the method lookup chain, so your methods are found first.

Step 1: Create the Migration

First, add a brand_id column to the products table:
bin/rails g migration AddBrandIdToSpreeProducts brand_id:integer:index
Edit the migration to add an index (but no foreign key constraint, keeping it optional):
db/migrate/XXXXXXXXXXXXXX_add_brand_id_to_spree_products.rb
class AddBrandIdToSpreeProducts < ActiveRecord::Migration[8.0]
  def change
    add_column :spree_products, :brand_id, :integer
    add_index :spree_products, :brand_id
  end
end
Run the migration:
bin/rails db:migrate
We intentionally don’t add a foreign key constraint. This keeps the association optional and avoids issues if brands are deleted. Spree follows this pattern for flexibility.

Step 2: Generate the Product Decorator

Spree provides a generator to create decorator files with the correct structure:
bin/rails g spree:model_decorator Spree::Product
This creates app/models/spree/product_decorator.rb:
app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      # Class-level configurations go here
    end
  end

  Product.prepend(ProductDecorator)
end

Step 3: Add the Brand Association to Product

Update the decorator to add the belongs_to association:
app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
    end
  end

  Product.prepend(ProductDecorator)
end

Understanding the Code

  • self.prepended(base) - This callback runs when the module is prepended to a class. The base parameter is the class being decorated (Spree::Product)
  • base.belongs_to - We call class methods on base to add associations, validations, scopes, etc.
  • optional: true - Products don’t require a brand (the brand_id can be NULL)

Step 4: Add Products Association to Brand

Now update your Brand model to define the reverse association:
app/models/spree/brand.rb
module Spree
  class Brand < Spree::Base
    # ... existing code ...

    has_many :products, class_name: 'Spree::Product', dependent: :nullify

    # ... rest of your model ...
  end
end
We use dependent: :nullify instead of dependent: :destroy. When a brand is deleted, products will have their brand_id set to NULL rather than being deleted. This is safer for e-commerce data.

Step 5: Permit the Brand Parameter

For the admin form to save the brand association, we need to permit the brand_id parameter. Add to your Spree initializer:
config/initializers/spree.rb
# .. other code ..

Spree::PermittedAttributes.product_attributes << :brand_id

Step 6: Add Brand Selector to Product Admin Form

Create a partial to inject the brand selector into the product form. Spree’s admin product form has injection points for customization. Create the partial:
app/views/spree/admin/products/_brand_field.html.erb
<div class="card mb-4">
  <div class="card-header">
    <h5 class="card-title"><%= Spree.t(:brand) %></h5>
  </div>
  <div class="card-body">
  <%= f.spree_select :brand_id,
      Spree::Brand.order(:name).pluck(:name, :id),
      { include_blank: true, label: Spree.t(:brand) } %>
</div>
Register this partial to appear in the product form. Add to your initializer:
  • Spree 5.2+
  • Spree 5.1 and below
config/initializers/spree.rb
Spree.admin.partials.product_form_sidebar << 'spree/admin/products/brand_field'

Step 7: Add Translation

Add the translation for the brand label:
config/locales/en.yml
en:
  spree:
    brand: Brand

Testing the Association

Verify everything works in the Rails console:
# Create a brand and product
brand = Spree::Brand.create!(name: 'Nike')
product = Spree::Product.first
product.update!(brand: brand)

# Test associations
product.brand        # => #<Spree::Brand id: 1, name: "Nike"...>
brand.products       # => [#<Spree::Product...>]
brand.products.count # => 1

Decorator Best Practices

Use prepended callback

Always use self.prepended(base) for class-level additions like associations, validations, and scopes.

Keep decorators focused

Each decorator should have a single responsibility. Create multiple decorators if needed.

Call super when overriding

When overriding methods, call super to preserve original behavior unless you intentionally want to replace it.

Test decorated behavior

Write tests specifically for your decorated functionality to catch regressions.

Common Decorator Patterns

Adding Validations

def self.prepended(base)
  base.validates :custom_field, presence: true
end

Adding Scopes

def self.prepended(base)
  base.scope :featured, -> { where(featured: true) }
end

Adding Callbacks

def self.prepended(base)
  base.before_save :do_something
end

def do_something
  # Your callback logic
end

Adding Class Methods

def self.prepended(base)
  base.extend ClassMethods
end

module ClassMethods
  def my_class_method
    # Class method logic
  end
end

Complete Files

Product Decorator

app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
    end
  end

  Product.prepend(ProductDecorator)
end

Brand Model (Updated)

app/models/spree/brand.rb
module Spree
  class Brand < Spree::Base
    has_many :products, class_name: 'Spree::Product', dependent: :nullify

    has_one_attached :logo
    has_rich_text :description

    validates :name, presence: true
  end
end
SEO features like slugs, meta titles, and FriendlyId are covered in the SEO tutorial.

Spree Initializer Additions

config/initializers/spree.rb
# Permit brand_id in product params
Spree::PermittedAttributes.product_attributes << :brand_id
  • Spree 5.2+
  • Spree 5.1 and below
config/initializers/spree.rb
# Add brand field to product form
Spree.admin.partials.product_form << 'spree/admin/products/brand_field'