Workers And Idempotency

Copy Markdown View Source

ObanPowertools.Worker is the default builder-facing entry point when you want app code to enqueue typed jobs without pushing validation and duplicate suppression into every caller.

What the wrapper adds

  • typed args: declarations backed by an embedded schema
  • validate/1 for synchronous argument validation
  • enqueue/2 for idempotent inserts through the Powertools receipt table
  • optional limits: declarations when the worker also needs durable rate control

The runtime still executes an Oban.Worker. Powertools just makes the builder contract stricter.

Minimal worker

defmodule MyApp.Billing.ProcessInvoiceWorker do
  use ObanPowertools.Worker,
    queue: :billing,
    args: [
      account_id: :integer,
      invoice_id: :integer,
      reason: :string
    ]

  @impl true
  def process(%Oban.Job{args: %__MODULE__.Args{} = args}) do
    MyApp.Billing.process_invoice(args.account_id, args.invoice_id, args.reason)
    :ok
  end
end

What callers do

case MyApp.Billing.ProcessInvoiceWorker.enqueue(%{
       account_id: 42,
       invoice_id: 9001,
       reason: "manual_retry"
     }) do
  {:ok, job} ->
    {:ok, job.id}

  {:conflict, job} ->
    {:ok, job.id}

  {:error, %Ecto.Changeset{} = changeset} ->
    {:error, changeset}
end

Important behavior:

  • invalid args fail before the job is inserted
  • the worker receives a casted %__MODULE__.Args{} struct
  • duplicate enqueue attempts return {:conflict, job} for the original durable insert

That gives callers one stable API instead of ad hoc validation and “did we already queue this?” checks spread across controllers, LiveViews, and services.

Validation without enqueue

Use validate/1 when you want to fail fast before presenting or persisting a user-facing action:

MyApp.Billing.ProcessInvoiceWorker.validate(%{
  account_id: 42,
  invoice_id: 9001,
  reason: "manual_retry"
})

When to stay on the wrapper

Use ObanPowertools.Worker when your job benefits from:

  • a stable typed argument shape
  • duplicate suppression at enqueue time
  • a durable limiter binding

Use plain Oban.Worker only when you explicitly do not want those guarantees.