---
title: Decorators
description: Use Spree decorators to add or modify behavior on core models, controllers, and helpers without forking the gem — patterns, file layout, and pitfalls.
---

> **WARNING:** **Decorators should be a last resort.** They tightly couple your code to Spree internals and can break during upgrades. Before using decorators, consider these modern alternatives that are safer and easier to maintain:
> 
>   - **[Events & Subscribers](../core-concepts/events.md)** - For reacting to model changes (after save, create, update, delete)
>   - **[Webhooks](../core-concepts/webhooks.md)** - For notifying external services when events occur
>   - **[Dependencies](dependencies.md)** - For swapping out services, serializers, and abilities
>   - **[Dashboard Navigation](../dashboard/customization/navigation.md)** - For adding menu items
>   - **[Dashboard Slots](../dashboard/customization/slots.md)** - For adding your own UI to an existing dashboard screen
>   - **[Dashboard Tables](../dashboard/customization/tables.md)** - For customizing list views

## When to Use Decorators vs Modern Alternatives

Before reaching for a decorator, check if your use case is better served by a modern alternative:

| Use Case | Instead of Decorator | Use This |
|----------|---------------------|----------|
| After-save hooks (sync to external service) | Model decorator with `after_save` | [Events subscriber](../core-concepts/events.md) |
| Notify external service on changes | Model decorator with callbacks | [Webhooks](../core-concepts/webhooks.md) |
| Custom add-to-cart logic | Service decorator | [Dependencies injection](dependencies.md) |
| Custom API responses | Serializer decorator | [Dependencies injection](dependencies.md) |
| Add a dashboard menu item | Controller decorator | [Navigation registry](../dashboard/customization/navigation.md) |
| Add a section to a dashboard form | View decorator/override | [Slots](../dashboard/customization/slots.md) |
| Narrow what an endpoint returns | Overriding an action | Controller decorator overriding `scope` |
| Accept a new attribute on write | Controller decorator | `Model.additional_permitted_attributes +=` |
| Add association to core model | - | Decorator (still appropriate) |
| Add validation to core model | - | Decorator (still appropriate) |
| Add new method to core model | - | Decorator (still appropriate) |
| Make a new column [filterable](../core-concepts/search-filtering.md) | - | Decorator (still appropriate) |

> **INFO:** Decorators are still appropriate for **structural changes** like adding associations, validations, scopes, and new methods to models. Use modern alternatives for **behavioral changes** like callbacks, hooks, and side effects.

## Overview

Spree's models, API controllers and helpers can be extended or overridden to meet your requirements using standard Ruby idioms.

The convention is a file under `server/app/models/spree` or `server/app/controllers/spree`, named after the original class with `_decorator` appended. The generators below place it for you.

> **NOTE:** There is nothing else to decorate. The dashboard is a React application that
>   talks to the Admin API, and the storefront is your own — so the only
>   controllers Spree ships are the API ones, and the only views are the emails.
>   Customize the dashboard through
>   [its own extension points](../dashboard/customization/quickstart.md).

## Why Use 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

```ruby
# 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.

## Generating Decorators

Spree provides generators to create decorator files with the correct structure:

### Model Decorator Generator


```bash Spree CLI (Docker)
spree generate model_decorator Spree::Product
```

```bash Without Spree CLI
bin/rails g spree:model_decorator Spree::Product
```


This creates `server/app/models/spree/product_decorator.rb`:

```ruby
module Spree
  module ProductDecorator
    def self.prepended(base)
      # Class-level configurations go here
    end
  end

  Product.prepend(ProductDecorator)
end
```

### Controller Decorator Generator


```bash Spree CLI (Docker)
spree generate controller_decorator Spree::Api::V3::Admin::ProductsController
```

```bash Without Spree CLI
bin/rails g spree:controller_decorator Spree::Api::V3::Admin::ProductsController
```


This creates `server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb`:

```ruby
module Spree::Api::V3::Admin
  module ProductsControllerDecorator
    def self.prepended(base)
      # base.before_action :my_filter
    end

    # add custom methods here
  end
end

Spree::Api::V3::Admin::ProductsController.prepend Spree::Api::V3::Admin::ProductsControllerDecorator
```

The generator accepts any namespace depth. `Spree::ProductsController` produces a top-level `module Spree` wrapper; `Spree::Api::V3::Store::ProductsController` produces a `module Spree::Api::V3::Store` wrapper. The final `.prepend` line is always fully qualified.

## Decorating Models

### Changing Behavior of Existing Methods

The most common use case is changing the behavior of existing methods. When overriding a method, you can call `super` to invoke the original implementation:

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def available?
      # Add custom logic before
      return false if discontinued?

      # Call the original method
      super
    end
  end

  Product.prepend(ProductDecorator)
end
```

