Spring Batch em ação: processamento de grandes lotes de dados

Foto do autor Rafael Sotero
Rafael Sotero
Compartilhar
Compartilhar no LinkedInCompartilhar no FacebookCompartilhar no XCompartilhar no WhatsApp
Imagem banner do post

O Spring Batch é o 'canivete suíço' da JVM para lidar com grandes volumes de dados. Neste artigo, aprenda a configurar essa ferramenta robusta para resolver um desafio real: consolidar relatórios de vendas de diversas concessionárias em uma única solução eficiente.

Contexto

O ecossistema do Spring surpreende por ser um canivete suíço, e não seria diferente no mundo batch, onde lidamos com grandes volumes de dados para executar as mais diversas rotinas em ambientes corporativos.

Seja no fechamento de folha de pagamento, no envio de campanhas via push notification ou em migrações/convivência entre sistemas, o Spring Batch brilha como uma alternativa robusta rodando dentro da JVM.

Neste artigo, você vai aprender a usar o Spring Batch para gerar um relatório (report) de vendas consolidado a partir dos relatórios individuais de várias concessionárias filiais espalhadas pelo país. Vem comigo!


O que são sistemas batch?

O termo batch pode parecer estranho quando você escuta pela primeira vez, mas dá para traduzi-lo como “lote” (e isso ajuda bastante).

Um sistema batch executa o processamento de uma quantidade finita de dados, normalmente sem interação humana durante a execução, por meio de:

  • execuções agendadas (data/hora);
  • gatilhos/estímulos externos (ex.: liberação de uma turma/aula, solicitação de geração de relatório, fechamento de ciclo etc.).

Batch vs. API REST / Microserviços

É importante notar a diferença entre API REST/microserviços e sistemas batch.

Pense assim: uma API REST/Microserviços é como um Uber (você chama agora e é atendido na hora), enquanto um Sistema Batch é como um ônibus (tem horário para passar e leva “todo mundo” de uma vez). Ambos têm propósitos distintos e são aplicáveis a cenários diferentes dentro de um negócio.

  • On-demand vs. Agendado: o Uber reage ao seu clique; o ônibus segue um cronograma.
  • Individual vs. Coletivo: a API trata o seu pedido; o batch trata o volume acumulado.

Demanda de mercado

Você já ouviu falar de COBOL? Essa linguagem foi uma das precursoras no ambiente batch e ainda é muito utilizada no sistema financeiro mundial. Segundo a IBM (responsável pelo COBOL), são cerca de 3 trilhões de dólares sendo processados diariamente por código COBOL.

O COBOL, felizmente, vem sendo descontinuado na maior parte das empresas. A migração acelerada para nuvem pública (AWS, Azure, GCP etc.), somada à escassez de profissionais no mercado e ao suporte exclusivo da IBM, são forças motrizes desse ciclo. O Itaú anunciou recentemente a intenção de modernizar 100% do banco em nuvem pública até 2028.

No mundo Java, usamos o Spring Batch como framework para dar a robustez necessária aos processamentos em lote. Ele é projetado para lidar com cenários em que “falhar não é uma opção”, mas se falhar, informar com precisão onde parou para permitir retomada.

Além disso, o Spring Batch se encaixa bem em arquiteturas modernas: ele pode executar de forma cloud native, com integração com orquestradores de containers e sendo agnóstico de fornecedor (executável em diferentes ambientes/clouds).

O Bradesco, um dos maiores bancos da América Latina, utiliza o Spring Batch em operações batch diariamente como alternativa moderna ao COBOL.

Conhecer como processos batch funcionam no Spring Batch me parece uma boa ideia, hein? 😉


Nosso caso de uso

Vamos transformar um problema real em solução usando o Spring Batch para resolver um problema do negócio do nosso cliente.

O nosso cliente é a maior rede de concessionárias de carros do Brasil chamada Aurora Car Dealer, uma grande rede (fictícia) que possui lojas em 11 estados do país.

