Skip to main content
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:

When to Use Decorators vs Modern Alternatives

Before reaching for a decorator, check if your use case is better served by a modern alternative:
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.
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.

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

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

Controller Decorator Generator

This creates server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb:
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:
server/app/models/spree/product_decorator.rb
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:
server/app/models/spree/product_decorator.rb

Adding Associations

Use the self.prepended(base) callback to add associations:
server/app/models/spree/product_decorator.rb

Adding Validations

server/app/models/spree/product_decorator.rb

Adding Scopes

server/app/models/spree/product_decorator.rb

Adding Class Methods

Use extend within the prepended callback to add class methods:
server/app/models/spree/product_decorator.rb
Usage:

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.
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.
server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb
current_user is the signed-in staff member, and spree_admin? asks whether they hold the admin role for the current store.
current_user is only set when the request carries a staff token. A request authenticated with a secret API key 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:
server/config/initializers/spree.rb
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:
server/app/controllers/spree/api/v3/admin/product_audits_controller.rb
Add the route to your application’s config/routes.rb, inside the engine’s route hook. See extending the API.
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.

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

Using Instance Variables in prepended

Circular Dependencies

Be careful when decorators depend on each other:

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):
server/app/models/spree/product_decorator.rb
After (Events subscriber):
server/app/subscribers/product_sync_subscriber.rb
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.