Serviço de detecção de fraudes construído sobre o padrão CQRS (Command Query Responsibility Segregation), demonstrando como separar operações de escrita e leitura para escalar cada lado independentemente.
Em sistemas tradicionais (arquitetura CRUD padrão), um único modelo de dados serve tanto para escrever quanto para ler. Isso funciona bem em sistemas simples, mas cria gargalos conforme o sistema cresce:
┌──────────────────────────────────────┐
│ CRUD Tradicional │
│ │
│ Cliente → [Único modelo] → DB │
│ │
│ Problema: leitura e escrita │
│ competem pelos mesmos recursos │
└──────────────────────────────────────┘
Cenários concretos onde isso vira problema:
| Situação | Consequência |
|---|---|
| Regras de negócio complexas na escrita | Queries de leitura ficam lentas esperando locks |
| Relatórios pesados na leitura | Travamentos nas operações de escrita |
| Escala horizontal | Não é possível escalar só a leitura sem escalar a escrita |
| Modelos conflitantes | Um schema que serve para tudo não serve bem para nada |
Separar completamente o fluxo de Commands (escrita, mutação de estado) do fluxo de Queries (leitura, projeções de dados):
┌──────────────────────────────────────────────────────────┐
│ CQRS │
│ │
│ ┌─────────────┐ Evento ┌──────────────────────┐ │
│ │ Command │ ─────────────▶│ Projector │ │
│ │ Side │ │ (atualiza read │ │
│ │ PostgreSQL │ │ model no Redis) │ │
│ └─────────────┘ └──────────────────────┘ │
│ ▲ │ │
│ │ ▼ │
│ Comando ┌─────────────┐ │
│ │ │ Query Side │ │
│ │ │ Redis │ │
│ Cliente ──────────────────────▶ └─────────────┘ │
│ Query │
└──────────────────────────────────────────────────────────┘
Benefícios diretos:
- Escalabilidade independente — o lado de leitura (Redis) pode ter 50 réplicas sem afetar a escrita
- Modelos otimizados — o write model garante consistência; o read model é otimizado para cada tela/consulta
- Performance — leituras nunca concorrem com escritas por locks de banco
- Auditoria natural — cada comando gera um evento, criando um log imutável de tudo que aconteceu
- Evolução independente — você pode trocar o banco de leitura (de Redis para Elasticsearch) sem tocar no write model
O antifraude é um caso de uso ideal para CQRS porque:
- Escrita é crítica e complexa — analisar uma transação envolve múltiplas regras, precisa ser consistente e auditável
- Leitura é intensiva — dashboards, alertas e consultas de risco precisam de latência sub-milissegundo
- Volume assimétrico — para cada 1 escrita (transação), podem existir 10 leituras (consultas de risco, dashboards, etc.)
- Histórico importa — todo evento de fraude precisa ser rastreável
┌────────────────────────────────────────────────────────────────────┐
│ HTTP API (:8080) │
│ chi router + middleware (logger, recover) │
└───────────┬────────────────────────────────────┬───────────────────┘
│ Commands │ Queries
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Command Bus │ │ Query Bus │
│ AnalyzeTransaction │ │ GetTransactionRisk │
│ BlockAccount │ │ GetAccountStatus │
│ FlagTransaction │ │ GetFraudAlerts │
└────────┬────────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌─────────────────────┐ ┌──────────────────────┐
│ Fraud Rules Engine│ │ Redis (Read Model) │
│ • Amount Rule │ │ TransactionRiskView │
│ • Velocity Rule │ │ AccountStatusView │
│ • Location Rule │ │ FraudAlertList │
│ • Blacklist Rule │ └──────────────────────┘
└────────┬────────────┘ ▲
│ │
▼ │
┌─────────────────────┐ Event ┌───────────┴──────────┐
│ PostgreSQL │ ──────────▶ │ Projectors │
│ (Write Model) │ (in-memory │ TransactionProjector│
│ transactions │ EventBus) │ AccountProjector │
│ accounts │ └──────────────────────┘
└─────────────────────┘
| Camada | Tecnologia |
|---|---|
| Linguagem | Go 1.25 |
| HTTP Router | chi v5 |
| Write Database | PostgreSQL 16 via pgx v5 |
| Read Database | Redis 7 via go-redis v9 |
| Event Bus | In-memory (substituível por Kafka/RabbitMQ) |
| Logging | Zap |
| Testes | testify |
| Load Tests | k6 |
| Lint | golangci-lint |
cqrs-exemple-golang/
├── cmd/api/main.go # Entrypoint — wiring de toda a aplicação
├── internal/
│ ├── config/ # Configuração via variáveis de ambiente
│ ├── domain/
│ │ ├── entity/ # Entidades do domínio (Transaction, Account)
│ │ ├── event/ # Eventos de domínio (TransactionAnalyzed, etc.)
│ │ └── repository/ # Interfaces dos repositórios de escrita
│ ├── command/
│ │ ├── model/ # DTOs de comando (AnalyzeTransaction, etc.)
│ │ └── handler/ # Handlers de comando (lógica de escrita)
│ ├── query/
│ │ ├── model/ # Views de leitura (TransactionRiskView, etc.)
│ │ ├── repository/ # Interfaces dos repositórios de leitura
│ │ └── handler/ # Handlers de query (lógica de leitura)
│ ├── application/
│ │ ├── bus/ # CommandBus e QueryBus
│ │ └── eventbus/ # Interface do EventBus
│ ├── fraud/rules/ # Engine de detecção de fraude e regras
│ └── infrastructure/
│ ├── http/ # Router, handlers HTTP, middleware, response
│ ├── messaging/inmemory/ # Implementação in-memory do EventBus
│ ├── persistence/postgres/ # Repositórios de escrita (pgx)
│ ├── persistence/redis/ # Repositórios de leitura (go-redis)
│ └── projector/ # Atualizam o read model a partir de eventos
├── tests/
│ ├── mocks/ # Mocks testify para testes unitários
│ ├── integration/ # Testes com Postgres + Redis reais
│ └── load/transaction_load_test.js # Load test com k6
├── scripts/migrate.sql # Schema do banco (write side)
├── docker/api/Dockerfile # Imagem multi-stage (final: scratch)
├── docker-compose.yml # API + Postgres + Redis
├── docker-compose.override.yml # Overrides para desenvolvimento local
├── .github/
│ ├── workflows/ci.yml # Pipeline CI (lint → test → integration → build)
│ └── PULL_REQUEST_TEMPLATE.md
├── .golangci.yml # Configuração do golangci-lint (20+ linters)
├── .env.example # Exemplo de variáveis de ambiente
└── Makefile # Comandos de build, test, lint, docker
- Go 1.25+
- Docker + Docker Compose
- golangci-lint (para lint local)
- k6 (para load tests)
Copie o arquivo de exemplo e ajuste conforme necessário:
cp .env.example .env| Variável | Padrão | Descrição |
|---|---|---|
HTTP_PORT |
8080 |
Porta do servidor HTTP |
LOG_LEVEL |
info |
Nível de log (debug, info, warn, error) |
POSTGRES_DSN |
postgres://postgres:postgres@localhost:5432/antifraude?sslmode=disable |
Connection string do PostgreSQL (write side) |
REDIS_ADDR |
localhost:6379 |
Endereço do Redis (read side) |
REDIS_PASSWORD |
(vazio) | Senha do Redis |
REDIS_DB |
0 |
Número do banco Redis |
FRAUD_AMOUNT_THRESHOLD |
10000 |
Valor em reais acima do qual a regra de valor dispara |
FRAUD_MAX_TX_PER_HOUR |
10 |
Máximo de transações por hora antes da regra de velocidade disparar |
# Sobe toda a stack (API + Postgres + Redis)
docker compose up -d --build
# Ver logs da API
docker compose logs -f api
# Derrubar tudo
docker compose down# 1. Subir somente a infra
docker compose up -d postgres redis
# 2. Aplicar migrations
make migrate
# 3. Rodar a API
make runmake build # Compila o binário em bin/api
make run # Roda a API diretamente com go run
make test # Roda os testes unitários
make cover # Verifica cobertura >= 70% (excluindo persistence)
make cover-html # Gera coverage.html navegável
make lint # Executa golangci-lint
make lint-fix # Aplica correções automáticas de lint
make integration-test # Roda testes de integração (exige POSTGRES_DSN e REDIS_ADDR)
make load-test # Roda load test com k6
make migrate # Aplica o schema SQL no PostgreSQL
make docker-up # Sobe toda a stack com Docker Compose
make docker-down # Derruba a stack
make docker-clean # Derruba a stack e remove volumes
make tidy # go mod tidyA API base é http://localhost:8080.
Verifica se o serviço está no ar.
Response 200 OK:
{
"status": "ok",
"time": "2026-03-01T12:00:00Z"
}Verifica se o serviço está pronto para receber tráfego.
Response 200 OK:
{
"status": "ready",
"time": "2026-03-01T12:00:00Z"
}Submete uma transação para análise de fraude. Executa todas as regras do engine, persiste no PostgreSQL e atualiza o read model no Redis via evento.
Request body:
{
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 1500.00,
"currency": "BRL",
"merchant_id": "merchant-abc-123",
"location": "BR",
"metadata": {
"channel": "mobile",
"device_id": "dev-xyz"
}
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
account_id |
UUID | Sim | ID da conta que originou a transação |
amount |
float64 | Sim | Valor da transação (deve ser > 0) |
currency |
string | Sim | Código ISO 4217 de 3 letras (BRL, USD, EUR) |
merchant_id |
string | Sim | Identificador do estabelecimento |
location |
string | Sim | Código de país ou região |
metadata |
object | Não | Dados extras em chave-valor |
Response 201 Created — baixo risco:
{
"transaction_id": "7f3b9a1c-4e2d-4f8a-b1c2-d3e4f5a6b7c8",
"status": "approved",
"risk_score": 0.1,
"risk_level": "low"
}Response 201 Created — risco médio:
{
"transaction_id": "7f3b9a1c-4e2d-4f8a-b1c2-d3e4f5a6b7c8",
"status": "flagged",
"risk_score": 0.65,
"risk_level": "medium"
}Response 201 Created — alto risco (bloqueada):
{
"transaction_id": "7f3b9a1c-4e2d-4f8a-b1c2-d3e4f5a6b7c8",
"status": "declined",
"risk_score": 0.90,
"risk_level": "high"
}Tabela de risco:
risk_score |
risk_level |
status |
|---|---|---|
| 0.00 – 0.49 | low |
approved |
| 0.50 – 0.79 | medium |
flagged |
| 0.80 – 1.00 | high |
declined |
Response 400 Bad Request:
{
"error": "Bad Request",
"message": "amount must be greater than zero"
}Consulta o resultado da análise de risco de uma transação. Lê diretamente do Redis (read model).
Response 200 OK:
{
"id": "7f3b9a1c-4e2d-4f8a-b1c2-d3e4f5a6b7c8",
"account_id": "550e8400-e29b-41d4-a716-446655440000",
"amount": 1500.00,
"currency": "BRL",
"merchant_id": "merchant-abc-123",
"location": "BR",
"status": "approved",
"risk_score": 0.10,
"risk_level": "low",
"fraud_reasons": [],
"created_at": "2026-03-01T12:00:00Z",
"updated_at": "2026-03-01T12:00:00Z"
}Quando a transação tem razões de fraude detectadas:
{
"id": "9a1c7f3b-...",
"status": "declined",
"risk_score": 0.90,
"risk_level": "high",
"fraud_reasons": [
"transaction amount 99999.99 exceeds threshold 10000.00",
"transaction from suspicious location: XX"
]
}Response 404 Not Found:
{
"error": "Not Found",
"message": "transaction not found"
}Sinaliza manualmente uma transação como suspeita (ação de analista).
Request body:
{
"reason": "padrão de compra atípico identificado pelo analista",
"flagged_by": "analyst-001"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
reason |
string | Sim | Motivo do flagging manual |
flagged_by |
string | Sim | Identificador do analista ou sistema |
Response 204 No Content — sem body.
Response 404 Not Found:
{
"error": "Not Found",
"message": "transaction not found"
}Retorna o status e métricas de risco agregadas de uma conta. Lê do Redis (read model atualizado pelos projectors).
Response 200 OK:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"status": "active",
"risk_level": 0.25,
"total_transactions": 42,
"flagged_count": 3,
"declined_count": 1,
"last_activity_at": "2026-03-01T11:55:00Z",
"created_at": "2025-01-15T08:00:00Z"
}Para conta bloqueada:
{
"id": "550e8400-...",
"status": "blocked",
"blocked_at": "2026-03-01T12:00:00Z"
}Valores possíveis de status: active | flagged | blocked
Bloqueia uma conta manualmente.
Request body:
{
"reason": "múltiplas tentativas de fraude confirmadas",
"blocked_by": "fraud-team"
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
reason |
string | Sim | Motivo do bloqueio |
blocked_by |
string | Sim | Identificador de quem bloqueou (operador, sistema) |
Response 204 No Content — sem body.
Response 500 Internal Server Error (conta já bloqueada):
{
"error": "Internal Server Error",
"message": "account 550e8400-... is already blocked"
}Lista as transações com risco >= 0.5 (medium ou high). Lê do Redis.
Query params:
| Parâmetro | Padrão | Descrição |
|---|---|---|
| (sem params por ora) | page=1, limit=20 | Paginação futura |
Response 200 OK:
{
"alerts": [
{
"id": "a1b2c3d4-...",
"transaction_id": "7f3b9a1c-...",
"account_id": "550e8400-...",
"amount": 99999.99,
"currency": "USD",
"risk_score": 0.90,
"risk_level": "high",
"reasons": [
"transaction amount 99999.99 exceeds threshold 10000.00",
"transaction from suspicious location: XX"
],
"status": "open",
"created_at": "2026-03-01T12:00:00Z"
}
],
"total": 1,
"page": 1,
"limit": 20
}Valores possíveis de status do alerta: open | reviewed | dismissed
O engine avalia cada transação em paralelo contra 4 regras. Os scores são somados e limitados a 1.0.
| Regra | Score | Condição de disparo |
|---|---|---|
amount_threshold |
+0.4 | Valor da transação > FRAUD_AMOUNT_THRESHOLD |
velocity |
+0.5 | Conta fez mais de FRAUD_MAX_TX_PER_HOUR transações na última hora |
suspicious_location |
+0.5 | País da transação está na lista de locais suspeitos (XX, ZZ por padrão) |
blacklist |
+0.9 – 1.0 | Conta ou merchant está na blacklist |
# Unitários (sem dependências externas)
make test
# Com cobertura (mínimo 70%)
make cover
# Relatório HTML de cobertura
make cover-html
# Integração (requer Postgres + Redis rodando)
POSTGRES_DSN="postgres://postgres:postgres@localhost:5432/antifraude?sslmode=disable" \
REDIS_ADDR="localhost:6379" \
make integration-test
# Load test (requer k6 instalado)
make load-test| Camada | Cobertura |
|---|---|
| Domain (entity, event) | 100% |
| Fraud Rules Engine | 97.4% |
| Application Bus | 100% |
| Command Handlers | 86.3% |
| Query Handlers | 100% |
| Infrastructure HTTP | 100% |
| Infrastructure Messaging | 100% |
| Projectors | 76.2% |
| Total (excluindo persistence) | 78.1% |
A camada
persistence/postgresepersistence/redissão cobertas pelos testes de integração, não pelos testes unitários.
O pipeline GitHub Actions (ci.yml) executa em todo PR e push:
lint → unit tests (≥70%) → integration tests → docker build
| Job | O que faz |
|---|---|
| Lint | golangci-lint com 20+ linters configurados em .golangci.yml |
| Unit Tests | go test excluindo persistence, verifica cobertura ≥ 70% |
| Integration Tests | Sobe Postgres e Redis via services:, aplica migration, roda testes |
| Build | Build da imagem Docker multi-stage, valida que compila sem erros |
O EventBus é definido por interface em internal/application/eventbus/event_bus.go. Para usar Kafka:
- Crie
internal/infrastructure/messaging/kafka/event_bus.goimplementando a interface - Substitua a injeção no
cmd/api/main.go
- Crie o arquivo em
internal/fraud/rules/minha_regra.goimplementandorules.Rule - Registre no engine em
cmd/api/main.go:rules.NewEngine( // regras existentes... rules.NewMinhaRegra(...), )
- Crie o command/query model em
internal/command/model/ouinternal/query/model/ - Implemente o handler em
internal/command/handler/ouinternal/query/handler/ - Registre no bus em
cmd/api/main.go - Adicione a rota em
internal/infrastructure/http/router.go
MIT