Como usamos Oban com Elixir para resolver nossas rotinas de faturamento
Caio Delgado

Caio Delgado @caiodelgado

About: JavaScript developer, learning Elixir and pursuing high performance. https://www.linkedin.com/in/caio-s-delgado/

Joined:
May 22, 2025

Como usamos Oban com Elixir para resolver nossas rotinas de faturamento

Publish Date: May 22
1 0

Esteira de processamento com símbolos que remetem banco de dados, tempo, agenda e tempo nas cores elixir

Em sistemas que precisam processar grandes volumes de dados em segundo plano, como rotinas de faturamento, é comum cair na armadilha de criar processos temporários ou rodar scripts manuais. E foi exatamente isso que decidimos evitar.

Neste artigo, compartilho como o Oban nos ajudou a estruturar um sistema de jobs resilientes e escaláveis, e como isso se tornou parte fundamental do nosso processo de geração de billing na Nextcode.

O desafio: rotinas complexas e recorrentes de faturamento

Nosso cenário envolvia:

  • Processar milhares de logs de consumo diariamente;
  • Buscar logs de aplicações e base de dados distintas;
  • Aplicar regras específicas por cliente e tipo de serviço;
  • Agregar e gerar logs;
  • Garantir reprocessamento seguro em caso de falhas;
  • Escalar horizontalmente sem perder rastreabilidade;
  • Agregar dados em bases otimizadas para consulta;
  • Rodar essas rotinas com tarefas manuais ou scripts pontuais era arriscado, especialmente se algo falhasse sem log ou reexecução programada.

Foi aí que o Oban entrou no jogo.

Por que escolhemos o Oban?

Além de ser 100% Elixir, o Oban entrega tudo que esperávamos:

✅ Persistência via PostgreSQL
✅ Reexecução automática com backoff
✅ Deduplicação de jobs com controle de uniqueness
✅ Visibilidade com dashboard, via oban_web (ainda não testamos)
✅ Execução distribuída com filas e concorrência isoladas
✅ Flexibilidade e integração nativa com Ecto

Instalação

A configuração inicial é simples. Basta adicionar a dependência no seu mix.exs:

def deps do
  [
    {:oban, "~> 2.17"},
  ]
end
Enter fullscreen mode Exit fullscreen mode

Em seguida, configure seu repositório e adicione a supervisão:

# config/config.exs
config :my_app, Oban,
  repo: MyApp.Repo,
  plugins: [
    # Limpar jobs com sucesso em 24hrs
    {Oban.Plugins.Pruner, max_age: 86_400},
  ],
  queues: [
    # Dessa forma será executado um job de cada vez
    mongodb_daily_log: 1
  ]
Enter fullscreen mode Exit fullscreen mode
# application.ex
children = [
  {Oban, Application.fetch_env!(:my_app, Oban)}
]
Enter fullscreen mode Exit fullscreen mode

Nosso worker: MongodbDailyLog

Para exemplificar, veja como estruturamos um dos nossos workers que processa logs diários para geração de billing:

defmodule MyApp.Job.MongodbDailyLog do
  use Oban.Worker,
    queue: :mongodb_daily_log,
    max_attempts: 2,
    unique: [
      fields: [:args],
      states: [:available, :scheduled, :executing],
      period: 60
    ]
Enter fullscreen mode Exit fullscreen mode

O que isso faz?

  • Define a fila específica para o job
  • Limita a 2 tentativas por job
  • Garante uniqueness para não executar dois jobs com os mesmos argumentos simultaneamente

Agendamento automático e execução segura

Nosso job tem dois modos de execução: por agendamento automático ou sob demanda. Usamos o Timex para trabalhar com datas e criar o intervalo de tempo para o processamento.

def perform(%Oban.Job{args: %{"date" => %{"day" => d, "month" => m, "year" => y}, "only_logs" => only_logs}}) do
  date = Timex.to_date({y, m, d})
  gte = Timex.to_datetime(date, "America/Sao_Paulo")
  lt = Timex.shift(gte, days: 1)

  job_impl().run(%{gte: gte, lt: lt}, only_logs)
end
Enter fullscreen mode Exit fullscreen mode

Por arity, definimos o default para quando não é especificado uma data, a execução do dia anterior:

def perform(%Oban.Job{args: %{}}) do
  %{day: d, month: m, year: y} = Timex.shift(Timex.today(), days: -1)
  %Oban.Job{args: %{"date" => %{"day" => d, "month" => m, "year" => y}, "only_logs" => false}}
  |> perform()
end
Enter fullscreen mode Exit fullscreen mode

Deduplicação e execução

Chamamos o job com verificação de duplicidade usando:

def run(%Date{} = date \\ Timex.shift(Timex.today(), days: -1), only_logs \\ false) do
    %{day: day, month: month, year: year} = date

    job =
      %{date: %{day: day, month: month, year: year}, only_logs: only_logs}
      |> NextID.Job.MongodbDailyLog.new()

    with {:ok, %Oban.Job{conflict?: true}} <- Oban.insert(job) do
      {:error, :job_already_exists}
    end
  end  with {:ok, %Oban.Job{conflict?: true}} <- Oban.insert(job) do
    {:error, :job_already_exists}
  end
end
Enter fullscreen mode Exit fullscreen mode

Isso evita que jobs repetidos sejam criados para o mesmo dia e reduz significativamente o risco de falhas por duplicidade.

Processamento histórico? Sem problemas.

Precisamos reprocessar dados históricos? Criamos um método que recursivamente chama os jobs dia a dia:

def run_history(%Date{} = date \\ Timex.today()) do
  case Timex.before?(date, ~D[2021-08-01]) do
    false ->
      run(date, true)
      run_history(Timex.shift(date, days: -1))
    true -> :ok
  end
end
Enter fullscreen mode Exit fullscreen mode

Resultados

  • Automatizamos o processamento de logs diários com alta confiabilidade
  • Conseguimos escala horizontal, segmentando filas por tipo de tarefa
  • Evitamos problemas de duplicidade e mantivemos a rastreabilidade
  • Reduzimos retrabalho e tarefas manuais
  • E o principal: temos visibilidade e controle total sobre todos os jobs, por enquanto acessando facilmente pelo Postgres.

Conclusão

Oban se mostrou uma solução robusta, simples de implementar e perfeitamente integrada ao ecossistema Elixir. Hoje, ele é um dos pilares do nosso sistema de billing — e já estamos expandindo seu uso para outras áreas do produto.

Se você trabalha com processos críticos em background como faturamento, recomendo fortemente testar.

📚 Repositório oficial do Oban: github.com/oban-bg/oban

💬 E se quiser trocar ideias sobre como usamos por aqui, só chamar.

Comments 0 total

    Add comment