This Elixir implementation of GoodJob is designed to be fully compatible with Ruby GoodJob when sharing the same database schema and conventions as Protocol.
This allows you to:
- Process jobs from Rails/GoodJob in Elixir/Phoenix
- Process jobs from Elixir/Phoenix in Rails/GoodJob
- Share the same job queue across both ecosystems
Database Schema Compatibility ✅
Both implementations use the exact same database schema:
good_jobstable with identical columnsgood_job_processestable for process trackinggood_job_executionstable for execution history- Same indexes and constraints
No schema changes needed when sharing a database between Ruby and Elixir applications.
Retry & Error Logic Compatibility ✅
Max Attempts
- Default:
5(matches Ruby GoodJob'sretry_ondefaultattempts: 5) - Configurable per job via
use GoodJob.Job, max_attempts: 10
Backoff Strategy
- Default: Constant 3 seconds (matches Ruby GoodJob's ActiveJob
retry_ondefaultwait: 3) - Ruby-compatible: Constant backoff with 15% jitter by default (matches Ruby ActiveJob's default jitter)
- Additional strategies: Exponential, linear, and polynomial backoff available
- Customizable via
backoff/1callback
Jitter Calculation
- Matches Ruby: Additive-only jitter (
rand * delay * jitter) - Same behavior as Ruby GoodJob's
Kernel.rand * delay * jitter
Usage Patterns
Elixir Job (Compatible with Ruby GoodJob)
defmodule MyApp.Jobs.ProcessOrder do
use GoodJob.Job,
queue: "default",
max_attempts: 5, # Matches Ruby GoodJob default
priority: 0
# Default backoff is constant 3 seconds (matches Ruby GoodJob's ActiveJob default)
# No need to override unless you want a different strategy
def perform(%{order_id: order_id}) do
# Process order
case MyApp.Orders.process(order_id) do
:ok -> :ok
{:error, reason} -> {:error, reason} # Will retry
end
end
endRuby Job (Compatible with Elixir GoodJob)
class ProcessOrderJob < ApplicationJob
queue_as :default
retry_on StandardError, attempts: 5, wait: 3.seconds
def perform(order_id)
MyApp::Orders.process(order_id)
end
endBoth jobs will:
- Share the same
good_jobstable - Use the same retry logic (5 attempts)
- Use the same backoff strategy (3 seconds constant)
- Be processable by either Ruby or Elixir workers
Differences (By Design)
Error Handling Paradigm
Ruby GoodJob (ActiveJob):
class MyJob < ApplicationJob
retry_on SomeError, attempts: 5
discard_on AnotherError
def perform(args)
raise SomeError if something_wrong
end
endElixir GoodJob:
defmodule MyJob do
use GoodJob.Job, max_attempts: 5
def perform(args) do
if something_wrong do
{:error, "reason"} # Will retry
else
:ok
end
end
endBoth approaches are functionally equivalent but use different paradigms:
- Ruby: Exception-based (Rails convention)
- Elixir: Explicit return values (Elixir convention)
Default Backoff
- Ruby GoodJob: Constant 3 seconds (default in
retry_on) - Elixir GoodJob: Constant 3 seconds (aligned with Ruby GoodJob)
Both implementations now use the same default backoff strategy. To use a different strategy:
# Use exponential backoff
def backoff(attempt), do: GoodJob.Backoff.exponential(attempt)
# Use polynomial backoff (matches Ruby's :polynomially_longer)
def backoff(attempt), do: GoodJob.Backoff.polynomial(attempt)Best Practices
- Use consistent
max_attemptsacross Ruby and Elixir jobs in the same queue - Use constant backoff if you need exact Ruby GoodJob behavior
- Use exponential backoff for better Elixir ecosystem alignment (default)
- Test job processing from both sides to ensure compatibility
Migration Notes
If you're migrating from Ruby GoodJob to Elixir GoodJob:
- No schema changes needed - our migrations match Ruby's exactly
- Update job definitions - convert from ActiveJob to
use GoodJob.Job - Update error handling - convert from exceptions to explicit returns
- Test thoroughly - ensure retry logic behaves as expected