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.

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.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:
- Desempenho impactado
- Transações concorrentes esperam o lock ser liberado
- Isso pode gerar lenditão em horários de pico
- Risco de deadlock
- Quando duas transações travam recursos em ordem diferentes, pode haver impasse.
- O banco precisa cancelar uma das transações para liberar o sistema.
- Acoplamento ao banco
- Estratégia dependente de como o banco lida com transações
- 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