Muitos desenvolvedores chegam ao mesmo gargalo: a API lenta. Adicionar um índice no banco alivia, mas não cura. Isso ocorre porque bancos relacionais, mesmo perfeitamente indexados, carregam uma burocracia inerente: parsing de SQL, planos de execução e leituras em disco. O Redis é o extremo oposto: elimina a burocracia operando exclusivamente em memória, com acesso direto à chave em O(1)*.
Neste artigo vamos demonstrar, com medições reais, como uma API Spring Boot com PostgreSQL se comporta e o que acontece quando você coloca Redis na frente de um banco relacional.
ℹ️ * Em computação, O(1) significa "tempo constante". Ou seja, o tempo que o Redis leva para encontrar uma chave é sempre o mesmo, não importa se ele tem cem ou cem milhões de registros armazenados.
Conceito
Redis (Remote Dictionary Server) é um armazenamento de estruturas de dados em memória. A diferença em relação a um banco relacional não está em "indexação melhor", mas na natureza do armazenamento e no protocolo de comunicação.
O padrão utilizado é o Cache-Aside Pattern: na primeira requisição (cache MISS), a aplicação consulta o PostgreSQL, armazena o resultado no Redis com um TTL e retorna ao cliente. Nas requisições seguintes (cache HIT), o Redis retorna o dado diretamente, sem tocar no banco.
O TTL (Time to Live) é outro conceito importante: ele controla por quanto tempo o dado permanece no Redis. Ao expirar, o próximo request consulta o PostgreSQL novamente e renova o cache. Isso significa que o banco não foi eliminado da arquitetura, ele continua sendo a fonte de verdade. O Redis serve como speed layer, não como substituto.
| PostgreSQL (com índice) | Redis | |
|---|---|---|
| Armazenamento | Disco com buffer pool em RAM | Memória RAM pura |
| Protocolo | TCP + wire protocol SQL | TCP + RESP2 (binário) |
| Operação | Parse → plano → B-tree → decode | GET key em O(1) |
| Latência típica | 40–150ms | 1–5ms |
| Papel na arquitetura | Fonte de verdade, escrita, integridade | Leitura frequente, hot data |
Mercado
No anúncio do Redis 8.2 GA, publicado em 8 de agosto de 2025, a Redis reporta até 35% de redução na latência de comandos e até 49% mais throughput em um workload típico de cache com 20% de escritas e 80% de leituras, superando 1 milhão de operações por segundo em uma única instância. No segmento de fintechs, seguradoras e serviços financeiros, onde consultas ocorrem com muita frequência, controlar a latência de resposta é requisito de negócio direto. Se o atraso é percebido pelo usuário, é ainda pior, pois impacta na experiência.
O Google Web Vitals define 100ms como o limiar de resposta percebida como instantânea. Geralmente uma API que faz JOIN em três tabelas a cada requisição raramente fica abaixo desse número sob carga moderada. Com Redis, esse limite passa a ser trivial de atingir.
Para o mercado de trabalho, dominar esse padrão de arquitetura é diferencial real. Levantamentos no LinkedIn Jobs mostram Redis como uma das tecnologias mais recorrentes em vagas de backend sênior no Brasil e na gringa.
Caso de Uso: FipeSearch API
Vamos explorar um serviço já conhecido pelo brasileiro: a tabela FIPE.
A tabela FIPE é a referência nacional para precificação de veículos no Brasil. Toda vez que alguém anuncia um carro no OLX, solicita seguro em uma corretora ou pede crédito com garantia de veículo, um sistema consulta a FIPE para saber quanto aquele veículo vale. Em uma plataforma de médio porte, essa consulta acontece milhares de vezes por minuto nas combinações mais populares: Corolla 2020, Onix 2021, Gol 2019.
Cada consulta envolve um JOIN entre três tabelas: marcas, modelos e referências de preço. Com PostgreSQL bem configurado e índices nas chaves estrangeiras, imagine que essa query responde em 40 a 100 milissegundos. À primeira vista parece aceitável. Mas com 500 requisições por minuto, isso representa 500 queries simultâneas no banco. Em horários de pico, o connection pool começa a enfileirar requisições, e a latência cresce não mais por causa da query, mas por espera de conexão disponível.
A solução não é aumentar o pool de conexões indefinidamente. É eliminar a query para os dados que já foram buscados.
Nossa aplicação, a FipeSearch API, expõe endpoints REST para consulta da tabela FIPE. Os dados vivem no PostgreSQL: marcas, modelos e referências mensais de preço. O Redis fica na frente, interceptando as consultas que já foram respondidas antes.
Endpoint principal: GET /fipe/{modeloId}/{anoModelo}
Sem cache: a requisição executa um JOIN entre referencias, modelos e marcas no PostgreSQL via JPA. Latência: ~8ms (sem carga, com carga pode ser bem maior) no ambiente de desenvolvimento.
Com Redis: a segunda requisição retorna o dado já serializado em JSON diretamente da memória. Latência: ~4ms. O banco não é consultado.
⚠️ São valores de referência no meu ambiente. No seu ambiente haverá variações, todavia o REDIS sempre será mais rápido.
Requisitos e Motivadores
Vamos entender o porquê de o REDIS ser especial no nosso caso de uso e quais problemas estamos resolvendo.
Frequência de variação: a tabela FIPE é atualizada mensalmente pela Fundação Instituto de Pesquisas Econômicas. Um TTL de 24 horas é mais do que conservador: garante frescor sem sacrificar performance.
Distribuição de acesso: consultas seguem a lei de Pareto. Cerca de 20% das combinações de modelo e ano respondem por 80% das consultas. Essas combinações "quentes" são exatamente as que aliviam o banco de dados, evitando retrabalho computacional.
Escalabilidade horizontal: cache local (in-memory na JVM) cria inconsistência quando a aplicação escala horizontalmente: cada instância teria sua própria versão do dado. Redis centralizado garante que todas as réplicas sirvam o mesmo resultado.
Connection pool: sem cache, cada requisição abre e fecha uma conexão com o PostgreSQL. Com Redis servindo as consultas quentes, o pool de conexões fica disponível para operações de escrita e para as consultas que realmente precisam de dado fresco.
Detalhes Técnicos
O Spring Boot expõe a abstração de cache via anotações: @Cacheable, @CacheEvict e @CachePut. Essa abstração é independente da implementação: o mesmo código funciona com Caffeine (in-memory, útil em testes) ou Redis (distribuído, para produção), sem alterar o código de negócio.
Para Redis, o RedisCacheManager serializa os valores como JSON via GenericJackson2JsonRedisSerializer. Isso permite inspecionar as entradas no Redis CLI e garante compatibilidade entre versões da JVM, algo que a serialização Java nativa não oferece.
A chave de cache gerada pelo Spring segue o padrão cacheName::key. Com a configuração key = "#modeloId + ':' + #anoModelo", o resultado de uma consulta ao modelo 59, ano 2020, fica armazenado como:
fipe::59:2020
O comando Redis executado pelo Spring ao popular o cache é um SET fipe::59:2020 <dado> EX 86400, onde EX 86400 define o TTL de 24 horas (86.400 segundos). A leitura é um GET fipe::59:2020, que retorna em microssegundos.
Implementação
Estrutura do projeto
fipe-search/ ├── docker/ │ └── docker-compose.yml ├── src/main/ │ ├── java/br/com/devsuperior/fipe_search/ │ │ ├── config/ │ │ │ └── CacheConfig.java │ │ ├── controller/ │ │ │ └── FipeController.java │ │ ├── dto/ │ │ │ └── ConsultaFipeDTO.java │ │ ├── entity/ │ │ │ ├── MarcaEntity.java │ │ │ ├── ModeloEntity.java │ │ │ └── ReferenciaEntity.java │ │ ├── repository/ │ │ │ └── ReferenciaRepository.java │ │ ├── service/ │ │ │ └── FipeService.java │ │ └── FipeSearchApplication.java │ └── resources/ │ ├── application.properties │ └── data.sql └── pom.xml
docker-compose.yml
Dois serviços: PostgreSQL como fonte de verdade e Redis como speed layer. Ambos obrigatórios para o projeto funcionar.
services: postgres: image: postgres:18-alpine container_name: fipesearch-postgres environment: POSTGRES_DB: fipesearch POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - "5432:5432" redis: image: redis:8.6.1-alpine container_name: fipesearch-redis ports: - "6379:6379" command: redis-server --save "" --appendonly no
pom.xml
Acesse o arquivo completo no GitHub. Clique aqui.
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>4.0.4</version> <relativePath/> </parent> <properties> <java.version>25</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webmvc</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> </dependencies>
application.properties
Acesse o arquivo completo no GitHub. Clique aqui.
spring.application.name=fipe-search spring.datasource.url=jdbc:postgresql://localhost:5432/fipesearch spring.datasource.username=postgres spring.datasource.password=postgres spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.defer-datasource-initialization=true spring.sql.init.mode=always spring.data.redis.host=localhost spring.data.redis.port=6379 app.cache.name=fipe app.cache.ttl-hours=24 logging.level.br.com.devsuperior.fipe_search=DEBUG
Entidades JPA
As três entidades representam o modelo relacional da tabela FIPE. O Spring cria as tabelas automaticamente via ddl-auto: create-drop.
// MarcaEntity.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/entity/MarcaEntity.java @Entity @Table(name = "marcas") public class MarcaEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String nome; // construtores, getters e setters completos no GitHub ↑ }
// ModeloEntity.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/entity/ModeloEntity.java @Entity @Table(name = "modelos") public class ModeloEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "marca_id") private MarcaEntity marca; private String nome; // construtores, getters e setters completos no GitHub ↑ }
// ReferenciaEntity.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/entity/ReferenciaEntity.java @Entity @Table(name = "referencias") public class ReferenciaEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "modelo_id") private ModeloEntity modelo; @Column(name = "ano_modelo") private Integer anoModelo; private BigDecimal preco; @Column(name = "mes_referencia") private String mesReferencia; // construtores, getters e setters completos no GitHub ↑ }
data.sql
O Spring Boot executa data.sql automaticamente ao iniciar, populando o banco com referências FIPE. Abaixo, um trecho representativo com 5 modelos. O arquivo completo está no repositório do GitHub: data.sql
-- Base de simulação, não são dados reais INSERT INTO marcas (nome) VALUES ('Toyota'), ('Honda'); INSERT INTO modelos (marca_id, nome) VALUES (1, 'Corolla'), (1, 'Hilux'), (2, 'Civic'), (2, 'HR-V'), (2, 'Fit'); INSERT INTO referencias (modelo_id, ano_modelo, preco, mes_referencia) VALUES (1, 2023, 118095.00, '2026-03'), (2, 2021, 208322.00, '2026-03'), (3, 2021, 128455.00, '2026-03'), (4, 2021, 115856.00, '2026-03'), (5, 2021, 84776.00, '2026-03');
ConsultaFipeDTO.java
// ConsultaFipeDTO.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/dto/ConsultaFipeDTO.java public record ConsultaFipeDTO( String marca, String modelo, Integer anoModelo, BigDecimal preco, String mesReferencia) implements Serializable { private static final long serialVersionUID = 1L; }
ReferenciaRepository.java
A query JPQL faz o JOIN pelas associações mapeadas nas entidades. O ORDER BY r.mesReferencia DESC combinado com findFirst() no service garante que sempre a referência mais recente do mês seja retornada.
// ReferenciaRepository.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/repository/ReferenciaRepository.java public interface ReferenciaRepository extends JpaRepository<ReferenciaEntity, Long> { @Query(""" SELECT new br.com.devsuperior.fipe_search.dto.ConsultaFipeDTO( ma.nome, mo.nome, r.anoModelo, r.preco, r.mesReferencia) FROM ReferenciaEntity r JOIN r.modelo mo JOIN mo.marca ma WHERE r.modelo.id = :modeloId AND r.anoModelo = :anoModelo ORDER BY r.mesReferencia DESC """) List<ConsultaFipeDTO> findReferencias( @Param("modeloId") Long modeloId, @Param("anoModelo") Integer anoModelo); }
CacheConfig.java
A configuração implementa CachingConfigurer, permitindo ao Spring utilizar automaticamente o cacheManager(), o cacheResolver() e o keyGenerator() definidos aqui, sem precisar referenciá-los explicitamente nos @Cacheable e @CacheEvict. O método keyGenerator concatena os parâmetros do método com :, gerando chaves no padrão fipe::1:2023.
// CacheConfig.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/config/CacheConfig.java @Configuration @EnableCaching public class CacheConfig implements CachingConfigurer { private final RedisConnectionFactory connectionFactory; private final String cacheName; private final long ttlHours; public CacheConfig( RedisConnectionFactory connectionFactory, @Value("${app.cache.name}") String cacheName, @Value("${app.cache.ttl-hours:24}") long ttlHours ) { this.connectionFactory = connectionFactory; this.cacheName = cacheName; this.ttlHours = ttlHours; } @Override public CacheManager cacheManager() { var defaultConfig = RedisCacheConfiguration.defaultCacheConfig() .disableCachingNullValues() .entryTtl(Duration.ofHours(ttlHours)); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(defaultConfig) .initialCacheNames(Set.of(cacheName)) .disableCreateOnMissingCache() .build(); } @Override public CacheResolver cacheResolver() { return context -> List.of( Objects.requireNonNull( cacheManager().getCache(cacheName), () -> "Cache not found: " + cacheName ) ); } @Override public KeyGenerator keyGenerator() { return (_, _, params) -> String.join(":", Arrays.stream(params) .map(String::valueOf) .toList()); } }
FipeService.java
Como CacheConfig implementa CachingConfigurer, o Spring utiliza automaticamente o keyGenerator definido lá para todos os métodos anotados nesse serviço. Por isso basta anotar com @Cacheable e @CacheEvict. Isso simplifica o service para focar no negócio e reduz a carga nas anotações.
Explicando as anotações do service:
@Cacheable: diz ao Spring "antes de executar o método, veja se já existe um valor no cache com essa chave". Se houver, ele retorna direto do Redis sem ir ao banco.@CacheEvict: diz ao Spring "remova do cache a entrada correspondente a essa chave". Útil para invalidar o cache antes do final do TTL.
// FipeService.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/service/FipeService.java @Service public class FipeService { private static final Logger LOGGER = LoggerFactory.getLogger(FipeService.class); private final ReferenciaRepository referenciaRepository; public FipeService(ReferenciaRepository referenciaRepository) { this.referenciaRepository = referenciaRepository; } @Cacheable public ConsultaFipeDTO consultar(Long modeloId, Integer anoModelo) { LOGGER.debug("Cache MISS — consultando PostgreSQL: modelo={}, ano={}", modeloId, anoModelo); return referenciaRepository .findReferencias(modeloId, anoModelo) .stream() .findFirst() .orElseThrow(() -> new ResponseStatusException( HttpStatus.NOT_FOUND, "Consulta FIPE não encontrada: modelo=%d, ano=%d" .formatted(modeloId, anoModelo))); } @CacheEvict public void invalidar(Long modeloId, Integer anoModelo) { LOGGER.debug("Cache invalidado: modelo={}, ano={}", modeloId, anoModelo); } } }
FipeController.java
// FipeController.java → https://github.com/devsuperior/blog/blob/main/articles/use-redis-para-reduzir-a-lat-ncia-de-apis-rest/projects/fipe-search/src/main/java/br/com/devsuperior/fipe_search/controller/FipeController.java @RestController @RequestMapping("/fipe") public class FipeController { private final FipeService fipeService; public FipeController(FipeService fipeService) { this.fipeService = fipeService; } @GetMapping("/{modeloId}/{anoModelo}") public ConsultaFipeDTO consultar( @PathVariable Long modeloId, @PathVariable Integer anoModelo) { return fipeService.consultar(modeloId, anoModelo); } @DeleteMapping("/{modeloId}/{anoModelo}/cache") public ResponseEntity<Void> invalidarCache( @PathVariable Long modeloId, @PathVariable Integer anoModelo) { fipeService.invalidar(modeloId, anoModelo); return ResponseEntity.noContent().build(); } }
Execução
Suba os dois serviços no docker e inicie a aplicação:
docker compose up -d ./mvnw spring-boot:run
Execute dois requests consecutivos para o mesmo veículo, medindo o tempo de resposta com a flag -w do curl:
- Primeira chamada, cache
MISS: FipeService executa, JPA consulta o PostgreSQL
curl -s "http://localhost:8080/fipe/1/2023" -w "\nTempo: %{time_total}s\n" # Resposta: {"marca":"Toyota","modelo":"Corolla","anoModelo":2023,"preco":118095.00,"mesReferencia":"2026-03"} Tempo: 0.351231s
- Segunda chamada, cache HIT: Spring retorna do Redis, método não é executado
curl -s "http://localhost:8080/fipe/1/2023" -w "\nTempo: %{time_total}s\n" # Resposta {"marca":"Toyota","modelo":"Corolla","anoModelo":2023,"preco":118095.00,"mesReferencia":"2026-03"} Tempo: 0.008070s
🔝 Se você dividir o tempo maior pelo menor (0.351231 ➗ 0.008070), verá que o novo tempo é cerca de 43,5 vezes mais rápido que o original!
Mas vou ser justo com você: a primeira request após o startup da aplicação no Spring geralmente demora um pouco, esse é o cold start da JVM, das classes do Spring e aberturas de conexão, ou seja, a primeira requisição com e sem o cache globalmente tende a ser mais lenta. Sem parar a aplicação, remova o cache executando a chamada abaixo e repita o teste.
- Exclusão do cache
curl -X "DELETE" -s "http://localhost:8080/fipe/1/2023/cache"
Repetindo os testes, tivemos (no meu ambiente):
- Sem cache: 0.007939s (~8ms)
- Com cache: 0.004395s (~4ms)
Agora sim! Esses são nossos números de referência!
Mesmo nesse cenário, o Redis termina quase na metade do tempo. 🎉
O log confirma: a mensagem de debug aparece somente no primeiro request. Nos subsequentes, o método consultar não é invocado: o Spring retorna o valor do Redis antes de chegar ao corpo do método.
DEBUG 2128 --- [fipe-search] [nio-8080-exec-2] b.c.d.fipe_search.service.FipeService : Cache MISS — consultando PostgreSQL: modelo=1, ano=2023
Conclusão
Com uma anotação @Cacheable e uma classe de configuração, a FipeSearch API diminuiu pela metade o tempo para buscar uma informação no banco de dados, e o ganho não se limita à latência: menos acesso ao banco de dados resulta em uma infraestrutura menor, em uma divisão de responsabilidade com o cache e em uma arquitetura segura para todos os tamanhos de empresas.
Essa é a dicotomia central do padrão Cache-Aside: não é "Redis em vez do banco", mas "Redis na frente do banco". O banco responde uma vez, o Redis responde todas as vezes seguintes enquanto o dado for válido. Quanto mais quente o dado, maior o ganho. No caso da tabela FIPE, onde 20% das combinações concentram 80% das consultas (em nosso caso de uso), o ganho composto ao longo do dia é expressivo.
Em nosso artigo, o Redis fez o que faz melhor: servir dados quentes em microssegundos. Imagine isso em escala, milhões de requisições. Legal pensar nisso, certo? 🤪


