Como descobri o Pessimistic Locking e por que isso mudou minha forma de pensar sobre concorrência

Recentemente participei de uma entrevista técnica do tipo whiteboard, e saí dela com algo muito mais valioso do que um possível “sim” ou “não”: aprendizado real, técnico e profundo. Foi a partir de uma sugestão do próprio avaliador que entrei em contato com o conceito de Pessimistic Locking, e desde então mergulhei no tema para entender os riscos que sistemas financeiros enfrentam quando se trata de concorrência de dados. Neste artigo, quero compartilhar esse processo — tanto o desafio apresentado, quanto os conceitos que aprendi depois, e como ter bons avaliadores faz toda a diferença para o crescimento profissional. O desafio apresentado Em um determinado momento da entrevista, o avaliador decidiu simular um caso real de construção de produto. A ideia era propor a criação de um POC - Proof of Concept (Prova de Conceito). O projeto precisava ser simples e direto, pois a equipe estava em fase de validação do produto e procisava de algo funcional para testar o modelo de negócio com usuários o quanto antes. Dentro desse contexto, me foi solicitado que o projetasse alguns endpoints essenciais para o sistema bancário proposto: GET /contas/{id}/extrato Para consultar o extrato e saldo de uma conta POST /transferencias Para permitir que um usuário envie dinheiro da conta A para a conta B. A arquitetura deveria ser mínima, funcional e segura, e deveria atender a três pilares fundamentais: Segurança dos dados e operações Boas práticas REST para API Consistência transacional, mesmo em um sistema ainda em fase inicial Minhas escolhas e justificativas: Usei GET para a leitura do extrato, pois é um verbo seguro e sem efeitos colateriais. Usei POST para a criação da transferência, pois trata-se de uma operação que modifica o estado do sistema. Os caminhos foram estruturados de forma RESTful, facilitando a compreensão e escalabilidade da API. A proposta do avaliador era clara: mesmo sendo uma POC, o ssitema deveria se comportar como algo confiável, pois a ideia era colocar a prova um produto real, ainda que em fase de testes. Até então, tudo parecia bem. Mas foi nesse momento que veio uma das perguntas mais importantes da entrevista, que elevou o nível do desafio: “Como você garantiria que duas transferências simultâneas não fariam com que o mesmo saldo fosse usado duas vezes?” E foi a partir dessa provocação que a entrevista tomou um novo rumo, um que me levou a buscar aprender mais sobre concorrência. O que é Race Condition? Uma race condition acontece quando duas ou mais operações acessam o mesmo recurso ao mesmo tempo e o resultado depende da ordem de execução Por exemplo: A conta de João possui R$ 100.00 Duas transferências de R$ 100.00 são disparadas ao mesmo tempo Ambas leem o saldo, consideram suficiente e processam a transação Nesse caso, João acaba transferindo R$ 200.00 com apenas R$ 100.00 de saldo → O sistema foi enganado por falta de controle de concorrência. Como funcionam os locks no banco de dados? Para evitar situações como a descrita acima, os bancos de dados implementam mecanismos de locks (bloqueio), que controlam o acesso concorrente a dados sensíveis. O que é um lock? Um lock impede que múltiplas transações leiam ou alterem os mesmo dados ao mesmo tempo. Ele pode ser aplicado em diferentes níveis e tipos: Shared lock (Leitura) Permite múltiplas leituras simultâneas Bloqueia a escrita enquanto está ativo. Ideal para operações de leitura que exigem consistência Exclusive lock (Escrita) Exclusivo: impede qualquer leitura ou escrita concorrente Lock de linha vs Lock de tabela Linha: bloqueia apenas o registro necessário. Permite maior concorrência Tabela: bloqueia a tabela inteira. Mais seguro, porém menos eficiente. Locks e transações Locks geralmente fazem parte de transações, que seguem as regras ACID: Atomicidade Consistência Isolamento Durabilidade Dessa forma, os locks acabam garantindo o isolamento da transação, impedindo que operações concorrentes interfiram nos dados enquanto a transação está em andamento. O que é Pessimistic Locking? É uma abordagem onde os dados são bloqueados no momento da leitura, assumindo que há risco real de conflito. SQL Exemplo: BEGIN; SELECT * FROM contas WHERE id = 1 FOR UPDATE; -- realiza a transferência COMMIT; Esse comando bloqueia a linha da conta até que a transação finalize, impedindo que qualquer outra operação use esses dados simultaneamente. Exemplo prático com Kotlin + Spring Boot 1. Entidade import jakarta.persistence.Entity import jakarta.persistence.Id import java.math.BigDecimal @Entity data class Conta( @Id val id: Long, var saldo: BigDecimal ) 2. Repositório com lock pessimista import br.com.pessimistic_locking.entity.Conta import jakarta.persistence.LockModeType import org.springframework.data.jpa.repository.JpaRepository import org.springframework.data.

