Skip to main content
Spree is designed to be customized without forking it. This guide presents the options in order of recommendation — start at the top and only move down when a simpler option doesn’t fit.
Business logic is extended through workflow hooks rather than by replacing classes wholesale, and the admin is a React application with its own extension points rather than a Rails engine extended through partials.

Quick reference

Customizing the admin? This page covers the backend — models, API and business logic. The admin is a separate React application with its own extension points. See Dashboard Customization.

Store settings

Best for: currency, markets, delivery zones, languages, and other business settings.Most day-to-day configuration is data, not code. Change it in the dashboard under Settings — no deploy required.
Best for: tweaking Spree’s behavior globally.
config/initializers/spree.rb
See Configuration.
Best for: running your own logic inside a core flow — checkout completion, order cancellation, refunds, fulfillment creation.
Hooks are the headline extension point in Spree, so you don’t have to replace an entire class and keep your copy in sync with core forever.
Register a handler against a hook key — <workflow key>.<hook name>:
config/initializers/spree.rb
Pass a class instead of a block for anything longer than a line:
There are three families of hook:Hook keys are validated after boot, so a typo fails startup rather than silently never running.See Services & Workflows for the full list of flows and their hooks.
Best for: reacting after something happened — syncing to external systems, notifications, audit logging.
app/subscribers/spree/order_placed_subscriber.rb
Events or hooks? A hook runs inside the flow and can influence or block it. An event fires after the fact and cannot. Reach for an event unless you need to change the outcome.See Events.
Best for: adding, removing or reordering checkout steps.Spree has no checkout state machine. A cart reports what it still needs, and you can add to that list:
config/initializers/spree.rb
Or attach a requirement to an existing step:
Registered requirements appear in the Cart API’s requirements array, so your storefront renders them without duplicating any rules.See Carts.
Best for: swapping a whole area of behavior for your own implementation.Spree exposes pluggable providers for the domains that vary most between businesses:Each is a class you register and select on the relevant record, so no conditional code lands in core.See Fulfillments and Taxes.
Best for: making custom fields searchable and sortable in the dashboard and API.
config/initializers/spree.rb
See Search & Filtering.
Best for: using your own user model or identity provider.Spree owns its authentication stack rather than depending on Devise, and separates the customer identity from the staff identity. Point Spree at your own classes with Spree.customer_class and Spree.admin_user_class.See Authentication.
Best for: notifying external services — ERPs, CRMs, fulfillment systems — without writing Ruby.Configure them in the dashboard under Settings → Webhooks, or through the Admin API.See Webhooks.
Best for: replacing an entire workflow or service with your own class.
config/initializers/spree.rb
Seam names end in _workflow. The old *_service names still resolve with a warning, but writes to them are ignored — a class written against the old service contract isn’t interchangeable with a workflow. Update the key if your code still uses the old name.
Prefer a hook when you only need to add behavior. Replace the class only when you need to change what the flow fundamentally does.See Dependencies.
Best for: adding associations, validations and scopes to Spree models. Use as a last resort.
Decorators couple your code to Spree internals and are the most common cause of painful upgrades.Do not use decorators for:
They remain appropriate for structural additions:
app/models/spree/product_decorator.rb
See Decorators.

Choosing between hooks, events and dependencies

These three overlap, and picking the wrong one is the most common source of upgrade pain:
1

Do you need to block or change the outcome?

Use a workflow hook. Only validate hooks can stop an operation, and only they run early enough to do so safely — before money moves.
2

Do you just need to know it happened?

Use an event subscriber. It’s decoupled, testable, and survives upgrades untouched.
3

Do you need the flow to do something fundamentally different?

Use dependencies to replace the class — and accept that you now own keeping it current.