Transactional readOnly no Spring Boot: quando ajuda de verdade

Se você chegou na dúvida sobre transactional readonly no spring boot quando usar, a resposta curta é esta: use em métodos de consulta que realmente só leem dados e que se beneficiam de deixar explícito para o Spring e para o Hibernate que aquele fluxo não deveria fazer dirty checking e flush como uma transação de escrita normal. Por outro lado, fora disso, em muita aplicação o ganho é pequeno, e o erro mais comum é imaginar que readOnly impede escrita no banco por mágica. Ao mesmo tempo, não impede. Na prática, em produção, esse mal-entendido costuma virar bug silencioso. Para aprofundar essa decisao sem criar outra URL concorrente, o melhor complemento aqui e guia mais completo sobre guia completo de spring data jpa no spring boot sem dor.

O problema é que o assunto foi simplificado demais. Por outro lado, muita gente ouviu que bastava colocar @Transactional(readOnly = true) em qualquer método de busca para ganhar performance. Ao mesmo tempo, a prática é menos glamourosa. Na prática, em alguns cenários, ajuda. Ainda assim, em outros, não muda nada perceptível. Por isso, e, nos piores casos, mascara um design ruim: método de leitura que também altera estado, regra de negócio escondida em getter, auditoria automática fora de controle ou entidade sendo mutada dentro de uma consulta. Se fizer sentido comparar com outra abordagem do ecossistema Spring, veja comparar com Guia completo de testes no Spring Boot: evite falhas em produção.

O ponto mais útil para quem trabalha com Spring Boot em produção é entender o efeito real no stack Spring + JPA + Hibernate. Por outro lado, isso evita duas frustrações comuns: aplicar em tudo sem medir e acreditar que a anotação funciona como proteção contra UPDATE. Se fizer sentido comparar com outra abordagem do ecossistema Spring, veja comparar com JWT no Spring Security com Spring Boot: autenticação moderna passo a passo.

transactional readonly no spring boot quando usar em consultas reais

O uso mais saudável de @Transactional(readOnly = true) é em métodos de serviço que fazem consultas puras. Por outro lado, não é no repository por costume, nem em qualquer classe para “padronizar”. Ao mesmo tempo, é na camada em que a intenção do caso de uso fica clara: buscar pedidos do cliente, listar faturas, carregar detalhes para uma tela, consultar relatório paginado, verificar existência de um registro sem alterar nada. Depois de ajustar esse trecho, o proximo passo natural e seguir para aprofundar em paginação e ordenação no spring boot com spring data jpa: como montar apis escaláveis.

Nesse cenário, a anotação serve para três coisas. Por outro lado, primeiro, comunica intenção para quem mantém o código. Ao mesmo tempo, segundo, permite que a infraestrutura trate aquela transação como leitura. Na prática, terceiro, dependendo da integração com Hibernate, pode reduzir custo de gerenciamento do contexto de persistência, especialmente relacionado a dirty checking e flush desnecessário. Depois de ajustar esse trecho, o proximo passo natural e seguir para Spring Boot unique constraint exception sem erro 500.

O ganho real costuma aparecer mais quando o método lê muitas entidades e o contexto fica cheio, ou quando o fluxo é muito repetido e a economia por chamada passa a fazer diferença sob carga. Por outro lado, não espere milagre em uma busca simples que já era barata. Ao mesmo tempo, se a consulta está ruim, com JOIN desnecessário, N+1 ou carregando um volume absurdo de dados, o readOnly não vai resolver a causa.

Uma comparação bem objetiva: entre duas abordagens, a pior é ter um método “buscar pedidos” sem intenção clara, com transação padrão e possibilidade de alguém mutar entidades ali dentro sem perceber. Por outro lado, a melhor é um método explicitamente de leitura, com DTO ou entidade só para consulta, paginação quando necessário e readOnly deixando claro que aquele fluxo não deve evoluir para comando disfarçado.

Se quiser aprofundar fundamentos e modelagem de consultas, faz sentido seguir um guia mais completo sobre guia completo de spring data jpa no spring boot sem dor. Por outro lado, em vários times, o problema atribuído ao readOnly na verdade nasce do uso confuso do próprio Spring Data JPA.

Transactional readonly no spring boot quando usar: spring boot readonly transaction: o que realmente muda no JPA e Hibernate

