🚀NestJS 🟩Node.js 📘TypeScript 🐘PostgreSQL Redis 🐇RabbitMQ 🐳Docker Prisma 📐Swagger 🧪Jest 🔧Portainer

Arquitetura de Venda de Ingressos

4 models · 8 endpoints · 5 containers Docker · 4 eventos · RabbitMQ · Redis · 88 testes.

01 — docker-compose.yml · docker-compose up --build
🐳  cinema_network · containers se comunicam pelo nome do serviço
$docker-compose up --build ← sobe os 5 serviços · migrations rodam automaticamente no boot · api aguarda postgres via healthcheck
🚀
cinema-api
container: cinema-api
build: ./Dockerfile · start:dev
3000:3000
App NestJS principal. Migrations automáticas no boot. Depende de postgres (healthy), redis e rabbitmq.
cinema_network
🐘
cinema-postgres
container: cinema-postgres
image: postgres:15
5432:5432
Banco relacional. Healthcheck com pg_isready. Armazena as 4 models. Logging desabilitado.
cinema_network
cinema-redis
container: cinema-redis
image: redis:7
6379:6379
SET seat:{id} NX PX 30000 — lock atômico por assento. TTL de 30s expira automaticamente.
cinema_network
🐇
cinema-rabbitmq
container: cinema-rabbitmq
image: rabbitmq:3-management
5672:567215672:UI
3 filas + DLQ por fila. UI em :15672. Mensagens persistidas (durable). prefetch(1) por consumer.
cinema_network
🔧
portainer
container: cinema-portainer
image: portainer/portainer-ce
9000:9000
Gerenciamento visual Docker. Logs, métricas e terminal dos containers via browser.
cinema_network
02 — Controle de Concorrência · race condition · deadlock · expiração
🔒Race ConditionRedis SET NX
1
2 usuários clicam no mesmo assento no mesmo milissegundo
2
SET seat:{id} NX PX 30000 — operação atômica no Redis
3
Usuário A adquire o lock → reserva criada → 201 Created
4
Usuário B recebe false409 Conflict imediato
🔐DeadlockPrevenido
1
Sistema reserva um assento por vez — nunca múltiplos recursos simultâneos
2
Cada reserva: 1 lock Redis + 1 linha Postgres — sem espera circular
3
A1+A3 → 2 requisições independentes — operação multi-lock não existe
4
Transaction Prisma garante atomicidade no pagamento — tudo ou nada
⏱️Expiração AutomáticaTTL 30s
1
Reserva criada → expiresAt = now+30s + lock Redis TTL 30s
2
ReservationConsumer aguarda com sleep(delay) — sem cron job
3
Se ainda PENDING → expire + assento volta a AVAILABLE
4
Se já CONFIRMED → idempotência — ignora sem reprocessar
03 — Models · 4 tabelas no PostgreSQL
📋Sessionraiz
idStringUUID auto-geradoPK
movieStringnome do filme
roomStringidentificador da sala
startsAtDateTimedata e hora da sessão
ticketPriceDecimalpreço do ingresso
createdAtDateTimeauto now
seatsSeat[]relação 1:N com Seat
💺Seatrecurso disputado
idStringUUID auto-geradoPK
sessionIdStringreferência à SessionFK
seatNumberStringex: A1, B3, C5
statusEnumAVAILABLE · RESERVED · SOLD
updatedAtDateTimeauto atualizado
sessionSessionrelação N:1 com Session
🎟️Reservationtemporária TTL 30s
idStringUUID auto-geradoPK
seatIdStringassento reservadoFK
userIdStringid externo do usuário
statusEnumPENDING · CONFIRMED · EXPIRED
expiresAtDateTimenow + 30 segundos
createdAtDateTimeauto now
saleSale?relação 1:1 opcional
💰Salevenda definitiva
idStringUUID auto-geradoPK
reservationIdStringreserva de origemFK
seatIdStringassento vendidoFK
userIdStringcomprador — usado no histórico
paidAtDateTimemomento da confirmação
reservationReservationrelação 1:1
04 — Endpoints · 8 rotas REST + Swagger em /api/docs
📋Sessions3 rotas
POST
/sessions
Cria sessão + gera assentos (mín. 16). Body: movie, room, startsAt, ticketPrice, totalSeats.
201 Created
GET
/sessions
Lista todas as sessões disponíveis.
200 OK
GET
/sessions/:id
Detalhe de uma sessão com seus assentos.
200 OK404
💺 Seats 1 rota
GET
/seats/:sessionId
Disponibilidade em tempo real. Cruza Postgres + Redis para status preciso: AVAILABLE, RESERVED ou SOLD + isLocked.
200 OKtempo real
🎟️ Reservations 3 rotas
POST
/reservations
Adquire lock Redis → cria Reservation (PENDING). Body: seatId, userId.
201 Created409 Conflict
GET
/reservations/:id
Status da reserva: PENDING, CONFIRMED ou EXPIRED.
200 OK404
GET
/reservations/user/:userId
Lista reservas de um usuário.
200 OK
💳 Payments 1 rota
POST
/payments/confirm/:reservationId
Confirma pagamento dentro da janela de 30s. Resposta: id, reservationId, seatId, userId, paidAt.
201 Created404409410 Gone422
💰 Sales 1 rota
GET
/sales/history/:userId
Consulta histórico de compras confirmadas. Retorna movie, room, seatNumber, startsAt, ticketPrice, paidAt por venda. Só leitura — Sale é criada pelo Payment.
200 OK
📘 Swagger docs
GET
/api/docs
Documentação interativa OpenAPI com exemplos de body, responses e schemas.
@nestjs/swagger
05 — Fluxo interno · NestJS (cinema-api)
🎬Clientes / Frontend / APIEXTERNO
GET /sessionsGET /seats/:sessionId POST /reservationsPOST /payments/confirm/:id GET /sales/history/:userId
Qualquer cliente REST em localhost:3000. Múltiplos usuários simultâneos tentando o mesmo assento — o sistema garante que apenas 1 venda ocorra.
HTTP · localhost:3000
📋sessions/CRUD
ControllerServiceRepositoryDTOsInterfaces
Cria sessões e gera os assentos vinculados (mín. 16, 8 por fileira: A1-A8, B1-B8...). Controller → Service → Repository → postgres:5432.
disponibilidade em tempo real
💺seats/CONSULTA
ControllerServiceRepositoryDTOs
Status por assento: AVAILABLE / RESERVED / SOLD + isLocked. Cruza Postgres + Redis com Promise.all paralelo.
🎟️reservations/TTL 30s
ControllerServiceRepositoryDTOs
1) SET NX no Redis → 409 se bloqueado. 2) Cria Reservation PENDING com expiresAt +30s. 3) Publica reservation.created.
💳payments/CONFIRM
ControllerServiceDTOs
Recebe :reservationId. Valida expiração → confirma → retorna Sale (id, reservationId, seatId, userId, paidAt).
📨events/ASYNC
publishersconsumers
Publishers enfileiram após cada ação. Consumers processam em background — expiração libera assento sem cron job.
persiste · lê · adquire lock
redis:6379
SET seat:{id} NX PX 30000
Atômico — só 1 processo adquire o lock. Expira sozinho em 30s. Fallback: @@unique no Postgres.
🐘
postgres:5432
Tabelas: sessions · seats · reservations · sales.
Fonte de verdade definitiva. Constraints de unicidade como fallback.
🐇
rabbitmq:5672
Filas: reservations · payments · expirations.
DLQ + retry backoff exponencial. prefetch(1). UI em :15672.
eventos assíncronos · rabbitmq:5672
📤PublishersPUBLICA
reservation.created payment.confirmed reservation.expired
📥ConsumersPROCESSA
→ espera TTL, expira se PENDING → libera assento AVAILABLE → idempotente: ignora CONFIRMED → DLQ + retry se falhar
06 — Testes · 88 passando · 11 suites · 0 falhas · ~3.7s
88
testes passando
11
suites verdes
0
falhas
3.7s
sem Docker
🧪Unit Tests53 testes
session.service8 testes · 100%
seat.service6 testes · 100%
reservation.service12 testes · 100%
reservation.consumer8 testes · 94%
payment.service13 testes · 100%
sale.service6 testes · 100%
📋Contract Tests20 testes
session.contract7 testes · HTTP
reservation.contract7 testes · HTTP
payment.contract6 testes · HTTP
Shape do JSON · status codes · 201 · 404 · 409 · 410 · 422
🔀Flow Tests15 testes
reserva → pagamento7 testes
expiração automática8 testes
Race condition · Promise.allSettled · idempotência · handler capturado
07 — Logger · Temas visuais por contexto · Interceptor + Filter globais
Output no terminal
09:21:58 ✓ INFO 🐇 [RabbitMQService] Fila declarada: reservations
09:21:59 ✓ INFO 🎬 [SessionService] Sessão criada { id: "db11ea..." }
09:22:01 ✓ INFO 🌐 [HTTP] POST /sessions → 201 · 89ms
09:22:05 ⚠ WARN 🌐 [HttpExceptionFilter] POST /reservations → 409
09:22:30 ✓ INFO 🗒️ 📻 [ReservationConsumer] Reserva expirada automaticamente
Identidade visual por módulo
🟢 Redis 🐇 RabbitMQ 🗄️ Prisma 🎬 Session 🪑 Seat 🗒️ Reservation 💳 Payment 💰 Sale 🌐 HTTP
✓ INFO → fluxo normal  ·  ⚠ WARN → erro do cliente (4xx)  ·  ✖ ERROR → erro do servidor (5xx)
LoggingInterceptor → captura 2xx automaticamente (arrays → total, objetos → id)
HttpExceptionFilter → captura erros automaticamente (4xx → warn, 5xx → error)
Registrados via APP_INTERCEPTOR / APP_FILTER no app.module.ts
cinema_network
HTTP · :3000
Redis · Lock TTL :6379
Payments · Sale
RabbitMQ :5672 · UI :15672
PostgreSQL :5432
Seats · Disponibilidade
Portainer · :9000