add-background-jobs
Use this workflow when integrating background jobs in Hanami 2.x.
Core principle: Background jobs are registered components. They are enqueued from Actions and executed asynchronously.
Core Process
[Register Job Adapter] — Load skill:
register-provider- Choose adapter: Sidekiq, GoodJob, or custom
- Create
config/providers/background_jobs.rb - Register the adapter client in the
startblock - Handoff condition: Adapter accessible via
Deps["jobs.client"]
# config/providers/background_jobs.rb Hanami.app.register_provider(:background_jobs) do start do require "sidekiq" Sidekiq.configure_client do |config| config.redis = { url: target[:settings].redis_url } end register("jobs.client", Sidekiq::Client) end endValidation: Verify registration with
MyApp::App["jobs.client"]in a Hanami console or container lookup spec. It should returnSidekiq::Clientwithout raisingDry::Container::Error.[Inject into Action] — Load skill:
inject-dependencies- Inject job adapter into Action
- Handoff condition: Adapter injectable
class Create < MyApp::Action include Deps["jobs.client", "repos.user_repo"] end[Enqueue from Action] — Load skill:
create-action- Define job class
- Enqueue job from Action after successful operation
- Handle enqueue failures gracefully — rescue enqueue errors and log them; do not crash the Action
- Handoff condition: Jobs are enqueued and executed
# app/jobs/welcome_email.rb module MyApp module Jobs class WelcomeEmail include Sidekiq::Worker def perform(user_id) user = user_repo.by_id(user_id).one mailer.deliver(to: user.email, subject: "Welcome!") end end end endclass Create < MyApp::Action def handle(request, response) result = create_user.call(request.params[:user]) case result in Success(user) jobs.client.push("class" => "MyApp::Jobs::WelcomeEmail", "args" => [user.id]) response.status = 201 in Failure(error) response.status = 422 end rescue => e Hanami.logger.error("Job enqueue failed: #{e.message}") response.status = 201 # Still succeed; enqueue failure is non-fatal end endValidation: Confirm jobs are being enqueued by checking the Sidekiq dashboard (
/sidekiq) or inspecting the Redis queue directly (Sidekiq::Queue.new.size). In tests, useSidekiq::Testing.fake!and assert onMyApp::Jobs::WelcomeEmail.jobs.[Write Job Specs] — Load skill:
write-action-spec- Test job class in isolation with stubbed dependencies
- Test that Action enqueues the job
- Handoff condition: Job specs pass
# spec/unit/my_app/jobs/welcome_email_spec.rb RSpec.describe MyApp::Jobs::WelcomeEmail do subject(:job) { described_class.new } let(:user_repo) { instance_double(MyApp::Repos::UserRepo) } let(:mailer) { instance_double(MyApp::Mailers::Welcome) } let(:user) { instance_double(MyApp::Entities::User, id: 42, email: "user@example.com") } before do allow(user_repo).to receive(:by_id).with(42).and_return(double(one: user)) allow(mailer).to receive(:deliver) job.instance_variable_set(:@user_repo, user_repo) job.instance_variable_set(:@mailer, mailer) end it "delivers a welcome email to the user" do job.perform(42) expect(mailer).to have_received(:deliver).with(to: "user@example.com", subject: "Welcome!") end end # spec/requests/users/create_spec.rb (enqueue assertion) RSpec.describe "POST /users" do before { Sidekiq::Testing.fake! } after { Sidekiq::Worker.clear_all } it "enqueues a WelcomeEmail job on success" do expect { post "/users", params: { user: { name: "Alice", email: "alice@example.com" } } }.to change(MyApp::Jobs::WelcomeEmail.jobs, :size).by(1) expect(last_response.status).to eq(201) end end
Common Pitfalls
- Synchronous execution of deferrable work — Use jobs for emails, notifications, exports, or anything that can be deferred.
- Missing enqueue error handling — Rescue enqueue errors and log them. Do not crash the Action on transient queue failures.
- Direct container access in jobs — Inject dependencies via DI container; do not access the container directly inside jobs.
- Passing complex objects as job arguments — Jobs serialize to JSON. Pass IDs or simple scalar data, never Ruby objects.
- Missing job tests — Always test the job class in isolation and verify the Action enqueues correctly.
- Jobs not registered in DI container — The job adapter client must be registered as a provider before it can be injected.