Callbacks
Hook into the operation lifecycle with before, after, and around. Callbacks run inside the transaction boundary, so errors in callbacks trigger a rollback.
before
Runs before perform. Use it for validation, setup, or precondition checks.
class PlaceOrder < Dex::Operation
prop :product_id, Integer
prop :quantity, _Integer(1..)
before :check_stock
def perform
Order.create!(product_id: product_id, quantity: quantity)
end
private
def check_stock
stock = Product.find(product_id).stock
error!(:out_of_stock, "Only #{stock} left") if stock < quantity
end
endCalling error! in a before callback stops execution – perform is never reached.
after
Runs after perform succeeds (or after success!). Skipped if perform raises or calls error!.
class CreateUser < Dex::Operation
prop :email, String
after :send_welcome_email
def perform
User.create!(email: email)
end
private
def send_welcome_email
WelcomeMailer.deliver_later(email: email)
end
endaround
Wraps the entire before/perform/after sequence. Your callback must yield (or call the continuation) to proceed – otherwise perform is never invoked.
class ImportData < Dex::Operation
around :with_timing
def perform
# heavy work
end
private
def with_timing
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
Rails.logger.info "ImportData took #{elapsed.round(2)}s"
end
endCallback forms
All three callbacks accept a Symbol (method name), a block, or a callable (lambda/proc):
class ProcessOrder < Dex::Operation
# Symbol – calls the named method
before :validate_stock
# Block – executed via instance_exec (has access to props, error!, etc.)
before { error!(:closed) if store_closed? }
# Lambda – for around, receives a continuation
around ->(cont) {
Rails.logger.tagged("ProcessOrder") { cont.call }
}
# ...
endExecution order
Multiple callbacks of the same type run in declaration order:
class Example < Dex::Operation
before :first
before :second
before :third
# Runs: first → second → third → perform
endThe full callback execution order is:
around do
before callbacks (in order)
perform
after callbacks (in order)
endInheritance
Callbacks inherit from parent classes. Parent callbacks run first, then child callbacks:
class BaseOperation < Dex::Operation
before { Rails.logger.info "Base before" }
end
class ChildOperation < BaseOperation
before { Rails.logger.info "Child before" }
def perform
# Runs: "Base before" → "Child before" → perform
end
endInteraction with error! and success!
error!in abeforecallback preventsperformandafterfrom runningerror!inperformpreventsafterfrom runningsuccess!inperformstill runsaftercallbacks (the operation succeeded)error!anywhere rolls back the transaction