> **WARNING:** Always consider whether you need to call `super` when overriding methods. Omitting it completely replaces the original behavior, which may break functionality.

### Adding New Methods

Add new instance methods directly in the decorator module:

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def featured?
      metadata[:featured] == true
    end

    def days_until_available
      return 0 if available_on.nil? || available_on <= Time.current
      (available_on.to_date - Date.current).to_i
    end
  end

  Product.prepend(ProductDecorator)
end
```

### Adding Associations

Use the `self.prepended(base)` callback to add associations:

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.belongs_to :brand, class_name: 'Spree::Brand', optional: true
      base.has_many :videos, class_name: 'Spree::Video', dependent: :destroy
    end
  end

  Product.prepend(ProductDecorator)
end
```

### Adding Validations

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.validates :external_id, presence: true, uniqueness: true
      base.validates :weight, numericality: { greater_than: 0 }, allow_nil: true
    end
  end

  Product.prepend(ProductDecorator)
end
```

### Adding Scopes

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.scope :featured, -> { where("metadata->>'featured' = ?", 'true') }
      base.scope :recently_added, -> { where('created_at > ?', 30.days.ago) }
      base.scope :on_sale, -> { joins(:variants).where('spree_prices.compare_at_amount > spree_prices.amount') }
    end
  end

  Product.prepend(ProductDecorator)
end
```

### Adding Class Methods

Use `extend` within the `prepended` callback to add class methods:

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.extend ClassMethods
    end

    module ClassMethods
      def search_by_name(query)
        where('LOWER(name) LIKE ?', "%#{query.downcase}%")
      end
    end
  end

  Product.prepend(ProductDecorator)
end
```

Usage:

```ruby
Spree::Product.search_by_name('shirt')
```

## Decorating Controllers

The only controllers Spree ships are the API ones, under
`Spree::Api::V3::Store` and `Spree::Api::V3::Admin`. There is no server-rendered
storefront or admin to decorate — the dashboard is a React app that talks to the
Admin API, and the storefront is yours.

> **WARNING:** **Reach for a decorator last.** A `ResourceController` subclass exposes named
>   hooks — `scope`, `permitted_params`, `serializer_class`, `collection_includes`
>   — and overriding one of those in a decorator is safe. Overriding an *action*
>   couples your code to Spree's internals and is what breaks on upgrade.

### Narrowing what an endpoint returns

The most common reason to decorate: restrict a listing beyond what the store
scope already does.

```bash
spree generate controller_decorator Spree::Api::V3::Admin::ProductsController
```

```ruby server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb
module Spree::Api::V3::Admin
  module ProductsControllerDecorator
    protected

    # `super` is already scoped to the current store. Chain onto it — never
    # replace it, or the endpoint stops being scoped and starts returning
    # another store's records. `current_user` is nil for a secret-key
    # request, hence `&.` — a caller that is not a staff member gets the
    # narrowed scope.
    def scope
      return super if current_user&.spree_admin?

      super.where(discontinue_on: nil)
    end
  end
end

Spree::Api::V3::Admin::ProductsController.prepend(
  Spree::Api::V3::Admin::ProductsControllerDecorator
)
```

`current_user` is the signed-in staff member, and `spree_admin?` asks whether
they hold the admin role for the current store.

> **WARNING:** `current_user` is only set when the request carries a staff token. A request
>   authenticated with a [secret API key](../../api-reference/admin-api/authentication.md)
>   has no user, so `current_user` is `nil` — guard for it, or a server-to-server
>   integration hits `NoMethodError` on every call.

### Accepting an extra attribute

If your decorator added a column to a core model, the controller has to permit
it — but you rarely need a decorator for that. Extensions append to the model's
own list from an initializer:

```ruby server/config/initializers/spree.rb
Spree::Product.additional_permitted_attributes += [:brand_id]
```

Always `+=`, never `=`, or you drop what another extension added.

### A new endpoint is a new controller

To add an action, write your own controller rather than decorating one of
Spree's. It inherits the same pagination, filtering and authorization:

```ruby server/app/controllers/spree/api/v3/admin/product_audits_controller.rb
module Spree::Api::V3::Admin
  class ProductAuditsController < ResourceController
    # Names the API-key scope this endpoint needs: `read_products` for
    # index/show, `write_products` for everything else. Every Admin API
    # controller must declare one.
    scoped_resource :products

    protected

    def model_class
      Spree::ProductAudit
    end

    def serializer_class
      MyApp::ProductAuditSerializer
    end
  end