Apr 21, 2025 - 00:32
 0
Como descobri o Pessimistic Locking e por que isso mudou minha forma de pensar sobre concorrência

Recentemente participei de uma entrevista técnica do tipo whiteboard, e saí dela com algo muito mais valioso do que um possível “sim” ou “não”: aprendizado real, técnico e profundo.

Foi a partir de uma sugestão do próprio avaliador que entrei em contato com o conceito de Pessimistic Locking, e desde então mergulhei no tema para entender os riscos que sistemas financeiros enfrentam quando se trata de concorrência de dados.

Neste artigo, quero compartilhar esse processo — tanto o desafio apresentado, quanto os conceitos que aprendi depois, e como ter bons avaliadores faz toda a diferença para o crescimento profissional.

O desafio apresentado

Em um determinado momento da entrevista, o avaliador decidiu simular um caso real de construção de produto. A ideia era propor a criação de um POC - Proof of Concept (Prova de Conceito).

O projeto precisava ser simples e direto, pois a equipe estava em fase de validação do produto e procisava de algo funcional para testar o modelo de negócio com usuários o quanto antes.

Dentro desse contexto, me foi solicitado que o projetasse alguns endpoints essenciais para o sistema bancário proposto:

GET /contas/{id}/extrato Para consultar o extrato e saldo de uma conta

POST /transferencias Para permitir que um usuário envie dinheiro da conta A para a conta B.

A arquitetura deveria ser mínima, funcional e segura, e deveria atender a três pilares fundamentais:

  • Segurança dos dados e operações
  • Boas práticas REST para API
  • Consistência transacional, mesmo em um sistema ainda em fase inicial

Minhas escolhas e justificativas:

  • Usei GET para a leitura do extrato, pois é um verbo seguro e sem efeitos colateriais.
  • Usei POST para a criação da transferência, pois trata-se de uma operação que modifica o estado do sistema.
  • Os caminhos foram estruturados de forma RESTful, facilitando a compreensão e escalabilidade da API.

A proposta do avaliador era clara: mesmo sendo uma POC, o ssitema deveria se comportar como algo confiável, pois a ideia era colocar a prova um produto real, ainda que em fase de testes.

Até então, tudo parecia bem. Mas foi nesse momento que veio uma das perguntas mais importantes da entrevista, que elevou o nível do desafio:

“Como você garantiria que duas transferências simultâneas não fariam com que o mesmo saldo fosse usado duas vezes?”

E foi a partir dessa provocação que a entrevista tomou um novo rumo, um que me levou a buscar aprender mais sobre concorrência.

O que é Race Condition?

Uma race condition acontece quando duas ou mais operações acessam o mesmo recurso ao mesmo tempo e o resultado depende da ordem de execução

Por exemplo:

  • A conta de João possui R$ 100.00
  • Duas transferências de R$ 100.00 são disparadas ao mesmo tempo
  • Ambas leem o saldo, consideram suficiente e processam a transação

Nesse caso, João acaba transferindo R$ 200.00 com apenas R$ 100.00 de saldo

→ O sistema foi enganado por falta de controle de concorrência.

Como funcionam os locks no banco de dados?

Para evitar situações como a descrita acima, os bancos de dados implementam mecanismos de locks (bloqueio), que controlam o acesso concorrente a dados sensíveis.

O que é um lock?

Um lock impede que múltiplas transações leiam ou alterem os mesmo dados ao mesmo tempo. Ele pode ser aplicado em diferentes níveis e tipos:

  1. Shared lock (Leitura)
  • Permite múltiplas leituras simultâneas
  • Bloqueia a escrita enquanto está ativo.
  • Ideal para operações de leitura que exigem consistência
  1. Exclusive lock (Escrita)
  • Exclusivo: impede qualquer leitura ou escrita concorrente
  1. Lock de linha vs Lock de tabela
  • Linha: bloqueia apenas o registro necessário. Permite maior concorrência

  • Tabela: bloqueia a tabela inteira. Mais seguro, porém menos eficiente.

Locks e transações

Locks geralmente fazem parte de transações, que seguem as regras ACID:

  • Atomicidade
  • Consistência
  • Isolamento
  • Durabilidade

Dessa forma, os locks acabam garantindo o isolamento da transação, impedindo que operações concorrentes interfiram nos dados enquanto a transação está em andamento.

O que é Pessimistic Locking?

É uma abordagem onde os dados são bloqueados no momento da leitura, assumindo que há risco real de conflito.

SQL Exemplo:

BEGIN;
SELECT * FROM contas WHERE id = 1 FOR UPDATE;
-- realiza a transferência
COMMIT;

Esse comando bloqueia a linha da conta até que a transação finalize, impedindo que qualquer outra operação use esses dados simultaneamente.

Exemplo prático com Kotlin + Spring Boot

1. Entidade

