Skip to main content
This is a backend customization. You need to perform it in the backend directory if you used create-spree-app to set up your Spree application.
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 and Admin 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

Choosing the Right Approach

Before extending Spree models, consider which approach fits your needs best:
This tutorial uses decorators because we’re adding a structural association between models. For behavioral changes like callbacks, prefer Events instead - they’re easier to test and maintain.

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
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:
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
Run the migration:
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:
This creates app/models/spree/product_decorator.rb:
app/models/spree/product_decorator.rb

Step 3: Add the Brand Association to Product

Update the decorator to add the belongs_to association:
app/models/spree/product_decorator.rb

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
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

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
Register this partial to appear in the product form. Add to your initializer:
config/initializers/spree.rb

Step 7: Add Translation

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

Testing the Association

Verify everything works in the Rails console:

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

Adding Scopes

Adding Callbacks

For callbacks that trigger side effects (syncing to external services, sending notifications, etc.), use Events subscribers instead of decorator callbacks. Events are easier to test and won’t break during Spree upgrades.
Decorator approach (use only for simple, internal logic):
Events approach (recommended for side effects): subscribe to product.updated and react in a Spree::Subscriber — no decorator, no callback coupling. The Events & Webhooks tutorial builds this out fully, including external-system sync (OMS, warehouse) and outbound webhooks.

Adding Class Methods

Complete Files

Product Decorator

app/models/spree/product_decorator.rb

Brand Model (Updated)

app/models/spree/brand.rb
SEO features like slugs, meta titles, and FriendlyId are covered in the Slugs documentation.

Spree Initializer Additions

config/initializers/spree.rb
config/initializers/spree.rb

Next Step

Now that Brands are connected to Products, let’s expose them through the Store API:

6. API

Create API endpoints for brands and extend the Product API response