end
```

Add the route to your application's `config/routes.rb`, inside the engine's
route hook. See [extending the API](api.md).

> **INFO:** **For side effects, use events rather than a decorator.** Subscribing to
>   `product.created` runs your code for every caller — the dashboard, a secret
>   API key, an import, the console. A controller override only fires for
>   requests that happen to pass through that controller. See
>   [Events](../core-concepts/events.md).

## Best Practices


  - **Use the prepended callback** — Always use `self.prepended(base)` for class-level additions like associations, validations, scopes, and callbacks.

  - **Keep decorators focused** — Each decorator should have a single responsibility. Create multiple decorators for different concerns if needed.

  - **Call super when overriding** — When overriding methods, call `super` to preserve original behavior unless you intentionally want to replace it entirely.

  - **Test decorated behavior** — Write tests specifically for your decorated functionality to catch regressions during upgrades.


### Organizing Multiple Decorators

If you have many customizations for a single class, consider splitting them into focused decorators:

```
server/app/models/spree/
├── product_decorator.rb           # Main decorator (loads others)
├── product/
│   ├── brand_decorator.rb         # Brand association
│   ├── inventory_decorator.rb     # Inventory customizations
│   └── seo_decorator.rb           # SEO-related methods
```

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    include Product::BrandDecorator
    include Product::InventoryDecorator
    include Product::SeoDecorator
  end

  Product.prepend(ProductDecorator)
end
```

Each focused module is a plain module — only the top-level decorator calls
`prepend`. Autoloading resolves them from their paths, so nothing needs
requiring by hand.

## Common Pitfalls

### Forgetting to Call Super

```ruby
# ❌ Bad - completely replaces original behavior
def available?
  in_stock? && active?
end

# ✅ Good - extends original behavior
def available?
  super && custom_availability_check
end
```

### Using Instance Variables in prepended

```ruby
# ❌ Bad - instance variables don't work in prepended
def self.prepended(base)
  @custom_setting = true  # This won't work as expected
end

# ✅ Good - use class attributes or methods
def self.prepended(base)
  base.class_attribute :custom_setting, default: true
end
```

### Circular Dependencies

Be careful when decorators depend on each other:

```ruby
# ❌ Bad - can cause loading issues
# product_decorator.rb
def self.prepended(base)
  base.has_many :variants  # Variant decorator might not be loaded yet
end

# ✅ Good - use strings for class names
def self.prepended(base)
  base.has_many :variants, class_name: 'Spree::Variant'
end
```

## Migrating from Decorators to Modern Patterns

If you have existing decorators that use callbacks for side effects, consider migrating them to Events subscribers for better maintainability.

### Example: Migrating an After-Save Callback

**Before (Decorator with callback):**

```ruby server/app/models/spree/product_decorator.rb
module Spree
  module ProductDecorator
    def self.prepended(base)
      base.after_save :sync_to_external_service
    end

    private

    def sync_to_external_service
      ExternalSyncJob.perform_later(self) if saved_change_to_name?
    end
  end

  Product.prepend(ProductDecorator)
end
```

**After (Events subscriber):**

```ruby server/app/subscribers/product_sync_subscriber.rb
class ProductSyncSubscriber < Spree::Subscriber
  subscribes_to 'product.updated'

  def handle(event)
    product = Spree::Product.find_by_prefix_id(event.payload['id'])
    return unless product

    ExternalSyncJob.perform_later(product)
  end
end
```

The payload is the record as the API serializes it, so it carries the new
values rather than a list of what changed. When a subscriber only cares about
one attribute, compare against what you last synced rather than looking for a
changeset in the event.

### Benefits of Migration


  - **Loose coupling** — Your code doesn't depend on Spree internals. Events provide a stable interface.

  - **Easier upgrades** — Events-based code is less likely to break when Spree is updated.

  - **Better testability** — Subscribers can be tested in isolation without loading the full model.

  - **Async by default** — Subscribers run via ActiveJob, keeping your requests fast.


## Related Documentation

- [Events](../core-concepts/events.md) - Learn about Spree's event system
- [Webhooks](../core-concepts/webhooks.md) - HTTP callbacks for external integrations
- [Dependencies](dependencies.md) - Swap core services with your own
- [Extending the API](api.md) - Add your own endpoints
- [Dashboard customization](../dashboard/customization/quickstart.md) - Extend the dashboard from its own React app
- [Extending Core Models Tutorial](../tutorial/model-and-api.md) - Step-by-step guide to connecting custom models with Spree core
- [Customization Overview](quickstart.md) - General customization patterns