O problema

A Aurora Car Dealer precisa automatizar a geração do seu principal relatório de vendas, que hoje é feito manualmente no Excel.

Semanalmente as filiais da Aurora (Aurora Rio, Aurora SP, etc.) enviam seus relatórios individuais de vendas para a matriz da Aurora Car Dealer consolidar a informação e criar seu relatório de performance de vendas único, de todo Brasil.

Como o processo hoje é manual, os acionistas da Aurora têm reclamado dos constantes atrasos na disponibilização da informação, dificultando o acompanhamento e a tomada de decisão.

Dado esse problema, o cliente deseja modernizar esse processo e possui alguns requisitos:

  • Automação da execução
  • Seja possível monitorar
  • Possua histórico de execução
  • Leitura automatizada de múltiplos arquivos
  • Escalabilidade automática caso surjam novas filiais

Por que o Spring Batch é necessário aqui?

Esse é um cenário clássico de processamento em lote (batch): toda semana precisamos ler muitos arquivos, padronizar/validar os dados, consolidar e gerar um único relatório para a matriz. Com confiabilidade e rastreabilidade.

O Spring Batch se encaixa muito bem porque ele entrega, de forma “pronta”:

  • Orquestração de um Job (pipeline) com etapas bem definidas: leitura → processamento → gravação.
  • Leitura de múltiplos arquivos (por filial) de forma padronizada e automatizada, sem depender de Excel manual.
  • Validação, tratamento de erros e tolerância a falhas (skip/retry), evitando que um arquivo ruim derrube todo o processo.
  • Reprocessamento seguro e retomada (restartability): se falhar no meio, conseguimos continuar de onde parou sem refazer tudo.
  • Histórico e auditoria via JobRepository: quem executou, quando, quanto processou, o que falhou e por quê.
  • Monitoramento e visibilidade operacional (status do job, métricas, logs), facilitando a vida de quem opera.
  • Escalabilidade quando o volume crescer: dá para paralelizar por filial/arquivo (partitioning/multi-thread) e suportar novas lojas sem redesenhar o fluxo.

Em resumo: o Spring Batch transforma esse processo manual e frágil em um processo repetível, observável e escalável, alinhado aos requisitos do negócio.

Spring Batch em ação

Vamos entender como aplicar o Spring Batch na resolução desse problema.

Esse é o diagrama em alto nível da solução:

As filiais exportam arquivos .csv no seguinte formato:

Copiar
dealer_id,sale_date,model,payment_type,sale_price_brl
D001,2026-02-07,Serra,Financiamento,149309.28
D001,2026-02-02,Lume,TED,96188.77
D002,2026-02-04,Lume,Consórcio,99056.68
...
D005,2026-02-03,Touro,PIX,229200.22

Você encontra os arquivos de exemplo na pasta src/main/resources/filial-report do projeto.

E a matriz gostaria de um relatório de vendas único, informando quantos modelos de cada carro foram vendidos e qual o valor total dessas vendas, seguindo o formato:

Copiar
dealer_name,model,units_sold,revenue_brl
Aurora Prime Paulista,Serra,1,149309.28
Aurora Prime Paulista,Lume,1,96188.77
Aurora Rio Mar,Lume,1,99056.68
Aurora Curitiba Norte,Touro,1,229200.22

Vamos exportar o report final para a pasta src/main/resources/matriz-report do projeto.

Mão na massa

O nosso projeto está disponível no GitHub do Blog, acesse aqui.

Nesta seção vamos explorar o projeto que já criamos para entender o funcionamento do Spring Batch.

Esse projeto utiliza:

  • Java 25
  • Maven
  • Spring Boot 4.0.2
  • Spring Batch
  • Spring JDBC
  • H2 Database

Importe seu projeto na IDE de sua preferência (Spring Tools, IntelliJ) e vamos começar.

Subindo os dados do negócio

