Spring Boot unique constraint exception sem erro 500

Spring Boot unique constraint exception sem erro 500

Você cadastra um usuário, produto ou cliente no Spring Boot, o banco encontra uma constraint violada e a API responde com 500. Por outro lado, esse é um dos erros mais comuns em projeto que já está em produção. Ao mesmo tempo, quando aparece uma spring boot unique constraint exception, quase nunca o melhor retorno é erro interno. Na prática, na prática, o problema normalmente é conflito de dados já existentes, campo obrigatório ausente persistido tardiamente ou referência inválida. Ainda assim, o caminho certo é entender por que o Spring traduziu a falha para DataIntegrityViolationException, identificar se o caso é conflito ou validação e devolver uma resposta útil, como 409 ou 422, sem esconder o problema atrás de um 500 genérico. 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.

Esse tipo de falha aparece muito em APIs que usam JPA, Hibernate e Spring Data. Por outro lado, o detalhe importante é que a exceção quase sempre nasce no banco ou no flush da sessão, não no momento em que você monta o objeto Java. Por isso ela surpreende quem acha que já validou tudo no DTO. Na prática, em produção, isso fica ainda mais evidente quando dois requests concorrentes tentam gravar o mesmo e-mail, o mesmo documento ou o mesmo código de pedido. 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.

Se você quiser reforçar a base de persistência antes de ajustar o tratamento, vale consultar o guia mais completo sobre guia completo de spring data jpa no spring boot sem dor, porque muita dor com constraint vem de entendimento incompleto do ciclo de persistência, flush e transação. Se fizer sentido comparar com outra abordagem do ecossistema Spring, veja comparar com API REST Spring Boot Java: Guia Completo com Exemplo Prático.

Spring boot unique constraint exception: secao pratica com codigo completo

Na pratica, um exemplo enxuto ajuda a sair da teoria e evitar erro comum de producao quando o projeto cresce. 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.

@RestController
@RequestMapping("/api/exemplo")
public class ExemploController {
  @GetMapping
  public ResponseEntity<String> listar() {
    return ResponseEntity.ok("ok");
  }
}

Por que a spring boot unique constraint exception acontece de verdade

DataIntegrityViolationException é uma exceção do ecossistema Spring para traduzir problemas de integridade de dados vindos de camadas mais baixas. Por outro lado, na prática, o Spring pega exceções do Hibernate, JPA ou JDBC e converte para uma hierarquia mais consistente. Ao mesmo tempo, isso é ótimo para não acoplar seu código a uma implementação específica, mas traz um efeito colateral: muita gente vê o nome genérico e responde 500 sem investigar a causa. 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.

Os cenários mais comuns são bem conhecidos em produção. Por outro lado, o primeiro é violação de unique constraint: e-mail duplicado, username repetido, CPF já existente, código externo já cadastrado. Ao mesmo tempo, o segundo é foreign key: tentativa de remover uma categoria que ainda possui produtos, ou salvar um registro apontando para uma entidade inexistente. Na prática, o terceiro é not null e outros limites de coluna, que passam despercebidos porque a validação de entrada não cobriu tudo ou porque a montagem da entidade foi alterada no service.

Um detalhe que pega muita gente: a exceção pode estourar no save, no saveAndFlush ou até no fechamento da transação. Por outro lado, se você usa apenas save e deixa o flush para mais tarde, o erro aparece fora do ponto em que a regra de negócio parecia estar sendo executada. Ao mesmo tempo, isso complica log, observabilidade e até o mapeamento da resposta.

Em sistemas com concorrência real, validar antes de persistir ajuda a UX, mas não elimina a necessidade da constraint. Dois requests podem executar existsByEmail ao mesmo tempo, ambos receberem false e um deles falhar só no commit. Ao mesmo tempo, isso não é bug do banco; é a razão pela qual a constraint existe.

DataIntegrityViolationException Spring Boot sem resposta 500: qual status usar

Se a API retorna 500 para toda violação de integridade, o cliente recebe a mensagem errada. Por outro lado, o servidor não quebrou. Ao mesmo tempo, ele recebeu uma operação inválida para o estado atual dos dados. Na prática, o código de status mais comum para unique constraint é 409 Conflict, porque o recurso entra em conflito com um estado já existente. Ainda assim, um e-mail que já está em uso, por exemplo, é um conflito clássico.

Já casos de dados semanticamente inválidos podem cair em 422 Unprocessable Entity, especialmente quando a operação está bem formada, mas não pode ser concluída por causa da regra persistida. Por outro lado, existe discussão aí, e times diferentes adotam padrões diferentes. Ao mesmo tempo, o ponto importante não é decorar uma resposta universal, e sim ser consistente. Na prática, se no seu projeto duplicidade de identificador único é conflito, devolva 409 em todos os endpoints equivalentes.

