Implement Background Job
Use this skill when the task is to add, configure, or review background jobs in a Rails application.
HARD-GATE
EVERY job MUST have its test written and validated BEFORE implementation:
1. Write the job spec (idempotency, retry, error handling)
2. Run the spec — verify it fails
3. ONLY THEN write the job class
The authoritative perform contract — EVERY perform method does exactly three things:
1. Load the record from the passed ID
2. Guard for idempotency / permanent no-op conditions
3. Delegate the side effect or orchestration to a service object
If perform needs more than that, extract a service.
EVERY job that performs a side effect (charge, email, API call) MUST have
an idempotency check BEFORE the side effect.
Core Rules
| Aspect |
Rule |
| Arguments |
Pass IDs, not objects |
| Retries |
retry_on (explicit attempts:) for transient; discard_on for permanent errors |
| Backend (Rails 8) |
Solid Queue (database-backed, no Redis) |
| Backend (Rails 7) |
Sidekiq + Redis for high throughput |
| Recurring |
config/recurring.yml (Solid Queue) or cron/sidekiq-cron |
| Anti-patterns |
No ActiveRecord objects as args; no :inline/:async in production; no business logic in perform |
Core Process
- Write the job spec first — idempotency, retry, and error handling — and run it to confirm it fails.
- Write the job class following the perform contract in HARD-GATE.
- Add
retry_on with explicit attempts: limit and discard_on for at least one permanent error.
- Run the full test suite.
- Enqueue or perform the job twice — confirm the second run is a no-op.
- For recurring jobs, define them in
config/recurring.yml (Rails 8) or the chosen scheduler config.
Extended Resources
Rails 8 vs Rails 7
| Aspect |
Rails 7 and earlier |
Rails 8 |
| Default |
No default; set queue_adapter (often Sidekiq) |
Solid Queue (database-backed) |
| Dev/test |
:async or :inline |
Same |
| Recurring |
External (cron, sidekiq-cron) |
config/recurring.yml |
| Dashboard |
Third-party (Sidekiq Web) |
Mission Control Jobs |
Examples
Thin job with idempotency and retry:
class SendInvoiceReminderJob < ApplicationJob
queue_as :default
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
discard_on ActiveRecord::RecordNotFound
def perform(invoice_id)
invoice = Invoice.find(invoice_id)
return if invoice.reminder_sent_at?
InvoiceReminders::Send.call(invoice:)
end
end
Service owns the side effect and state update:
module InvoiceReminders
class Send
def self.call(invoice:)
InvoiceMailer.overdue(invoice).deliver_now
invoice.update!(reminder_sent_at: Time.current)
end
end
end
BACKENDS.md — Solid Queue vs Sidekiq setup, configuration details, and Redis requirements.
Load these files only when their specific content is needed:
assets/job_patterns.md — Use when implementing multi-step orchestration or batch job patterns
assets/retry_examples.md — Use when configuring retry_on/discard_on for specific error classes beyond the basic patterns above
Output Checklist
Integration
| Skill |
When to chain |
| review-migration |
Solid Queue uses DB tables; add migrations safely |
| security-check |
Jobs receive serialized input; validate like any entry point |
| write-tests |
TDD gate: write job spec before implementation; use perform_enqueued_jobs |
| create-service-object |
Keep perform thin; call service objects for business logic |
1---2name: implement-background-job3description: Use when adding or reviewing an Active Job / Sidekiq / Solid Queue worker. Cover idempotency, retry_on, and discard_on. Trigger words: background job, Active Job, Sidekiq, Solid Queue, worker.4license: MIT5---67# Implement Background Job89Use this skill when the task is to add, configure, or review background jobs in a Rails application.1011## HARD-GATE1213```text14EVERY job MUST have its test written and validated BEFORE implementation:15 1. Write the job spec (idempotency, retry, error handling)16 2. Run the spec — verify it fails17 3. ONLY THEN write the job class1819The authoritative perform contract — EVERY perform method does exactly three things:20 1. Load the record from the passed ID21 2. Guard for idempotency / permanent no-op conditions22 3. Delegate the side effect or orchestration to a service object2324If perform needs more than that, extract a service.25EVERY job that performs a side effect (charge, email, API call) MUST have26an idempotency check BEFORE the side effect.27```2829## Core Rules3031| Aspect | Rule |32|--------|------|33| Arguments | Pass IDs, not objects |34| Retries | `retry_on` (explicit `attempts:`) for transient; `discard_on` for permanent errors |35| Backend (Rails 8) | Solid Queue (database-backed, no Redis) |36| Backend (Rails 7) | Sidekiq + Redis for high throughput |37| Recurring | `config/recurring.yml` (Solid Queue) or cron/sidekiq-cron |38| Anti-patterns | No ActiveRecord objects as args; no `:inline`/`:async` in production; no business logic in `perform` |3940## Core Process41421. Write the job spec first — idempotency, retry, and error handling — and run it to confirm it fails.432. Write the job class following the perform contract in HARD-GATE.443. Add `retry_on` with explicit `attempts:` limit and `discard_on` for at least one permanent error.454. Run the full test suite.465. Enqueue or perform the job twice — confirm the second run is a no-op.476. For recurring jobs, define them in `config/recurring.yml` (Rails 8) or the chosen scheduler config.4849## Extended Resources5051**Rails 8 vs Rails 7**52| Aspect | Rails 7 and earlier | Rails 8 |53|--------|---------------------|---------|54| Default | No default; set `queue_adapter` (often Sidekiq) | **Solid Queue** (database-backed) |55| Dev/test | `:async` or `:inline` | Same |56| Recurring | External (cron, sidekiq-cron) | `config/recurring.yml` |57| Dashboard | Third-party (Sidekiq Web) | **Mission Control Jobs** |5859**Examples**6061**Thin job with idempotency and retry:**62```ruby63class SendInvoiceReminderJob < ApplicationJob64 queue_as :default65 retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 566 discard_on ActiveRecord::RecordNotFound6768 def perform(invoice_id)69 invoice = Invoice.find(invoice_id)70 return if invoice.reminder_sent_at?7172 InvoiceReminders::Send.call(invoice:)73 end74end75```7677**Service owns the side effect and state update:**78```ruby79module InvoiceReminders80 class Send81 def self.call(invoice:)82 InvoiceMailer.overdue(invoice).deliver_now83 invoice.update!(reminder_sent_at: Time.current)84 end85 end86end87```8889- [BACKENDS.md](./BACKENDS.md) — Solid Queue vs Sidekiq setup, configuration details, and Redis requirements.90Load these files only when their specific content is needed:9192- **[assets/job_patterns.md](assets/job_patterns.md)** — Use when implementing multi-step orchestration or batch job patterns93- **[assets/retry_examples.md](assets/retry_examples.md)** — Use when configuring `retry_on`/`discard_on` for specific error classes beyond the basic patterns above9495## Output Checklist9697- [ ] Backend decision stated (Rails version/scale → Solid Queue or Sidekiq)98- [ ] Job spec shown first; command run; confirms failure before implementation99- [ ] `perform` receives IDs, loads record, guards idempotency, delegates to service100- [ ] `retry_on` with `attempts:` limit and `discard_on` for permanent error101- [ ] Double-run verification confirms second run is a no-op102- [ ] Recurring job (if any) defined in `config/recurring.yml` or scheduler config103- [ ] If ops docs requested: record backend, retry, recurring schedule, and idempotency decisions in `process_log.md`104105## Integration106107| Skill | When to chain |108|-------|---------------|109| **review-migration** | Solid Queue uses DB tables; add migrations safely |110| **security-check** | Jobs receive serialized input; validate like any entry point |111| **write-tests** | TDD gate: write job spec before implementation; use `perform_enqueued_jobs` |112| **create-service-object** | Keep `perform` thin; call service objects for business logic |