Para nosso exemplo, toda informação está disponível no diretório src/main/resources.

  • application.properties: conexão com H2, caminhos de entrada e saída, e inicialização do banco.
  • schema-all.sql: estrutura da tabela dealers, onde existe "de-para" do ID para nome da concessionária.
  • data.sql: dados do ID vs Nome das filiais.

Conteúdo principal do application.properties:

Copiar
spring.application.name=car-dealer

spring.datasource.url=jdbc:h2:mem:cardealer
spring.datasource.username=sa
spring.datasource.password=

spring.sql.init.mode=always
spring.sql.init.schema-locations=classpath:schema-all.sql
spring.sql.init.data-locations=classpath:data.sql

app.filial-report-pattern=file:src/main/resources/filial-report/*.csv
app.matriz-report-file=src/main/resources/matriz-report/sales-report.csv

Nessa configuração, inicializamos o banco de dados em memória e setamos o caminho dos dados (filial e matriz).

Conteúdo do schema-all.sql:

Copiar
DROP TABLE IF EXISTS dealers;

CREATE TABLE dealers (
  dealer_id   VARCHAR(10)  NOT NULL PRIMARY KEY,
  dealer_name VARCHAR(120) NOT NULL
);

Estamos apontando esse arquivo em spring.sql.init.schema-locations do application.properties.

Conteúdo do data.sql:

Copiar
INSERT INTO dealers (dealer_id, dealer_name) VALUES
('D001', 'Aurora Prime Paulista'),
('D002', 'Aurora Rio Mar'),
('D003', 'Aurora Minas Centro'),
('D004', 'Aurora Sul Cristal'),
('D005', 'Aurora Curitiba Norte'),
('D006', 'Aurora Salvador Atlântico'),
('D007', 'Aurora Recife Boa Viagem'),
('D008', 'Aurora Fortaleza Dunas'),
('D009', 'Aurora Brasília Eixo'),
('D010', 'Aurora Goiânia Anhanguera'),
('D011', 'Aurora Manaus Rio Negro'),
('D012', 'Aurora Belém Guajará');

Estamos apontando esse arquivo em spring.sql.init.data-locations do application.properties.

Essa é nossa configuração mínima.

Camadas básicas do Spring Batch

Para se orientar, pense no batch como um pipeline que sai de um Job, passa por um Step e percorre as camadas de leitura, processamento e escrita.

  1. JobRepository (infraestrutura): persiste metadados de execução (tabelas BATCH_*, parâmetros, status, timestamps) e dá suporte a restart. Um repository atende N jobs e N steps.
  2. Job Launcher (execução): dispara o job e registra a execução no JobRepository.
  3. Job: representa o processo completo. Um launcher pode disparar N jobs; um job pode ser executado N vezes.
  4. Step: uma etapa dentro do job. Um job tem 1..N steps.
  5. ItemReader: lê os dados de entrada. Um step usa um reader.
  6. ItemProcessor: transforma os dados (opcional). Um step pode ter 0..1 processor.
  7. ItemWriter: grava o resultado. Um step usa um writer.

O JobLauncher cria a JobExecution e chama o Job. No Spring Boot, o JobLauncherApplicationRunner dispara automaticamente quando a aplicação sobe.

O JobRepository é o registro oficial do batch: ele guarda o histórico de execuções, parâmetros usados e status final, e é isso que permite retomar jobs com segurança.

Classes do projeto e o que cada método faz

Abaixo vamos aprofundar nos conceitos de cada classe do nosso projeto, e é importante que você já tenha feito o clone do projeto no seu ambiente local, repare em que pacote estamos criando cada classe, esses pacotes refletem as camadas do Spring Batch para facilitar o entendimento.

CarDealerApplication Classe principal do Spring Boot que inicia a aplicação e dispara o job.

Copiar
@SpringBootApplication
public class CarDealerApplication {
    public static void main(String[] args) {
        SpringApplication.run(CarDealerApplication.class, args);
    }
}

O @SpringBootApplication liga o auto-config e sobe o contexto. Ao subir, o JobLauncherApplicationRunner identifica o job configurado e executa.

config.SalesReportJobConfig Configuração do Job e do Step principal.

Copiar
@Configuration
public class SalesReportJobConfig {

    @Bean
    public Job salesReportJob(JobRepository jobRepository, Step salesReportStep) {
        return new JobBuilder("salesReportJob", jobRepository)
            .start(salesReportStep)
            .build();
    }

    @Bean
    public Step salesReportStep(JobRepository jobRepository,
                                PlatformTransactionManager transactionManager,
                                MultiResourceItemReader<SaleRecord> saleReader,
                                ItemProcessor<SaleRecord, SaleRecord> saleProcessor,
                                SalesReportWriter writer) {
        return new StepBuilder("salesReportStep", jobRepository)
            .<SaleRecord, SaleRecord>chunk(100)
            .reader(saleReader)
            .processor(saleProcessor)
            .writer(writer)
            .transactionManager(transactionManager)
            .build();
    }
}

A anotação @Configuration indica classe de configuração, base para declarar o nosso processo batch.

@Bean registra o Step no contexto do Spring. O Step faz referência para as classes de reader, processor e writer.

O chunk(100) define o tamanho de transação: o Spring lê, processa e grava 100 itens por commit.

reader.SaleReaderConfig Configuração dos readers usados para varrer e mapear os CSVs.

Copiar
@Configuration
public class SaleReaderConfig {

    @Bean
    @StepScope
    public MultiResourceItemReader<SaleRecord> saleReader(
            ResourcePatternResolver resolver,
            FlatFileItemReader<SaleRecord> saleFileReader,
            @Value("${app.filial-report-pattern}") String pattern) throws IOException {
        Resource[] resources = resolver.getResources(pattern);
        Arrays.sort(resources, Comparator.comparing(Resource::getFilename));
        MultiResourceItemReader<SaleRecord> reader = new MultiResourceItemReader<>(saleFileReader);
        reader.setResources(resources);
        return reader;
    }

    @Bean
    @StepScope
    public FlatFileItemReader<SaleRecord> saleFileReader() {
        RecordFieldSetMapper<SaleRecord> mapper = new RecordFieldSetMapper<>(SaleRecord.class);
        return new FlatFileItemReaderBuilder<SaleRecord>()
            .name("saleFileReader")
            .linesToSkip(1)
            .delimited()
            .names("dealerId", "saleDate", "model", "paymentType", "salePriceBrl")
            .fieldSetMapper(mapper)
            .build();
    }
}

@StepScope cria o bean no momento em que o step roda, permitindo usar parâmetros do job e garantindo que o reader/processor/writer não compartilhe estado entre execuções.

O MultiResourceItemReader busca vários arquivos usando o pattern do application.properties. O FlatFileItemReader mapeia cada linha do CSV para a classe modelo SaleRecord.

processor.SaleRecordProcessor Processor simples que normaliza campos antes da agregação.

Copiar
@Component
public class SaleRecordProcessor implements ItemProcessor<SaleRecord, SaleRecord> {
    @Override
    public SaleRecord process(SaleRecord item) {
        String model = item.model() == null ? null : item.model().trim();
        String paymentType = item.paymentType() == null ? null : item.paymentType().trim();
        return new SaleRecord(item.dealerId(), item.saleDate(), model, paymentType, item.salePriceBrl());
    }
}

Ao implements ItemProcessor<I,O>, o Spring chama o process para cada item. Como SaleRecord é imutável, retornamos uma nova instância com os campos normalizados.

domain.SaleRecord Record imutável que representa uma venda lida do CSV.

Copiar
public record SaleRecord(
    String dealerId,
    String saleDate,
    String model,
    String paymentType,
    BigDecimal salePriceBrl
) {}

Por ser record, o Java gera acessores (dealerId(), saleDate() etc.) automaticamente.

domain.ReportLine Record que agrega vendas por filial e modelo.

Copiar
public record ReportLine(String dealerName, String model, int unitsSold, BigDecimal revenueBrl) {
    public ReportLine(String dealerName, String model) {
        this(dealerName, model, 0, BigDecimal.ZERO);
    }

    public ReportLine addSale(BigDecimal salePrice) {
        return new ReportLine(dealerName, model, unitsSold + 1, revenueBrl.add(salePrice));
    }

    public String toCsv() {
        BigDecimal rounded = revenueBrl.setScale(2, RoundingMode.HALF_UP);
        return dealerName + "," + model + "," + unitsSold + "," + rounded.toPlainString();
    }
}

O record garante imutabilidade. A cada venda, criamos um novo agregado com addSale.

writer.SalesReportWriter Writer que consolida as vendas e grava o CSV final da matriz.

Copiar
@Component
public class SalesReportWriter implements ItemWriter<SaleRecord>, StepExecutionListener {

    @Override
    public void beforeStep(StepExecution stepExecution) {
        dealerNames = jdbcTemplate.query("SELECT dealer_id, dealer_name FROM dealers", rs -> {
            Map<String, String> map = new HashMap<>();
            while (rs.next()) {
                map.put(rs.getString("dealer_id"), rs.getString("dealer_name"));
            }
            return map;
        });
    }

    @Override
    public void write(Chunk<? extends SaleRecord> items) {
        for (SaleRecord item : items) {
            String dealerName = dealerNames.getOrDefault(item.dealerId(), item.dealerId());
            String key = dealerName + "|" + item.model();
            report.compute(key, (k, existing) -> {
                ReportLine base = existing == null ? new ReportLine(dealerName, item.model()) : existing;
                return base.addSale(item.salePriceBrl());
            });
        }
    }

    @Override
    public ExitStatus afterStep(StepExecution stepExecution) {
        writeReport();
        return ExitStatus.COMPLETED;
    }
}

Ao realizar o implements ItemWriter o Spring chama write a cada chunk. Já a interface StepExecutionListener permite executar código antes e depois do step (beforeStep e afterStep). O writer busca os nomes das filiais no H2 via JdbcTemplate, agrega em memória e grava o CSV final no caminho configurado.

Executando o job

Você pode executar o código usando a invocação do plugin do spring-boot via Maven:

Copiar
./mvnw spring-boot:run

Ou simplesmente executar a classe principal no método main na sua IDE:

Copiar
CarDealerApplication.main(...)

A aplicação sobe, o job é executado e o processo finaliza após concluir o step. Em produção, normalmente um agendador dispara a aplicação no horário combinado (cron, Kubernetes CronJob, Airflow, Control-M, Cloud Scheduler etc.).

E confira o arquivo gerado em src/main/resources/matriz-report/sales-report.csv. O arquivo bate com a especificação do nosso projeto.

Copiar
dealer_name,model,units_sold,revenue_brl
Aurora Prime Paulista,Serra,1,149309.28
Aurora Prime Paulista,Lume,1,96188.77
Aurora Rio Mar,Lume,1,99056.68
Aurora Curitiba Norte,Touro,1,229200.22

Conclusão

Soluções Batch são requisitos da maior parte dos negócios de grande e médio porte, saber se posicionar no tema é fundamental para todo desenvolvedor. Mesmo em empresas que não usam Spring Batch como solução, os conceitos aprendidos no estudo do framework são aplicáveis a todas as ferramentas de mercado.

E aí, curtiu? Esse é um exemplo enxuto, mas já mostra o poder do Spring Batch no dia a dia.

O código completo está no repositório do blog.

Fontes

Foto do autor Rafael Sotero
Rafael Sotero
Especialista em Engenharia de Software
Tenho mais de 12 anos de experiência na concepção e implementação de soluções robustas e escaláveis. Atuei em grandes empresas do setor financeiro e telecom levando inovação e simplicidade em ambientes distribuídos com Java, Python, e Cloud AWS.