Para foreign key, também é comum usar 409 quando existe conflito com estado relacional atual, como excluir algo ainda referenciado. Por outro lado, se o problema é um identificador enviado para criar uma associação que não existe, muitos times preferem 422. Ao mesmo tempo, em APIs mais simples, 400 aparece bastante, mas tecnicamente costuma ser menos preciso quando o payload está sintaticamente correto.

A abordagem pior é esta: qualquer falha de persistência vira 500 com mensagem “erro interno”. Por outro lado, a abordagem melhor é separar erro de infraestrutura real de erro de integridade previsível. Ao mesmo tempo, isso melhora o contrato da API, reduz retrabalho do frontend e deixa o monitoramento mais honesto. Na prática, se quiser comparar esse desenho com uma API REST mais ampla, pode comparar com API REST Spring Boot Java: Guia Completo com Exemplo Prático.

Erro 500 constraint Spring Boot: sintomas comuns e como diagnosticar

Quando o problema chega como “erro 500 constraint spring boot”, normalmente o sintoma no log já entrega bastante coisa. Por outro lado, você vai ver DataIntegrityViolationException envolvendo uma causa mais específica, como ConstraintViolationException do Hibernate, SQLIntegrityConstraintViolationException do JDBC ou alguma mensagem do banco citando índice único, chave estrangeira ou coluna nula.

Situação real 1: cadastro duplicado sob concorrência

Um endpoint de cadastro de usuário verifica se o e-mail já existe e depois salva. Por outro lado, em ambiente local, tudo parece correto. Ao mesmo tempo, em produção, duas requisições quase simultâneas passam na checagem e uma falha no banco. Na prática, o sintoma é intermitente, difícil de reproduzir manualmente e aparece mais em campanhas, importações ou reprocessamentos. Ainda assim, a correção não é remover a constraint. Por isso, é assumir que a concorrência existe, manter a verificação prévia para feedback rápido e tratar a exceção para responder 409 quando o banco bloquear a duplicidade.

Situação real 2: exclusão de entidade ainda referenciada

Um endpoint remove um cliente ou categoria sem verificar dependências. Por outro lado, em alguns cenários funciona; em outros, o banco barra por foreign key. Ao mesmo tempo, o sistema então responde 500 e o suporte abre chamado dizendo que “deletar está quebrado”. Na prática, o problema real não é indisponibilidade. Ainda assim, o recurso está em uso. Por isso, a resposta ideal explica que a entidade não pode ser removida porque existem registros relacionados.

Bloco de erro comum

Causa: unique constraint no campo email e tratamento genérico da exceção.
Sintoma: POST /usuarios devolve 500 quando o e-mail já existe, embora o serviço esteja funcionando normalmente.
Correção: identificar a violação como conflito previsível, mapear para 409 e retornar payload de erro padronizado com mensagem clara para o cliente.

Se você estiver com dúvidas sobre como validar melhor antes da persistência e como isso se conecta com 400 ou 422, há um material complementar útil para comparar decisões de contrato: comparar com Spring boot 422 ou 400 validacao api rest: erros comuns e ajuste.

Como tratar DataIntegrityViolationException Spring Boot com código completo

Uma solução prática precisa cobrir três pontos: a entidade com constraint real no banco, a camada de serviço com regra de negócio legível e um handler global para padronizar a resposta. Por outro lado, um exemplo simples com cadastro de usuário já mostra a diferença.

Entidade

<code>@Entity
@Table(name = "users", uniqueConstraints = {
    @UniqueConstraint(name = "uk_users_email", columnNames = "email")
})
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 120)
    private String name;

    @Column(nullable = false, length = 180)
    private String email;

    protected User() {}

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}</code>

Repository

<code>public interface UserRepository extends JpaRepository<User, Long> {
    boolean existsByEmail(String email);
}</code>

DTO e resposta de erro

<code>public record CreateUserRequest(
    @NotBlank String name,
    @Email @NotBlank String email
) {}

public record ApiError(
    String code,
    String message
) {}</code>

Exceção de domínio opcional

<code>public class BusinessConflictException extends RuntimeException {
    public BusinessConflictException(String message) {
        super(message);
    }
}</code>

Service com validação prévia e proteção final pelo banco

<code>@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public User create(CreateUserRequest request) {
        if (userRepository.existsByEmail(request.email())) {
            throw new BusinessConflictException("E-mail já cadastrado");
        }

        try {
            User user = new User(request.name(), request.email());
            return userRepository.saveAndFlush(user);
        } catch (DataIntegrityViolationException ex) {
            if (isUniqueEmailViolation(ex)) {
                throw new BusinessConflictException("E-mail já cadastrado");
            }
            throw ex;
        }
    }

    private boolean isUniqueEmailViolation(DataIntegrityViolationException ex) {
        Throwable current = ex;
        while (current != null) {
            String message = current.getMessage();
            if (message != null && message.contains("uk_users_email")) {
                return true;
            }
            current = current.getCause();
        }
        return false;
    }
}</code>

