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 underserver/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 likeSpree::Product or Spree::Order. However, you shouldn’t modify these files directly because:
- Upgrades - Your changes would be lost when updating Spree
- Maintainability - It’s hard to track what you’ve customized
- Conflicts - Direct modifications can conflict with Spree’s code
How Decorators Work
In Ruby, classes are “open” - you can add methods to them at any time. Decorators leverage this by:- Creating a module with your new methods
- Using
Module#prependto inject your module into the class’s inheritance chain - Your methods run first, and can call
superto invoke the original method
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
server/app/models/spree/product_decorator.rb:
Controller Decorator Generator
server/app/controllers/spree/api/v3/admin/products_controller_decorator.rb:
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 callsuper to invoke the original implementation:
server/app/models/spree/product_decorator.rb
Adding New Methods
Add new instance methods directly in the decorator module:server/app/models/spree/product_decorator.rb
Adding Associations
Use theself.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
Useextend within the prepended callback to add class methods:
server/app/models/spree/product_decorator.rb
Decorating Controllers
The only controllers Spree ships are the API ones, underSpree::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.
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.
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
+=, 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
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
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
server/app/subscribers/product_sync_subscriber.rb
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 - Learn about Spree’s event system
- Webhooks - HTTP callbacks for external integrations
- Dependencies - Swap core services with your own
- Extending the API - Add your own endpoints
- Dashboard customization - Extend the dashboard from its own React app
- Extending Core Models Tutorial - Step-by-step guide to connecting custom models with Spree core
- Customization Overview - General customization patterns

