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.
| Abordagem | 100 CPFs | 1.000 CPFs | 10.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âmetro | Valor Recomendado | Justificativa |
|---|---|---|
| Pool size | 10-20 | Equilibra concorrência e rate limit |
| Timeout por request | 10s | Evita threads travadas |
| Batch size | 100-500 | Evita sobrecarga de memória |
| Delay entre batches | 1-5s | Respeita 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.
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.



