Como Processar Consultas de CPF em Lote Usando Java Streams e CompletableFuture

Aprenda a processar consultas de CPF em lote usando Java Streams e CompletableFuture, com concorrência controlada e agregação de resultados.

Redação CPFHub.io
Redação CPFHub.io
··7 min de leitura
Como Processar Consultas de CPF em Lote Usando Java Streams e CompletableFuture

Para processar consultas de CPF em lote com Java, combine Streams para o pipeline declarativo com CompletableFuture e um ExecutorService de tamanho fixo para controlar a concorrência. Com 10 a 20 threads paralelas, um lote de 1.000 CPFs consultado via CPFHub.io é concluído em cerca de 20 segundos — contra mais de 3 minutos em processamento sequencial.

Introdução

O processamento em lote de CPFs é necessário em diversas operações: migração de bases de clientes, validação periódica de cadastros, compliance regulatório e onboarding em massa. O Java oferece duas ferramentas poderosas para esse cenário: Streams para processamento declarativo e CompletableFuture para concorrência assíncrona.


Processamento sequencial vs. paralelo

A diferença de performance entre abordagens sequenciais e paralelas é significativa em lotes grandes.

Abordagem100 CPFs1.000 CPFs10.000 CPFs
Stream sequencial~20s~200s~2000s
Parallel stream~4s~40s~400s
CompletableFuture (10 conc.)~2s~20s~200s
CompletableFuture (50 conc.)~0.4s~4s~40s

Stream sequencial -- simples, mas lento para lotes grandes.

Parallel stream -- usa o ForkJoinPool comum, limitado e compartilhado com outras operações.

CompletableFuture -- controle total sobre concorrência com executor customizado.


Cliente HTTP base

O cliente HTTP que será usado por todas as abordagens.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

public class CpfApiClient {

    private final HttpClient httpClient;
    private final String apiKey;
    private final ObjectMapper mapper;

    public CpfApiClient(String apiKey) {
    this.httpClient = HttpClient.newBuilder()
    .connectTimeout(Duration.ofSeconds(5))
    .build();
    this.apiKey = apiKey;
    this.mapper = new ObjectMapper();
    }

    public ResultadoCpf consultar(String cpf) {
    try {
    HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create(
    "https://api.cpfhub.io/cpf/" + cpf
    ))
    .header("x-api-key", apiKey)
    .timeout(Duration.ofSeconds(10))
    .GET()
    .build();

    HttpResponse<String> response = httpClient.send(
    request,
    HttpResponse.BodyHandlers.ofString()
    );

    JsonNode root = mapper.readTree(response.body());
    if (root.get("success").asBoolean()) {
    JsonNode data = root.get("data");
    return new ResultadoCpf(
    cpf, true,
    data.get("name").asText(),
    data.get("gender").asText(),
    data.get("birthDate").asText(),
    null
    );
    }
    return new ResultadoCpf(
    cpf, false, null, null, null,
    "Nao encontrado"
    );
    } catch (Exception e) {
    return new ResultadoCpf(
    cpf, false, null, null, null,
    e.getMessage()
    );
    }
    }
}

public record ResultadoCpf(
    String cpf,
    boolean sucesso,
    String nome,
    String genero,
    String dataNascimento,
    String erro
) {}

Processamento com Streams