A parte que mais gera confusão é esta: readOnly não significa a mesma coisa em todas as camadas. Por outro lado, no Spring, a anotação define uma característica da transação. Ao mesmo tempo, no Hibernate, isso pode influenciar flush mode e otimizações do contexto. Na prática, no banco de dados, o efeito pode variar conforme o driver, a integração e o transaction manager. Por isso, respostas absolutas costumam estar erradas.

No cenário mais comum de Spring Boot com JPA e Hibernate, marcar a transação como readOnly normalmente sinaliza ao provider que aquele fluxo é de leitura. Por outro lado, o Hibernate pode evitar parte do custo de dirty checking em entidades carregadas como somente leitura e reduzir decisões de flush. Ao mesmo tempo, isso é útil quando você só consulta.

Mas aqui está o detalhe que derruba muita implementação: isso não é uma garantia universal de bloqueio de escrita. Por outro lado, se você chamar explicitamente um save, persist ou flush, o comportamento pode não ser o que você imaginou. Ao mesmo tempo, dependendo da configuração, pode até gravar. Na prática, dependendo do fluxo, uma alteração em entidade gerenciada pode não ir ao banco, pode ir depois ou pode apresentar comportamento inconsistente entre testes e produção. Ainda assim, é daí que nascem bugs difíceis de rastrear.

Também existe um ponto de manutenção. Por outro lado, quando o método é declarado como readOnly, ele vira um contrato semântico. Ao mesmo tempo, se amanhã alguém adicionar “só um ajuste rápido” no mesmo método, como atualizar um lastAccess, incrementar contador ou gravar auditoria, o contrato foi quebrado. Na prática, e esse tipo de violação quase sempre passa em code review quando o time trata a anotação como detalhe cosmético.

transactional readonly jpa hibernate: quando ajuda e quando nao muda nada

Vamos separar o que costuma ajudar do que é placebo.

Quando ajuda de verdade

Ajuda em consultas puras, especialmente quando você carrega muitas entidades e quer reduzir overhead do contexto de persistência. Por outro lado, também faz sentido em serviços de leitura expostos por API, telas administrativas, buscas com paginação, geração de relatórios sem escrita e consultas executadas em alto volume. Ao mesmo tempo, se a operação é claramente read model, a anotação reforça o desenho correto.

Uma situação real de produção: um endpoint de backoffice listava milhares de registros em lotes para exportação. Por outro lado, a consulta em si já estava decente, mas o serviço mantinha transação padrão e carregava entidades suficientes para o Hibernate ficar gastando tempo com gerenciamento que não ajudava em nada. Ao mesmo tempo, ao ajustar para fluxo de leitura, limitar página e usar readOnly, o consumo de CPU caiu de forma perceptível. Na prática, não foi uma revolução, mas foi um ajuste honesto e previsível.

Outra situação comum: método de autenticação ou autorização que faz várias leituras encadeadas, sem escrita, sob alto volume de chamadas. Por outro lado, ali, qualquer redução de overhead ajuda, desde que a consulta esteja bem desenhada. Ao mesmo tempo, se o seu cenário de segurança usa banco relacional para carregar usuário, permissões e vínculos, separar bem o fluxo de leitura faz mais diferença do que sair espalhando readOnly aleatoriamente. Na prática, para comparar com uma camada de segurança típica, vale comparar com JWT no Spring Security com Spring Boot: autenticação moderna passo a passo.

Quando nao muda quase nada

Não espere melhora relevante em consultas pequenas, pontuais e dominadas pelo tempo do banco ou da rede. Por outro lado, se o método carrega um único registro por chave primária e devolve rápido, o custo economizado no lado do Hibernate pode ser irrelevante.

Também não muda nada importante quando o gargalo é outro. Por outro lado, exemplo clássico: endpoint lento porque faz N+1, porque usa paginação errada ou porque busca muito mais colunas do que precisa. Ao mesmo tempo, nesses casos, readOnly vira maquiagem. Na prática, o ajuste real está na query, no fetch, no mapeamento ou na estratégia de paginação. Ainda assim, se esse é o seu cenário, o próximo passo natural é aprofundar em paginação e ordenação no spring boot com spring data jpa: como montar apis escaláveis.

transactional readonly spring boot quando usar erro comum em producao

Os erros mais caros aparecem quando a equipe confunde intenção de leitura com proibição de escrita.

