Microserviços, Eventos e a Arte de Processar Transações: Um Projeto para Tópicos Avançados de Banco de Dados
Entreguei semana passada o que é provavlmente o projeto mais legal que já fiz pra faculdade. Na reta final do semestre, com menos de duas semanas para entregar o projeto de Tópicos Avançados de Banco de Dados. Junto com meu amigo Leandro (já citado previamente e um dos malucos que me cobra a escrever aqui), passamos o semestre todo discutindo ideias, mas como essa matéria é a ultima aula da sexta feira, sempre acabávamos esquecendo ou mudando de ideia. Até que finalmente decidimos: vamos construir um sistema de transações financeiras. Parte da Ideia veio da vontade de fazer algo com uma funcionalidade “real” e utilizar a abordagem de microsserviços que atualmente é a queridinha da área tech.
O Projeto
O objetivo era usar diferentes bancos de dados para diferentes necessidades. A arquitetura que tínhamos que seguir era bem específica:
-------
--------> | |
| | DB1 |
| ------- | |
| | -------
| v
------ -------------- ------ -------
| | | | --> | | --> | |
| S1 | ---> | mensageria | | S2 | | DB2 |
| | | | <-- | | <-- | |
------ -------------- ------ -------
| | ^ |
| ------ | | | -------
| | | | | -----> | |
--->| S3 |<--| | | RDB |
| | ---------| |
------ -------
Cada componente tinha seu papel:
- S1: Gerador de eventos (API)
- S2: Processador de eventos (serviços propriamente ditos)
- S3: Validador (nosso sistema de auditoria)
- RDB: Banco relacional (PostgreSQL)
- DB1 e DB2: Bancos NoSQL (MongoDB e Cassandra)
- Mensageria: RabbitMQ
Por que essas escolhas?
Confesso que algumas escolhas foram por praticidade. O RabbitMQ parecia mais fácil de integrar que o Kafka, e tanto eu quanto meu amigo já tínhamos experiência com PostgreSQL e MongoDB. Mas no final, essas escolhas fizeram muito sentido:
- PostgreSQL: Para dados dos usuários, porque precisávamos de consistência forte e transações ACID
- MongoDB: Para cotações de moedas e transações, já que os dados mudam frequentemente e o schema é flexível
- Cassandra: Para o sistema de validação e auditoria, porque precisávamos de alta disponibilidade e escrita distribuída
- RabbitMQ: Para mensageria, porque era mais simples de configurar e tinha uma boa documentação
O Sistema em Detalhes
1. O Gerador (S1)
Este é o ponto de entrada do sistema. Quando um usuário quer comprar ou vender moedas, o gerador cria um evento e publica no RabbitMQ. A parte mais legal foi implementar o sistema de filas (Além de trabalhar com structs que trazem o GO um pouco mais pra perto da Orientação a Objetos e me deixa mais em “casa”):
// Exemplo simplificado do nosso gerador
type TransactionEvent struct {
Action string `json:"action"` // "BUY" ou "SELL"
Data struct {
UserID string `json:"user_id"`
Amount float64 `json:"amount"`
CurrencyPair string `json:"currency_pair"`
Timestamp time.Time `json:"timestamp"`
} `json:"data"`
}
// Nossa função de compra
func (h *Handler) BuyCurrency(c *gin.Context) {
// Validação do usuário
userID := c.GetString("user_id")
// Criação do evento
event := TransactionEvent{
Action: "BUY",
Data: TransactionData{
UserID: userID,
Amount: amount,
// ... outros campos
},
}
// Publicação no RabbitMQ
// Usamos filas específicas para cada tipo de evento
h.publisher.Publish("transactions", event)
}
O sistema usa filas específicas para cada tipo de evento:
- users: Para eventos de usuário (criação, atualização, deleção)
- transactions: Para eventos de compra e venda
- quotations: Para atualizações de cotação
- transactions-validator: Para validação de transações
2. O Processador (S2)
Aqui é onde tudo acontece. Dividimos em três serviços:
User Service (PostgreSQL)
// Atualização de saldo com transação
func (s *UserService) UpdateBalance(userID string, amount float64) error {
tx, err := s.db.Begin()
if err != nil {
return err
}
// Atualização atômica do saldo
_, err = tx.Exec(`
UPDATE users
SET balance = balance + $1
WHERE id = $2
`, amount, userID)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
Quotation Service (MongoDB)
// Busca de cotação mais recente
func (s *QuotationService) GetLatestRate(pair string) (*Quotation, error) {
var quotation Quotation
// Usamos índices TTL para limpar cotações antigas
err := s.collection.FindOne(
context.Background(),
bson.M{"currency_pair": pair},
options.FindOne().SetSort(bson.M{"timestamp": -1}),
).Decode("ation)
return "ation, err
}
Transaction Service (MongoDB)
// Registro de transação
func (s *TransactionService) SaveTransaction(tx *Transaction) error {
// Salvamos a transação no MongoDB
return s.collection.InsertOne(
context.Background(),
tx,
)
}
3. O Validador (S3)
Este foi o serviço mais desafiador. Ele precisa:
- Registrar todas as mensagens enviadas
- Validar se cada mensagem teve resposta
- Garantir que os dados estão consistentes
- Gerar relatórios de auditoria
// Exemplo simplificado do validador
func (v *Validator) ValidateTransaction(txID string) error {
// Busca a mensagem original
originalMsg, err := v.store.GetMessage(txID)
if err != nil {
return err
}
// Busca a resposta
responseMsg, err := v.store.GetResponse(txID)
if err != nil {
return err
}
// Valida consistência
if !v.isConsistent(originalMsg, responseMsg) {
v.alertInconsistency(txID)
return ErrInconsistency
}
return nil
}
A Lógica por Trás do Sistema
Quando começamos a projetar o sistema, nos deparamos com alguns desafios fundamentais que precisávamos resolver. O primeiro deles era garantir que cada transação fosse processada corretamente, sem perder dados ou criar inconsistências. Para isso, decidimos usar o RabbitMQ como nossa “fonte da verdade”. Se uma mensagem chegou lá, ela será processada, não importa o que aconteça.
Implementamos um sistema robusto de mensageria com:
- Retry automático com Nack para mensagens com erro
- QoS (Quality of Service) configurado para 1 mensagem por vez
- Filas duráveis para garantir persistência
- Consumer Manager para gerenciar múltiplos consumidores
- Graceful shutdown para todos os consumidores
A consistência dos dados entre os diferentes bancos foi outro desafio interessante. O PostgreSQL se tornou nossa fonte da verdade para o dinheiro dos usuários, mantendo os saldos com toda a segurança das transações ACID. O MongoDB, por sua vez, funciona como um cache rápido para as cotações e armazena as transações, com um TTL (Time To Live) que garante que sempre usamos dados atualizados. O Cassandra entra como nosso sistema de auditoria, registrando todo o histórico de validações de forma escalável.
O que Aprendemos?
Mensageria é Complexa
Para quem está começando, como eu, mensageria pode ser um dos pontos mais difíceis de entender. O conceito parece simples: um serviço envia uma mensagem, outro consome. Mas na prática, há muito mais envolvido. É fácil subestimar a complexidade quando não se tem ainda uma visão completa do sistema.
Performance
Trabalhar com PostgreSQL foi relativamente tranquilo. Por já ter familiaridade e usá-lo com frequência, consegui aproveitar bem seus recursos para consultas complexas e controle transacional. Com o MongoDB, a experiência também foi fluida. A flexibilidade do schema e a forma como lida com dados semi-estruturados facilitaram o trabalho com cotações de moedas.
Já o Cassandra foi, sem dúvida, o mais trabalhoso. A configuração inicial é detalhista, e pequenos erros podem comprometer a distribuição de dados. A modelagem precisa ser feita pensando exclusivamente em leitura o que exige uma mudança de mentalidade em relação a bancos relacionais.
Conclusão
Esse projeto foi desafiador, principalmente pelo tempo limitado e pela complexidade dos conceitos envolvidos. Ainda assim, a experiência foi extremamente valiosa. Foi a primeira vez que precisei pensar em uma arquitetura distribuída de verdade, lidar com mensageria de forma prática e enfrentar os problemas reais de consistência entre diferentes bancos de dados.
Aprendi na marra como os serviços se comunicam, o que significa manter dados sincronizados em sistemas diferentes, e como eventos são tratados em ambientes com múltiplos pontos de falha. No fim de tudo, de verdade, a gente percebe que o que adianta mesmo é fazer um projeto que você acredita com pessoas que tem as mesmas piras que você e principalmente com alguém que você se dê bem e de resto tudo vai se ajeitando.
O código completo está disponível no GitHub.
Fora isso… Obrigado pela atenção, um abraço e até mais!
j