DEV Community

Luis Fabrício De Llamas
Luis Fabrício De Llamas

Posted on

Quarkus Scheduler na prática: agendando cobranças como um serviço fintech

Se você já trabalhou com sistemas financeiros, sabe que uma das tarefas mais comuns é lidar com tarefas agendadas: verificar vencimentos, processar cobranças, gerar relatórios diários. No mundo Spring Boot, muita gente recorre ao @Scheduled. No Quarkus, temos o Quarkus Scheduler — e ele é bem direto ao ponto.

Nesse post vou mostrar como usar o Scheduler num contexto real de fintech, sem aquele exemplo de "contador que incrementa a cada 10 segundos" que todo tutorial usa. Bora?

O que vamos construir

Uma API de cobranças com três jobs agendados:

  • A cada 30 segundos: verifica vencimentos e atualiza status
  • Todo dia às 8h: inicia o processamento das cobranças pendentes
  • Todo dia às 18h: gera um relatório com totais

Estrutura do projeto:

src/main/java/dev/dellamas/fintech/
├── model/
│   └── Cobranca.java
├── service/
│   └── CobrancaService.java
├── scheduler/
│   └── CobrancaScheduler.java
└── resource/
    └── CobrancaResource.java
Enter fullscreen mode Exit fullscreen mode

Criando o projeto

mvn io.quarkus.platform:quarkus-maven-plugin:3.18.4:create \
  -DprojectGroupId=dev.dellamas \
  -DprojectArtifactId=quarkus-scheduler-fintech \
  -Dextensions="rest,scheduler,rest-jackson" \
  -DnoCode
Enter fullscreen mode Exit fullscreen mode

O modelo: Cobranca.java

Vamos começar pelo modelo. Nada sofisticado aqui — uma cobrança com id, cliente, valor, vencimento e status. Mas já pensou quantas vezes você viu um sistema que não tem nem isso bem definido?

package dev.dellamas.fintech.model;

import java.math.BigDecimal;
import java.time.LocalDate;

public class Cobranca {

    private String id;
    private String cliente;
    private BigDecimal valor;
    private LocalDate vencimento;
    private StatusCobranca status;

    public enum StatusCobranca {
        PENDENTE, PROCESSANDO, PAGA, VENCIDA
    }

    public Cobranca(String id, String cliente, BigDecimal valor, LocalDate vencimento) {
        this.id = id;
        this.cliente = cliente;
        this.valor = valor;
        this.vencimento = vencimento;
        this.status = StatusCobranca.PENDENTE;
    }

    public String getId() { return id; }
    public String getCliente() { return cliente; }
    public BigDecimal getValor() { return valor; }
    public LocalDate getVencimento() { return vencimento; }
    public StatusCobranca getStatus() { return status; }
    public void setStatus(StatusCobranca status) { this.status = status; }
}
Enter fullscreen mode Exit fullscreen mode

Sem persistência por enquanto — o foco aqui é no Scheduler. Num projeto real, isso viraria uma entidade Panache sem dor.

O serviço: CobrancaService.java

Esse é o coração da lógica. Ele mantém as cobranças em memória e expõe os métodos que os jobs vão chamar. Perceba o ConcurrentHashMap — os jobs rodam em threads separadas, então se você não pensar em concorrência aqui, vai ter problema cedo ou tarde.

package dev.dellamas.fintech.service;

import dev.dellamas.fintech.model.Cobranca;
import dev.dellamas.fintech.model.Cobranca.StatusCobranca;
import jakarta.enterprise.context.ApplicationScoped;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

@ApplicationScoped
public class CobrancaService {

    private final Map<String, Cobranca> cobrancas = new ConcurrentHashMap<>();

    public CobrancaService() {
        cobrancas.put("C001", new Cobranca("C001", "Empresa Alpha",
                new BigDecimal("1500.00"), LocalDate.now().minusDays(1)));
        cobrancas.put("C002", new Cobranca("C002", "Startup Beta",
                new BigDecimal("890.50"), LocalDate.now()));
        cobrancas.put("C003", new Cobranca("C003", "Loja Gamma",
                new BigDecimal("3200.00"), LocalDate.now().plusDays(5)));
        cobrancas.put("C004", new Cobranca("C004", "Consultoria Delta",
                new BigDecimal("650.00"), LocalDate.now().minusDays(3)));
    }

    public List<Cobranca> listarTodas() {
        return new ArrayList<>(cobrancas.values());
    }

    public List<Cobranca> listarVencidas() {
        return cobrancas.values().stream()
                .filter(c -> c.getVencimento().isBefore(LocalDate.now())
                        && c.getStatus() == StatusCobranca.PENDENTE)
                .collect(Collectors.toList());
    }

    public List<Cobranca> listarPendentes() {
        return cobrancas.values().stream()
                .filter(c -> c.getStatus() == StatusCobranca.PENDENTE)
                .collect(Collectors.toList());
    }

    public int marcarVencidasComoVencidas() {
        List<Cobranca> vencidas = listarVencidas();
        vencidas.forEach(c -> c.setStatus(StatusCobranca.VENCIDA));
        return vencidas.size();
    }

