CINEMA API ADMIT ONE SEAT A1 · SALA 7 · 19:00 A1 SALA 7 R$ 25,00
A história de um ingresso que não pode ser vendido duas vezes

Cinema API

Um sistema distribuído onde cada assento tem 30 segundos para ser comprado — e nenhum milissegundo de margem para erro.

13
Pull Requests
88
Testes
6
Containers
30s
TTL do Lock
Rolar para baixo
01
O Problema
Dois clientes, um assento

Imagine um cinema com 500 pessoas olhando o mesmo assento A1 da pré-estreia de Interestelar. Às 19:00:00.000, dois dedos clicam "Reservar" exatamente ao mesmo tempo.

Sem controle, os dois recebem "Reservado com sucesso". Dois ingressos, um assento. Um deles chega no cinema e descobre que o lugar é de outra pessoa.

Esse é o problema de race condition — e é o problema que esse sistema resolve.

A Corrida pelo Assento A1
Usuário A
👤
SET NX → OK
✓ 201 RESERVADO
Usuário B
👤
SET NX → FAIL
✗ 409 BLOQUEADO
Redis SET NX — operação atômica. Não existe "verificar e depois setar". É tudo ou nada no mesmo milissegundo.
02
Os Personagens
Cada um faz uma coisa

O sistema foi dividido em personagens com responsabilidades únicas. Nenhum deles sabe como o outro funciona por dentro — só conhecem o contrato (a interface). Se um trocar de implementação, os outros nem percebem.

📋
Session
A Raiz
Cria a sessão de cinema e gera automaticamente todos os assentos. Sem ela, nada existe. É o ponto de partida de tudo.
💺
Seat
O Recurso Disputado
O assento que todo mundo quer. Cruza Postgres + Redis para saber em tempo real se está livre, reservado ou vendido.
🎟️
Reservation
O Guardião dos 30s
Segura o assento por 30 segundos com lock no Redis. Se o pagamento não chegar a tempo, libera automaticamente.
💳
Payment
O Orquestrador
Valida 4 condições, executa uma transaction atômica com 3 escritas, libera o lock e publica o evento. Tudo ou nada.
💰
Sale
O Registro Final
Só leitura. Consulta o histórico de compras com dados completos — filme, sala, assento, preço, horário. Uma query, tudo incluído.
📨
Events
Os Mensageiros
Publishers enfileiram, Consumers processam. A expiração automática acontece aqui — sem cron job, sem polling.
03
A Jornada
Do clique à venda

Cada ingresso percorre um caminho preciso — da criação da sessão até o registro permanente da venda. Qualquer falha no meio do caminho resulta em rollback automático.

📋
Session
POST /sessions
gera assentos
💺
Seat
GET /seats/:id
status tempo real
🔒
Lock
Redis SET NX
TTL 30s
🎟️
Reserva
POST /reservations
status PENDING
💳
Pagamento
POST /payments
$transaction
💰
Venda
GET /sales
registro final
04
O Relógio
30 segundos para decidir

Quando alguém reserva um assento, o relógio começa a contar. O Redis segura o lock, o Consumer assiste. Se o pagamento não chegar em 30 segundos, tudo é desfeito automaticamente — o assento volta ao mercado como se nada tivesse acontecido.

30
segundos

Expiração Automática

O Consumer recebe o evento reservation.created, calcula quanto falta pro expiresAt, espera o tempo exato, e verifica se ainda está PENDING. Se sim: expira tudo. Se o pagamento já chegou: ignora. Sem cron job, sem polling — o consumer já sabe quem vai expirar e quando.

🎟️
T + 0s
Reserva criada
Lock adquirido no Redis · Reservation PENDING · evento publicado no RabbitMQ
⏱️
T + 15s
Metade do tempo
Consumer está esperando. O assento está isLocked: true no GET /seats. Ninguém mais consegue reservar.
💳
T + 22s ✓
Cenário A: Pagamento chegou
Transaction atômica executada → Seat SOLD → lock liberado → Sale criada. Consumer ignora quando chegar no T+30s (já CONFIRMED).
💀
T + 30s ✗
Cenário B: Tempo esgotou
Consumer age: Reservation → EXPIRED · Seat → AVAILABLE · publica reservation.expired. Assento livre de novo.
4
Models
8
Endpoints
6
Containers
3
Filas
4
Eventos
13
PRs
05
A Infraestrutura
Cada ferramenta com seu papel

Nenhuma ferramenta foi escolhida por moda. Cada uma resolve um problema específico que as outras não conseguem resolver sozinhas.

🐘
PostgreSQL
Fonte de verdade
Redis
Lock atômico 30s
🐇
RabbitMQ
Eventos async
🔷
Prisma
ORM type-safe
🏗️
NestJS
Framework DI
🐳
Docker
6 containers
🧪
Jest
88 testes
📘
Swagger
Docs interativas
06
A Prova
88 testes. Zero falhas.

Três camadas de teste que se complementam: unitários isolam a lógica sem I/O, contracts validam os status codes via HTTP real, e flows testam fluxos completos incluindo race condition e expiração automática. Tudo roda em 3.7 segundos — sem Docker, sem banco, sem Redis.

🏆
88
testes passando
53
Unit tests
20
Contract tests
15
Flow tests
// O teste mais importante: race condition
const [result1, result2] = await Promise.allSettled([
  reservationService.create({ seatId: 'seat-001', userId: 'usuario-001' }),
  reservationService.create({ seatId: 'seat-001', userId: 'usuario-002' }),
]);

expect(result1.status).toBe('fulfilled');  // ✓ primeiro conseguiu
expect(result2.status).toBe('rejected');  // ✗ segundo bloqueado
07
A Construção
13 Pull Requests, uma história

Cada PR foi construída em cima da anterior, numa ordem pensada para que o módulo anterior validasse o funcionamento antes do próximo ser construído. Nenhuma dependência circular, nenhum retrabalho.

#1
Init
#2
Arquitetura
#3
Infra
#4
Session
#5
Seat
#6
Reservation
#7
Payment
#8
Sale
#9
Events
#10
Logger
#11
Portainer
#12
Tests
#13
Lint