Coders Club · Aula técnica
Do Backend ao Data Warehouse: construindo sistemas orientados a eventos e dados
What Happens After You Click Buy?
Alex Seixas · Senior Software Engineer · CTO
O que acontece tecnicamente depois que você clica em Comprar?
POST /orders
{
"productId": "prod_123",
"quantity": 1
}
Um clique. Um POST. Um banco.
const order = await createOrder(input); return { id: order.id, status: "PENDING" };
O cliente espera só o caminho crítico do request.
ACID é o contrato do banco — não de todo o sistema distribuído.
BEGIN; INSERT INTO orders (...); INSERT INTO order_items (...); COMMIT;
Cobrar. Reservar. Checar. Avisar. Medir.
Novas responsabilidades. Mesmo request. Novo risco.
await createOrder(); await chargePayment(); await reserveInventory(); await runFraudCheck(); await sendEmail(); await trackAnalytics();
TOTAL > 1s · o usuário espera tudo
Cada await no caminho crítico é tempo na cara do usuário.
Se não queremos esperar tudo, o que muda no modelo do request?
Sync
Async
| Sync | Async | |
|---|---|---|
| Latency | soma tudo | só o caminho crítico |
| Failure isolation | fraca | melhor |
| Complexity | baixa | mais estados, retries, observabilidade |
| Consistency | imediata no request | eventual entre serviços |
Async não apaga o trabalho. Muda quando ele acontece.
Sem transação global, o pedido vira uma máquina de estados.
Everything was fine.
Until Marketing arrived.
“A gente precisa de um dashboard.”
SELECT DATE(created_at), product_id, SUM(total) FROM orders WHERE created_at >= NOW() - INTERVAL '1 year' GROUP BY 1, 2;
500M rows scanned.
O checkout acabou de competir com o dashboard.
OLTP
Operação
transações curtas · baixa latência · INSERT/UPDATE · concorrência
OLAP
Analytics
scans grandes · agregações · columnar · throughput de leitura
Mesmos dados. Workloads completamente diferentes.
ETL
Transforma antes de entrar no destino.
ELT
Carrega cru. Transforma no warehouse — dbt, SQL, escala do destino.
A ordem de Transform e Load muda quem paga a complexidade.
Batch
Processar 1× por hora
Simples. Barato. Atraso aceitável para muitos dashboards.
Streaming
À medida que acontece
Menor atraso. Mais peças móveis. Operação mais cara.
Real-time sobe complexity, cost e operational burden.
Se o Order Service não deve conhecer cada sistema, como distribuímos a informação?
Producer doesn't need to know every consumer.
{
"eventId": "evt_01J...",
"type": "OrderCreated",
"version": 1,
"occurredAt": "2026-09-01T18:00:00Z",
"data": {
"orderId": "ord_987",
"customerId": "usr_42",
"total": 249.90
}
}
Um evento é um fato versionado — não um dump da tabela.
Kafka não é uma fila tradicional. É um log distribuído.
Ordem existe — mas o escopo importa.
partition key = orderId
Um grupo escala partições. Grupos diferentes não roubam mensagens uns dos outros.
Mesmo tópico. Offsets independentes por consumer group.
Kafka
Event streaming · Replay · Log
Multiple consumer groups · High throughput
Vários times leem o mesmo histórico. Analytics entra depois e ainda alcança o passado.
SQS
Queue · Worker processing
Jobs assíncronos · Retry / DLQ · AWS managed
Uma mensagem, um worker. Simples para “faça este trabalho”. Sem replay nativo de log.
There is no universal winner.
SQS não é “Kafka simples”. São modelos diferentes.
At-most-once
Pode perder.
Nunca duplica.
AT-LEAST-ONCE
Não perde.
Pode duplicar.
Exactly-once
Caro. Condicional.
Não é mágica.
At-least-once + idempotency. Esse é o combo prático.
Retries resolvem falhas transitórias. E criam um problema novo.
Idempotency-Key: order_987 // gateway de pagamento // mesma chave = mesma cobrança
processed_events event_id PK processed_at // se event_id existe → skip
Chave de idempotência no provedor. Ou tabela local de eventos processados. Ou os dois.
Retrying is easy. Retrying safely is hard.
Backoff protege o destino. Não é só cortesia.
1s → 2s → 4s → 8s
Poison message: nunca vai passar. Retry infinito é um loop de CPU e custo.
Conseguimos entregar eventos. Como garantir que o evento exista sempre que o pedido existir?
await db.orders.insert(order); await kafka.publish({ type: "OrderCreated", orderId: order.id });
DB COMMIT ✓
KAFKA PUBLISH ✕
Two writes. Two systems. No shared transaction.
BEGIN; INSERT orders INSERT outbox_events COMMIT;
Persist the change and the intent to publish together.
| id | aggregate_id | event_type | payload | created_at | published_at |
|---|---|---|---|---|---|
| 1 | ord_987 | OrderCreated | {…} | 18:00:01 | 18:00:02 |
| 2 | ord_988 | OrderCreated | {…} | 18:00:04 | NULL |
Publisher lê unpublished, envia, marca published_at. Crash? Lê de novo.
Observe changes instead of repeatedly asking for them.
Agora temos um histórico de eventos. Como viram informação para o negócio?
Now we have events.
Data Engineering wants them.
Everything is an event.
Raw events
Como chegou. Replay possível.
Clean / normalized
Tipos, chaves, dedup.
Business metrics
Pronto para o dashboard.
Common pattern, not a universal rule.
Operational models answer transactions. Analytical models answer questions.
Lake não substitui warehouse — são camadas com jobs diferentes.
Fact = métrica. Dimension = contexto.
SELECT order_id, customer_id, total FROM {{ ref('stg_orders') }} WHERE status = 'PAID'
Transformações versionadas. Lineage legível.
order_id
customer_id
total
Data quality starts at the source.
// ANTES { "status": "paid" }
// DEPOIS { "status": "completed" }
Your event is an API.
Compatibilidade é disciplina — não acidente.
A arquitetura funciona. Quando algo quebra, como descobrimos onde?
Um pedido desapareceu. Onde ele está?
If an order disappears, can we trace its journey?
Order #987
PAYMENT_PROCESSING
alguns momentos depois
PAID
Atraso de convergência ≠ bug automático.
Clique um card · R revela o próximo
O que acontece no checkout?
Outbox guarda o evento. Publisher tenta de novo. Compra pode completar. Consumidores atrasam — não deveriam falhar o POST.
O pedido some?
Pedido fica PAYMENT_PROCESSING. Retry + timeout → PAYMENT_FAILED. Não apagar o pedido.
Cobra duas vezes?
Não, se idempotência existir. processed_events ou Idempotency-Key no gateway.
Paid antes de Created?
Partition por orderId reduz isso. State machine ignora transições inválidas. Não assumir ordem global.
As mensagens morrem?
Kafka retém. Group retoma do offset. Lag cresce. Alerta de consumer lag.
O checkout para?
Não. Caminho operacional isolado. Pipeline acumula. Batch/stream alcança depois.
CI verde resolve?
Consumidores quebram ou vão pra DLQ. Contract tests e registry deveriam ter barrado o deploy.
Pode religar analytics?
Com log (Kafka) e consumidores idempotentes, sim. Com fila que apaga mensagem, o passado já foi.
Cada caixa existe por um problema que já vimos.
Marketplace · 5M usuários · 500k pedidos/dia
Checkout · Payment · Inventory · Fraud · Notifications · Near-real-time analytics
Trade-offs: Kafka pede operação. CDC pede WAL expertise. Near-real-time analytics custa mais que batch de 5 minutos — 500k/dia talvez não precise de stream em tudo.
Clique ou R — uma de cada vez.
How would you design an order processing system?
What is idempotency?
How would you solve the dual-write problem?
Kafka vs SQS?
Same event processed twice?
OLTP vs OLAP?
Operational data → analytics?
How do you evolve event schemas?
One click.
Dozens of architectural decisions.
Software generates data.
Data tells us what the software did.
Architecture connects both.
Perguntas?