DEV Community

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

Posted on Edited on

Quarkus Scheduler na prática

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)