Controller

<code>@RestController
@RequestMapping("/users")
public class UserController {

    private final UserService userService;

    public UserController(UserService userService) {
        this.userService = userService;
    }

    @PostMapping
    public ResponseEntity<Map<String, Object>> create(@Valid @RequestBody CreateUserRequest request) {
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED)
                .body(Map.of(
                        "id", user.getId(),
                        "name", user.getName(),
                        "email", user.getEmail()
                ));
    }
}</code>

Handler global

<code>@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(BusinessConflictException.class)
    public ResponseEntity<ApiError> handleBusinessConflict(BusinessConflictException ex) {
        return ResponseEntity.status(HttpStatus.CONFLICT)
                .body(new ApiError("CONFLICT", ex.getMessage()));
    }

    @ExceptionHandler(DataIntegrityViolationException.class)
    public ResponseEntity<ApiError> handleDataIntegrity(DataIntegrityViolationException ex) {
        return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY)
                .body(new ApiError("DATA_INTEGRITY", "Não foi possível concluir a operação por violação de integridade de dados"));
    }
}</code>

Esse desenho evita dois extremos ruins. Por outro lado, o primeiro é deixar tudo para o banco e responder uma mensagem crua ou 500. Ao mesmo tempo, o segundo é acreditar que a verificação no service sozinha basta. Na prática, a verificação prévia melhora a experiência. Ainda assim, o tratamento da exceção no saveAndFlush garante que a aplicação continue correta sob concorrência.

Perceba também o uso de saveAndFlush para forçar o erro a aparecer no ponto certo. Por outro lado, nem sempre você vai querer isso em toda operação, mas para cenários de cadastro com retorno imediato e tratamento local faz bastante sentido. Ao mesmo tempo, se preferir não acoplar a detecção ao nome da constraint, você pode inspecionar SQLState, tipo específico da causa ou criar uma camada utilitária para classificar violações.

Spring boot unique constraint exception no service ou no handler global?

Aqui entra uma decisão técnica que muda bastante a manutenção do projeto. Por outro lado, tratar no service e tratar no @ControllerAdvice resolvem problemas diferentes.

Quando tratar no service

Faz sentido quando a violação representa uma regra de negócio conhecida. Por outro lado, exemplo clássico: “e-mail já cadastrado”, “documento já utilizado”, “SKU já existe no catálogo”. Ao mesmo tempo, nesse caso, capturar a exceção técnica e convertê-la para uma exceção de domínio deixa o código mais legível. Na prática, quem lê o service entende a intenção da operação. Ainda assim, também fica mais fácil retornar mensagens específicas por caso e escrever testes alinhados ao comportamento esperado.

O lado menos bom é o risco de espalhar lógica repetida por vários services. Por outro lado, se cada service fizer sua própria inspeção de constraint com strings e causas profundas, a manutenção degrada rápido.

Quando tratar no handler global

O handler global é excelente para padronizar resposta de erro da API. Por outro lado, se escapar uma violação de integridade não mapeada, você ainda devolve um contrato consistente, sem deixar cair em 500. Ao mesmo tempo, isso é especialmente útil em sistemas maiores, com vários controllers e times diferentes mexendo na base.

O ponto fraco é que o handler global, sozinho, costuma ter menos contexto de negócio. Por outro lado, ele consegue dizer “houve conflito” ou “houve violação de integridade”, mas pode não saber se o conflito é email duplicado, username duplicado ou remoção bloqueada por relacionamento, a menos que você faça parsing mais profundo da causa.

Abordagem prática que costuma funcionar melhor

Na maior parte dos projetos, a combinação é melhor que a escolha exclusiva. Por outro lado, regra de negócio conhecida fica no service, convertida para exceção de domínio. Ao mesmo tempo, o handler global padroniza a saída HTTP e serve de rede de segurança para falhas não mapeadas. Na prática, isso entrega clareza no código e consistência na API.

Se a sua aplicação tem autenticação, perfis e endpoints protegidos, a padronização do erro fica ainda mais importante para não misturar falha de autorização com falha de integridade. Por outro lado, para esse cenário, faz sentido comparar com JWT no Spring Security com Spring Boot: autenticação moderna passo a passo, porque o desenho global de erros precisa ser coerente entre camadas.

Quando usar e quando evitar cada abordagem

Use validação prévia no service quando

Você quer feedback rápido, mensagem amigável e regra explícita. Por outro lado, é muito útil para cadastros com campos únicos e fluxos em que o usuário precisa de resposta clara. Ao mesmo tempo, só não trate isso como substituto da constraint do banco.