A Stream API permite processamento declarativo e funcional.

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class CpfBatchProcessor {

    private final CpfApiClient apiClient;

    public CpfBatchProcessor(String apiKey) {
    this.apiClient = new CpfApiClient(apiKey);
    }

    // Processamento sequencial com Stream
    public List<ResultadoCpf> processarSequencial(List<String> cpfs) {
    return cpfs.stream()
    .map(cpf -> cpf.replaceAll("\\D", ""))
    .filter(cpf -> cpf.length() == 11)
    .map(apiClient::consultar)
    .toList();
    }

    // Processamento paralelo com Parallel Stream
    public List<ResultadoCpf> processarParalelo(List<String> cpfs) {
    return cpfs.parallelStream()
    .map(cpf -> cpf.replaceAll("\\D", ""))
    .filter(cpf -> cpf.length() == 11)
    .map(apiClient::consultar)
    .toList();
    }

    // Agregação de resultados
    public Map<Boolean, List<ResultadoCpf>> processarEAgrupar(
    List<String> cpfs) {
    return cpfs.parallelStream()
    .map(cpf -> cpf.replaceAll("\\D", ""))
    .filter(cpf -> cpf.length() == 11)
    .map(apiClient::consultar)
    .collect(Collectors.partitioningBy(
    ResultadoCpf::sucesso
    ));
    }
}

CompletableFuture com concorrência controlada

O CompletableFuture permite controle preciso sobre a concorrência.

import java.util.List;
import java.util.concurrent.*;

public class CpfAsyncBatchProcessor {

    private final CpfApiClient apiClient;
    private final ExecutorService executor;

    public CpfAsyncBatchProcessor(
    String apiKey, int concorrencia) {
    this.apiClient = new CpfApiClient(apiKey);
    this.executor = Executors.newFixedThreadPool(concorrencia);
    }

    public List<ResultadoCpf> processarAsync(
    List<String> cpfs) {

    List<CompletableFuture<ResultadoCpf>> futures =
    cpfs.stream()
    .map(cpf -> cpf.replaceAll("\\D", ""))
    .filter(cpf -> cpf.length() == 11)
    .map(cpf -> CompletableFuture.supplyAsync(
    () -> apiClient.consultar(cpf),
    executor
    ))
    .toList();

    return futures.stream()
    .map(CompletableFuture::join)
    .toList();
    }

    public CompletableFuture<List<ResultadoCpf>> processarAsyncNaoBloqueante(
    List<String> cpfs) {

    List<CompletableFuture<ResultadoCpf>> futures =
    cpfs.stream()
    .map(cpf -> cpf.replaceAll("\\D", ""))
    .filter(cpf -> cpf.length() == 11)
    .map(cpf -> CompletableFuture.supplyAsync(
    () -> apiClient.consultar(cpf),
    executor
    ))
    .toList();

    CompletableFuture<Void> allOf =
    CompletableFuture.allOf(
    futures.toArray(new CompletableFuture[0])
    );

    return allOf.thenApply(v ->
    futures.stream()
    .map(CompletableFuture::join)
    .toList()
    );
    }

    public void shutdown() {
    executor.shutdown();
    try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
    executor.shutdownNow();
    }
    } catch (InterruptedException e) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
    }
    }
}
ParâmetroValor RecomendadoJustificativa
Pool size10-20Equilibra concorrência e rate limit
Timeout por request10sEvita threads travadas
Batch size100-500Evita sobrecarga de memória
Delay entre batches1-5sRespeita rate limit

Processamento com relatório e progresso

Em lotes grandes, acompanhar o progresso é essencial.

import java.util.concurrent.atomic.AtomicInteger;

public class CpfBatchProcessorComProgresso {

    private final CpfAsyncBatchProcessor processor;

    public CpfBatchProcessorComProgresso(
    String apiKey, int concorrencia) {
    this.processor = new CpfAsyncBatchProcessor(
    apiKey, concorrencia
    );
    }

    public RelatorioLote processar(List<String> cpfs) {
    long inicio = System.currentTimeMillis();
    AtomicInteger processados = new AtomicInteger(0);
    int total = cpfs.size();

    List<ResultadoCpf> resultados = cpfs.stream()
    .map(cpf -> cpf.replaceAll("\\D", ""))
    .filter(cpf -> cpf.length() == 11)
    .map(cpf -> {
    ResultadoCpf resultado =
    processor.apiClient.consultar(cpf);
    int atual = processados.incrementAndGet();
    if (atual % 100 == 0 || atual == total) {
    double progresso =
    (atual * 100.0) / total;
    System.out.printf(
    "Progresso: %d/%d (%.1f%%)%n",
    atual, total, progresso
    );
    }
    return resultado;
    })
    .toList();

    long duracao = System.currentTimeMillis() - inicio;

    long sucessos = resultados.stream()
    .filter(ResultadoCpf::sucesso).count();
    long falhas = resultados.size() - sucessos;

    return new RelatorioLote(
    total, sucessos, falhas, duracao, resultados
    );
    }
}

