Use REDIS para reduzir a Latência de APIs REST

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

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
ArmazenamentoDisco com buffer pool em RAMMemória RAM pura
ProtocoloTCP + wire protocol SQLTCP + RESP2 (binário)
OperaçãoParse → plano → B-tree → decodeGET key em O(1)
Latência típica40–150ms1–5ms
Papel na arquiteturaFonte de verdade, escrita, integridadeLeitura 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:

Copiar
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

Código completo no GitHub

Copiar
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.

Copiar
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.

Copiar
<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.

Copiar
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.

Copiar
// 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 ↑
}
Copiar
// 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 ↑
}
Copiar
// 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

Copiar
-- 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

Copiar
// 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.

Copiar
// 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.

Copiar
// 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.
Copiar
// 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

Copiar
// 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:

Copiar
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
Copiar
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
Copiar
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
Copiar
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.

Copiar
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? 🤪

Código completo no GitHub

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.