Use handler global quando

Você precisa garantir que nenhuma violação previsível estoure como 500. Por outro lado, também é o lugar certo para centralizar formato de erro, código interno, timestamp, path e correlação de logs.

Evite capturar tudo no controller

Controller com try/catch de exceções técnicas vira código repetitivo e embaralha responsabilidade. Por outro lado, o controller deveria orquestrar HTTP, não interpretar detalhe de SQL ou Hibernate.

Evite remover constraints para “resolver” erro 500

Isso mascara o sintoma e enfraquece a integridade do sistema. Por outro lado, dado duplicado ou relação quebrada custa caro depois, principalmente em integrações e relatórios.

Evite confiar só em Bean Validation

Validação de DTO é necessária, mas opera antes da persistência e sem visão total do estado concorrente do banco. Por outro lado, em ambiente real, ela não substitui integridade relacional.

Se você quiser fechar esse ciclo com cobertura automatizada, o próximo passo natural é comparar com Guia completo de testes no Spring Boot: evite falhas em produção. Por outro lado, teste de integração para esse tema vale muito mais do que mock isolado, porque a falha real depende do banco, do flush e da transação.

Erros comuns de produção que atrasam a correção

Um erro frequente é logar só a exceção traduzida do Spring e perder a causa raiz. Por outro lado, quando isso acontece, o time enxerga apenas DataIntegrityViolationException e não sabe se foi unique, foreign key ou not null. Ao mesmo tempo, outro erro é expor a mensagem crua do banco no response. Na prática, isso ajuda pouco o cliente, pode vazar detalhe interno e ainda amarra sua API ao banco atual.

Também é comum o time discutir por horas se o correto é 400, 409 ou 422, enquanto a API segue respondendo 500. Por outro lado, melhor um padrão consistente e documentado do que uma teoria perfeita nunca implementada. Ao mesmo tempo, se o caso é conflito por duplicidade, 409 resolve muito bem na maioria das APIs.

Outro ponto prático: se você usa paginação, busca e atualização em endpoints mais carregados, erros de integridade acabam aparecendo junto de operações de listagem e manutenção administrativa. Por outro lado, aprofundar em paginação e ordenação no spring boot com spring data jpa: como montar apis escaláveis ajuda a pensar contratos mais estáveis quando sua API cresce.

Spring boot unique constraint exception: 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

Como tratar DataIntegrityViolationException no Spring Boot sem retornar 500?

Mapeie a exceção para respostas semânticas. Por outro lado, para duplicidade, 409 Conflict costuma ser a melhor escolha. Ao mesmo tempo, para outros casos de integridade, 422 pode fazer sentido. Na prática, o ideal é capturar exceções conhecidas no service quando houver regra de negócio clara e usar um @RestControllerAdvice para padronizar a saída e impedir que algo previsível caia em 500.

Quando retornar 409 Conflict em vez de 400 ou 422 para constraint no banco?

Use 409 quando existe conflito com o estado atual do recurso ou dos dados persistidos, como e-mail já cadastrado ou exclusão bloqueada por dependência. Por outro lado, 422 é mais útil quando o payload está correto estruturalmente, mas a operação não pode ser processada por uma restrição semântica. Ao mesmo tempo, 400 costuma ser menos preciso nesses casos, embora alguns times adotem por simplicidade.

É melhor tratar DataIntegrityViolationException no service ou no handler global?

Para regra de negócio conhecida, o service costuma ser melhor porque deixa a intenção explícita. Por outro lado, para padronizar respostas e evitar vazamento de erro técnico, o handler global é indispensável. Ao mesmo tempo, em projeto real, a combinação das duas abordagens normalmente entrega o melhor resultado.

Conclusão

Quando aparece spring boot unique constraint exception, o erro quase nunca deveria terminar em 500 genérico. Por outro lado, a exceção sinaliza que a aplicação encontrou uma violação de integridade previsível e tratável. Ao mesmo tempo, o ganho real vem de separar conflito de negócio, falha de validação persistida e erro interno de verdade. Na prática, isso melhora contrato da API, observabilidade e manutenção.

A escolha mais madura costuma ser esta: manter constraints no banco, validar previamente no service para dar feedback melhor, converter violações conhecidas em exceções de domínio e usar um handler global como rede de segurança e padronização. Por outro lado, é uma abordagem mais honesta com quem consome a API e muito menos dolorosa quando o volume e a concorrência aumentam.

Como próximos passos, revise os endpoints que hoje devolvem 500 para conflitos previsíveis, adicione testes de integração cobrindo concorrência e persistência real, e padronize um payload de erro claro para 409 e 422. Por outro lado, leitura complementar: 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 comparar com API REST Spring Boot Java: Guia Completo com Exemplo Prático. 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