public record RelatorioLote(
    int total,
    long sucessos,
    long falhas,
    long duracaoMs,
    List<ResultadoCpf> resultados
) {
    public double taxaSucesso() {
    return total > 0
    ? (sucessos * 100.0) / total : 0;
    }

    public void imprimir() {
    System.out.println("--- Relatorio de Lote ---");
    System.out.printf("Total: %d%n", total);
    System.out.printf("Sucessos: %d%n", sucessos);
    System.out.printf("Falhas: %d%n", falhas);
    System.out.printf("Taxa: %.1f%%%n", taxaSucesso());
    System.out.printf("Duracao: %dms%n", duracaoMs);
    }
}

Perguntas frequentes

Quantas threads devo usar para processar CPFs em lote com CompletableFuture?

Para a maioria dos casos, entre 10 e 20 threads oferecem o melhor equilíbrio entre velocidade e controle de consumo. Com latência média de ~900ms por consulta, 10 threads paralelas processam cerca de 11 CPFs por segundo. Aumente o pool gradualmente e monitore o tempo de resposta; se ele começar a subir, reduza a concorrência.

A API CPFHub.io bloqueia requisições quando o limite mensal é atingido?

Não. A CPFHub.io nunca retorna 429 nem bloqueia o serviço. Quando o limite do plano é ultrapassado, as consultas adicionais continuam funcionando normalmente e são cobradas a R$ 0,15 cada. Para evitar cobranças inesperadas em lotes grandes, implemente um contador local que interrompe o processamento ao atingir o limite desejado.

Como estruturar o processamento de lotes muito grandes (acima de 10.000 CPFs)?

Divida o lote em sub-lotes de 100 a 500 CPFs, introduza um delay de 1 a 5 segundos entre cada sub-lote e persista os resultados intermediários para permitir retomada em caso de falha. Use o RelatorioLote para acompanhar o progresso e identifique os CPFs com erro para reprocessamento seletivo.

Como garantir conformidade com a LGPD ao processar CPFs em lote?

Processe apenas os CPFs para os quais há base legal documentada (consentimento, contrato ou obrigação regulatória), conforme orienta a ANPD. Não armazene os CPFs em texto puro após o processamento, registre logs de auditoria com hash do CPF em vez do número real e aplique política de retenção mínima para os dados processados.


Conclusão

A combinação de Java Streams com CompletableFuture oferece uma solução poderosa e flexível para processamento em lote de CPFs. Streams provêm uma API declarativa para transformação e agregação de dados, enquanto CompletableFuture com executor customizado garante concorrência controlada e eficiente. O relatório de progresso e a agregação de resultados completam a solução, tornando-a adequada para operações de produção.

Cadastre-se em cpfhub.io — 50 consultas mensais gratuitas, sem cartão de crédito — e comece a processar lotes de CPF com alta performance diretamente do seu ambiente Java.

CPFHub.io

Pronto para integrar a API?

50 consultas gratuitas para testar agora. Sem cartão de crédito. Acesso imediato à documentação.

Redação CPFHub.io

Sobre a redação

Redação CPFHub.io

Time editorial especializado em APIs de CPF, identidade digital e compliance no mercado brasileiro. Produzimos guias técnicos, análises regulatórias e tutoriais sobre LGPD e KYC para desenvolvedores e líderes de produto.

WhatsAppFale conosco via WhatsApp