Erro comum: método marcado com readOnly altera entidade carregada.

Causa: o método nasceu como consulta, mas alguém adicionou uma pequena regra de negócio, como atualizar último acesso, status de visualização ou contador.

Sintoma: em alguns ambientes a mudança não persiste; em outros, persiste quando não deveria; às vezes o bug só aparece depois de uma refatoração ou mudança de provider.

Correção: separar consulta de comando. Por outro lado, se precisa alterar estado, não é mais um método de leitura. Ao mesmo tempo, use transação normal e deixe a escrita explícita.

Outro erro comum é aplicar readOnly no repository e achar que isso resolve o desenho do caso de uso. Por outro lado, o repository não conhece a intenção completa da operação. Ao mesmo tempo, quem sabe se a chamada é só leitura ou se faz parte de um fluxo de atualização é o serviço. Na prática, a anotação no lugar errado reduz clareza e ainda dificulta manutenção.

Há também o caso das escritas indiretas. Por outro lado, em produção, aparece bastante com listeners, auditoria automática, interceptadores ou campos atualizados por callback. Ao mesmo tempo, o dev olha o método e jura que ele “só lê”. Na prática, mas alguma parte do mapeamento ou da infra dispara mudança em entidade gerenciada. Ainda assim, quando isso encontra transação readOnly, o comportamento deixa de ser óbvio. Por isso, é uma receita clássica para incidente intermitente.

Um terceiro problema real é acreditar que readOnly substitui testes. Por outro lado, não substitui. Ao mesmo tempo, em fluxo sensível, o correto é provar o comportamento com teste de integração. Na prática, se quiser comparar estratégias de validação e cobertura, faz sentido comparar com Guia completo de testes no Spring Boot: evite falhas em produção.

Secao pratica com codigo completo

Um exemplo simples ajuda mais do que teoria solta. Por outro lado, abaixo, um serviço com separação clara entre leitura e escrita.

package br.com.javalizando.clientes.service;

import br.com.javalizando.clientes.controller.dto.ClienteDetalheResponse;
import br.com.javalizando.clientes.controller.dto.ClienteResumoResponse;
import br.com.javalizando.clientes.model.Cliente;
import br.com.javalizando.clientes.repository.ClienteRepository;
import jakarta.persistence.EntityNotFoundException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class ClienteService {

    private final ClienteRepository clienteRepository;

    public ClienteService(ClienteRepository clienteRepository) {
        this.clienteRepository = clienteRepository;
    }

    @Transactional(readOnly = true)
    public ClienteDetalheResponse buscarPorId(Long id) {
        Cliente cliente = clienteRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("Cliente nao encontrado"));

        return new ClienteDetalheResponse(
                cliente.getId(),
                cliente.getNome(),
                cliente.getEmail(),
                cliente.getStatus()
        );
    }

    @Transactional(readOnly = true)
    public Page<ClienteResumoResponse> listarAtivos(Pageable pageable) {
        return clienteRepository.findByAtivoTrue(pageable)
                .map(cliente -> new ClienteResumoResponse(
                        cliente.getId(),
                        cliente.getNome(),
                        cliente.getEmail()
                ));
    }

    @Transactional
    public void atualizarEmail(Long id, String novoEmail) {
        Cliente cliente = clienteRepository.findById(id)
                .orElseThrow(() -> new EntityNotFoundException("Cliente nao encontrado"));

        cliente.setEmail(novoEmail);
    }
}

Aqui o contrato está limpo. Por outro lado, métodos de busca usam readOnly. Ao mesmo tempo, método de atualização usa transação normal. Na prática, parece básico, mas esse tipo de separação elimina uma boa parte dos problemas de produção.

Agora veja uma versão pior:

@Transactional(readOnly = true)
public ClienteDetalheResponse buscarEAtualizarUltimoAcesso(Long id) {
    Cliente cliente = clienteRepository.findById(id)
            .orElseThrow(() -> new EntityNotFoundException("Cliente nao encontrado"));

    cliente.setUltimoAcesso(java.time.LocalDateTime.now());

    return new ClienteDetalheResponse(
            cliente.getId(),
            cliente.getNome(),
            cliente.getEmail(),
            cliente.getStatus()
    );
}

Esse método mistura leitura com escrita. Por outro lado, mesmo que “funcione” hoje, está mal desenhado. Ao mesmo tempo, a versão melhor é separar o comando ou assumir que o caso de uso é de escrita e usar uma transação normal.