    public int processarPendentes() {
        List<Cobranca> pendentes = listarPendentes();
        pendentes.forEach(c -> c.setStatus(StatusCobranca.PROCESSANDO));
        return pendentes.size();
    }

    public BigDecimal totalVencido() {
        return cobrancas.values().stream()
                .filter(c -> c.getStatus() == StatusCobranca.VENCIDA)
                .map(Cobranca::getValor)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
    }
}
Enter fullscreen mode Exit fullscreen mode

Os jobs agendados: CobrancaScheduler.java

Aqui é onde o Quarkus Scheduler entra. A anotação @Scheduled aceita dois formatos: intervalo fixo com every e expressão cron com cron. Qual usar? Depende. Pra verificações frequentes, every resolve. Pra tarefas que precisam rodar em horário específico, cron é o caminho.

package dev.dellamas.fintech.scheduler;

import dev.dellamas.fintech.service.CobrancaService;
import io.quarkus.scheduler.Scheduled;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.jboss.logging.Logger;

@ApplicationScoped
public class CobrancaScheduler {

    private static final Logger LOG = Logger.getLogger(CobrancaScheduler.class);

    @Inject
    CobrancaService cobrancaService;

    @Scheduled(every = "30s")
    void verificarVencimentos() {
        int total = cobrancaService.marcarVencidasComoVencidas();
        if (total > 0) {
            LOG.infof("Vencimentos processados: %d cobrança(s) marcadas como vencidas", total);
        }
    }

    @Scheduled(cron = "0 0 8 * * ?")
    void processarCobrancasDiarias() {
        int total = cobrancaService.processarPendentes();
        LOG.infof("Processamento diário iniciado: %d cobrança(s) em processamento", total);
    }

    @Scheduled(cron = "0 0 18 * * ?")
    void gerarRelatorioDiario() {
        var vencidas = cobrancaService.listarVencidas().size();
        var totalVencido = cobrancaService.totalVencido();
        LOG.infof("Relatório diário — Cobranças vencidas: %d | Total em aberto: R$ %s",
                vencidas, totalVencido);
    }
}
Enter fullscreen mode Exit fullscreen mode

O formato de cron padrão no Quarkus é o do Quartz (6 campos, com segundos). Se você prefere o formato Unix de 5 campos, troca no application.properties:

quarkus.scheduler.cron-type=unix
Enter fullscreen mode Exit fullscreen mode

O endpoint REST: CobrancaResource.java

Pra fechar, precisamos expor o estado das cobranças. Afinal, de que adianta processar tudo em background se ninguém consegue consultar?

package dev.dellamas.fintech.resource;

import dev.dellamas.fintech.model.Cobranca;
import dev.dellamas.fintech.service.CobrancaService;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import java.util.List;

@Path("/cobrancas")
@Produces(MediaType.APPLICATION_JSON)
public class CobrancaResource {

    @Inject
    CobrancaService cobrancaService;

    @GET
    public List<Cobranca> listarTodas() {
        return cobrancaService.listarTodas();
    }

    @GET
    @Path("/vencidas")
    public List<Cobranca> listarVencidas() {
        return cobrancaService.listarVencidas();
    }

    @GET
    @Path("/pendentes")
    public List<Cobranca> listarPendentes() {
        return cobrancaService.listarPendentes();
    }

    @GET
    @Path("/resumo")
    @Produces(MediaType.TEXT_PLAIN)
    public String resumo() {
        return String.format("Total vencido: R$ %s | Vencidas: %d | Pendentes: %d",
                cobrancaService.totalVencido(),
                cobrancaService.listarVencidas().size(),
                cobrancaService.listarPendentes().size());
    }
}
Enter fullscreen mode Exit fullscreen mode

Configuração

application.properties:

quarkus.application.name=quarkus-scheduler-fintech
quarkus.scheduler.cron-type=quartz
Enter fullscreen mode Exit fullscreen mode

Rodando

git clone https://github.com/dellamas/quarkus-scheduler-fintech
cd quarkus-scheduler-fintech
./mvnw quarkus:dev
Enter fullscreen mode Exit fullscreen mode

Assim que subir, o job de verificação de vencimentos já começa a rodar a cada 30 segundos. Acompanhe no terminal e, em paralelo, teste:

curl http://localhost:8080/cobrancas
curl http://localhost:8080/cobrancas/vencidas
curl http://localhost:8080/cobrancas/resumo
Enter fullscreen mode Exit fullscreen mode

Por que o Scheduler e não o Quartz?

O quarkus-scheduler é a opção leve — resolve bem pra aplicações de instância única. Precisa de clustering, com múltiplas instâncias disputando o mesmo job? Aí o quarkus-quartz faz mais sentido. Mas pra maioria dos cenários do dia a dia, o Scheduler padrão dá conta.


O código completo está no GitHub. Se fez sentido pra você, deixa uma estrela no repositório — ajuda mais do que parece.

Eu sou o Luis De Llamas, Developer Advocate na act digital, Oracle ACE e IBM Champion. Se quiser acompanhar mais conteúdo sobre Quarkus, Java e o que rola no ecossistema, me encontra aqui:

Nos vemos no próximo.

Top comments (0)