No Spring, readOnly não é proteção

readOnly não transforma método em área segura.

Muita gente olha @Transactional(readOnly = true) e entende aquilo como uma trava confiável contra alteração de dados.

E, quando quer reforçar a ideia de "consulta pura", ainda empilha Propagation.NEVER por cima.

As duas leituras erram pelo mesmo motivo: tratam configuração transacional como se ela fosse política de negócio.

No Spring, não é isso que essas opções fazem.


readOnly é dica, não bloqueio

No geral, readOnly funciona como hint para a infraestrutura transacional e para o provider de persistência.

Ele pode comunicar intenção semântica e, em alguns cenários, ajudar em otimizações.

Mas isso é bem diferente de dizer que o método ficou tecnicamente impedido de escrever.

Um código como este mostra por que a leitura fica perigosa:

@Transactional(readOnly = true)
public void atualizarCadastro(Long clienteId, String novoEmail) {
    Cliente cliente = clienteRepository.findById(clienteId)
        .orElseThrow();

    cliente.alterarEmail(novoEmail);
    clienteRepository.save(cliente);
}

A anotação pode passar a sensação de "aqui ninguém escreve".

Só que readOnly não é mecanismo de autorização, não é trava de domínio e não é cerca de proteção contra código de escrita.

O ponto nem é discutir aqui a reação exata de cada stack ou provider diante desse fluxo.

O ponto é mais simples:

se o time quer impedir alteração, precisa modelar essa garantia na regra, na arquitetura ou no banco.

Depositar essa responsabilidade na anotação é inventar uma proteção que ela não prometeu.


O erro irmão: usar NEVER como padrão de leitura

Outro tropeço comum é decidir que toda consulta deveria rodar com Propagation.NEVER.

A ideia costuma soar elegante:

"leitura boa é leitura fora de transação."

Só que NEVER não significa "modo otimizado de leitura".

Significa: esse método falha se for chamado dentro de uma transação existente.

Um exemplo simples deixa isso mais claro:

@Service
public class FechamentoFaturaService {

    private final ClienteQueryService clienteQueryService;
    private final FaturaRepository faturaRepository;

    public FechamentoFaturaService(
        ClienteQueryService clienteQueryService,
        FaturaRepository faturaRepository
    ) {
        this.clienteQueryService = clienteQueryService;
        this.faturaRepository = faturaRepository;
    }

    @Transactional
    public void fechar(Long faturaId) {
        Fatura fatura = faturaRepository.findById(faturaId)
            .orElseThrow();

        ClientePerfil perfil = clienteQueryService.buscarPerfil(fatura.getClienteId());

        fatura.fecharCom(perfil);
    }
}

@Service
public class ClienteQueryService {

    @Transactional(readOnly = true, propagation = Propagation.NEVER)
    public ClientePerfil buscarPerfil(Long clienteId) {
        return clienteRepository.buscarPerfil(clienteId);
    }
}

Na superfície, parece que buscarPerfil() está "protegido" como leitura.

Na prática, ele virou um ponto que explode sempre que um fluxo transacional legítimo precisa consultar algo no meio do caminho.

O método nem executa.

Ele falha antes da consulta porque a regra configurada não era "leitura segura".

Era "não aceito transação ativa".

Esse ponto conversa diretamente com o post sobre propagation: o nome parece explicar a intenção, mas o que vale é a semântica operacional.


O erro real é inventar semântica em cima da anotação

É isso que conecta os dois casos.

readOnly parece mais forte do que realmente é.

NEVER parece mais inocente do que realmente é.

Nos dois cenários, o erro nasce da mesma fonte: olhar para a configuração e imaginar uma semântica mais confortável do que a real.

readOnly não quer dizer "ninguém escreve aqui".

NEVER não quer dizer "consulta saudável".

Uma opção fala sobre intenção de leitura e possíveis otimizações.

A outra fala sobre falhar quando existe transação ativa.

Nenhuma delas substitui desenho transacional consciente.


O ponto que vale fixar

Se a equipe quer garantir que um fluxo não escreva, essa garantia precisa existir em um lugar mais sólido do que a anotação.

Se a equipe quer que uma consulta possa ser chamada dentro e fora de transação, precisa escolher propagation com base nesse contrato real, não em estética de leitura.

No Spring, configuração transacional ajuda muito.

Mas ela não corrige interpretação errada do time.