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
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
]
# application.ex
children = [
{Oban, Application.fetch_env!(:my_app, Oban)}
]
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
]
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
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
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
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
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.