██╗██████╗ ███████╗██████╗ ██╗ █████╗ ███████╗
     ██║██╔══██╗╚══███╔╝██╔══██╗██║██╔══██╗██╔════╝
     ██║██████╔╝  ███╔╝ ██║  ██║██║███████║███████╗
██   ██║██╔═══╝  ███╔╝  ██║  ██║██║██╔══██║╚════██║
╚█████╔╝██║     ███████╗██████╔╝██║██║  ██║███████║
 ╚════╝ ╚═╝     ╚══════╝╚═════╝ ╚═╝╚═╝  ╚═╝╚══════╝   
⠀⠀
    

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(&quotation)
    
    return &quotation, 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