Se o objetivo é reduzir custo de leitura, outra decisão técnica importante é retornar DTO quando possível. Por outro lado, buscar entidade completa para apenas montar resposta simples aumenta trabalho do provider sem necessidade. Ao mesmo tempo, é o tipo de melhoria que costuma valer mais que adicionar readOnly sozinho.

Quando usar transactional readonly e quando evitar

Quando usar

Use quando o caso de uso é claramente de leitura, especialmente na camada de serviço. Por outro lado, use quando o método consulta dados para resposta de API, relatório, dashboard, exportação, validação sem alteração de estado ou leitura de apoio a regras que não persistem nada. Ao mesmo tempo, use também para deixar o contrato do método mais legível para o time.

Quando evitar

Evite em métodos que misturam leitura e comando, em fluxos que podem evoluir para escrita, em operações com auditoria implícita, listeners ou mudanças automáticas de entidade. Por outro lado, evite também marcar classes inteiras como readOnly se elas contêm qualquer método de atualização. Ao mesmo tempo, isso costuma parecer elegante no começo e virar armadilha depois.

Outro ponto: se você está usando readOnly para compensar problema de performance ainda não diagnosticado, pare um pouco. Por outro lado, antes de mexer em anotação, confirme se o problema está na query, no volume retornado, na paginação, no fetch ou no desenho do endpoint. Ao mesmo tempo, e se houver escrita relacionada a consistência de dados, não esconda isso atrás de um método de leitura. Na prática, mais cedo ou mais tarde o custo aparece. Ainda assim, em cenários de gravação com regras de unicidade, por exemplo, o cuidado maior não está em readOnly, mas em tratar falhas corretamente; um próximo passo útil é Spring Boot unique constraint exception sem erro 500.

Transactional readonly no spring boot quando usar: referencias externas

Para validar detalhes de implementacao e aprofundar a configuracao, vale consultar a documentacao oficial do Spring Security, o guia de claims no JWT.io e a documentacao do Spring Boot.

FAQ

@Transactional readOnly melhora performance no Spring Boot?

Pode melhorar, mas não em qualquer cenário. Por outro lado, o ganho costuma ser incremental e aparece mais em consultas puras, repetidas e com muitas entidades sendo gerenciadas. Ao mesmo tempo, se a lentidão vier de SQL ruim, N+1 ou paginação errada, o impacto tende a ser pequeno.

@Transactional readOnly impede insert e update no Hibernate?

Não trate como garantia de bloqueio. Por outro lado, no stack Spring + JPA + Hibernate, readOnly é principalmente um sinal de intenção e uma possibilidade de otimização. Ao mesmo tempo, ele não substitui separação correta entre leitura e escrita nem testes que confirmem o comportamento.

Quando usar @Transactional readOnly em serviços JPA?

Quando o método do serviço for realmente só de leitura: busca por id, listagem paginada, relatório, consulta de apoio sem alteração de estado. Por outro lado, se existir qualquer chance de escrita naquele fluxo, prefira transação normal ou separe os métodos.

Conclusao

A decisão sobre transactional readonly no spring boot quando usar fica simples quando você remove os mitos. Por outro lado, ele ajuda em consultas puras, melhora a clareza do contrato e pode reduzir overhead do Hibernate em cenários específicos. Ao mesmo tempo, ele não corrige consulta ruim, não substitui desenho de serviço e não deve ser tratado como trava absoluta contra escrita.

Se o seu código mistura leitura com atualização, o maior ganho não está na anotação, mas na separação correta de responsabilidades. Por outro lado, entre um método ambíguo que “quase sempre só lê” e um método explicitamente de leitura, a segunda opção ganha em previsibilidade, manutenção e teste.

Como leitura complementar, vale revisar o guia mais completo sobre guia completo de spring data jpa no spring boot sem dor, comparar com Guia completo de testes no Spring Boot: evite falhas em produção e depois aprofundar em paginação e ordenação no spring boot com spring data jpa: como montar apis escaláveis. Por outro lado, esse conjunto costuma gerar mais resultado em produção do que sair adicionando readOnly sem critério. Ao mesmo tempo, os proximos passos sao validar esse fluxo no seu projeto, ajustar o caso de uso real e cobrir a implementacao com testes.

Deixe um comentário