DEV Community

Caio Delgado
Caio Delgado

Posted on

Como usamos Oban com Elixir para resolver nossas rotinas de faturamento

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.

Top comments (0)