import jakarta.persistence.Entity
import jakarta.persistence.Id
import java.math.BigDecimal

@Entity
data class Conta(
    @Id val id: Long,
    var saldo: BigDecimal
)

2. Repositório com lock pessimista

import br.com.pessimistic_locking.entity.Conta
import jakarta.persistence.LockModeType
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Lock
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository

@Repository
interface ContaRepository : JpaRepository<Conta, Long> {
    @Lock(LockModeType.PESSIMISTIC_WRITE)
    @Query("SELECT c FROM Conta c WHERE c.id = :id")
    fun findByIdForUpdate(@Param("id") id: Long): Conta
}

3. Serviço de transferência

import br.com.pessimistic_locking.repository.ContaRepository
import jakarta.transaction.Transactional
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
import java.math.BigDecimal

@Service
class ContaService(
    @Autowired private val contaRepository: ContaRepository
) {

    @Transactional
    fun transferir(origemId: Long, destinoId: Long, valor: BigDecimal) {
        val origem = contaRepository.findByIdForUpdate(origemId)
        val destino = contaRepository.findByIdForUpdate(destinoId)

        if (origem.saldo < valor) throw IllegalArgumentException("Saldo insuficiente")

        origem.saldo -= valor
        destino.saldo += valor

        contaRepository.save(origem)
        contaRepository.save(destino)
    }
}

4. Controller

@Controller
@RequestMapping("/contas")
class ContaController(
    private val service: ContaService
) {
    @GetMapping("/{id}/extrato")
    fun get(@PathVariable id: Long): ResponseEntity<Conta> = ResponseEntity.ok(service.extrato(id))
}
@Controller
@RequestMapping("/transferencias")
class TransferenciasController(
    private val service: ContaService
) {
    @PostMapping
    fun post(@RequestBody body: RequestTransferencia): ResponseEntity<Void> {
        service.transferir(
            origemId = body.origemId,
            destinoId = body.destinoId,
            valor = body.valor
        )
        return ResponseEntity.ok().build()
    }
}

data class RequestTransferencia(
    val origemId: Long,
    val destinoId: Long,
    val valor: BigDecimal

)

5. Carga de contas na base de dados

INSERT INTO public.conta
(id, saldo)
VALUES(1, 100);

INSERT INTO public.conta
(id, saldo)
VALUES(2, 100);

5. Consultado extrato

curl --request GET \
  --url http://localhost:8080/contas/1/extrato \
  --header 'User-Agent: insomnia/11.0.0'

6. Executando transferência

#!/bin/bash

echo "Iniciando teste de concorrência com duas transferências simultâneas..."

# Requisição 1 (em background)
curl --request POST \
  --url http://localhost:8080/transferencias \
  --header 'Content-Type: application/json' \
  --header 'User-Agent: insomnia/11.0.0' \
  --data '{
    "origemId": 1,
    "destinoId": 2,
    "valor": 100
}' &
PID1=$!

# Requisição 2 (em background)
curl --request POST \
  --url http://localhost:8080/transferencias \
  --header 'Content-Type: application/json' \
  --header 'User-Agent: insomnia/11.0.0' \
  --data '{
    "origemId": 1,
    "destinoId": 2,
    "valor": 100
}' &
PID2=$!

# Aguardar as duas finalizarem
wait $PID1
wait $PID2

echo "Teste finalizado."

Limitações do Pessimistic Locking

Apesar de poderoso essa abordagem possui seus trade-offs:

  1. Desempenho impactado
    1. Transações concorrentes esperam o lock ser liberado
    2. Isso pode gerar lenditão em horários de pico
  2. Risco de deadlock
    1. Quando duas transações travam recursos em ordem diferentes, pode haver impasse.
    2. O banco precisa cancelar uma das transações para liberar o sistema.
  3. Acoplamento ao banco
    1. Estratégia dependente de como o banco lida com transações
    2. Pode dificultar migrações para bancos NoSQL ou cloud-native

Alternativas para sistema maiores

Em sistema com alta demanda, é possível utilizar estratégias diferentes:

  • Optimistic Locking - usa versionamento e detecta conflitos antes de salvar
  • Filas com mensageria (Kafka, RabbitMQ) - serializa operações por recurso
  • Event Sourcing e CQRS - separa leitura e escrita, com controle por eventos.

O que essa experiência me ensinou

Eu não conhecia essa abordagem a fundo antes da entrevista. Mas, graças ao avaliador que soube conduzir com clareza, paciência e provocações construtiva, saí mais preparado tecnicamente do que entrei.

Conclusão

Mais importante do que sair de uma entrvista com um “sim” ou "não”, é sair com uma certeza de que você cresceu como profissional.

Essa experiência me ensinou sobre concorrência, sobre engenharia e também sobre humildade: não saber algo é normal - o que define você é o que você faz depois disso.

Repositório do projeto → aqui