Voltar ao blog
9 de maio de 20266 min de leitura

Repository Pattern com Múltiplos Bancos - PostgreSQL e InfluxDB no Mesmo Projeto!

godatabasebackendprogramming

Três milhões de registros de preço em 30 dias. Era isso que acontecia quando eu monitorava 10 criptomoedas a cada 10 segundos e jogava tudo no PostgreSQL. As queries de "me dá o histórico dos últimos 7 dias" começaram a demorar 4 segundos, e criar índices e particionar tabelas só adiava o inevitável: banco relacional não foi feito para séries temporais em alta frequência. A solução foi usar dois bancos, cada um para o que faz melhor. Mas aí veio o desafio real: como organizar o código para que essa decisão de infraestrutura não vaze para a lógica de negócio?

Guia de tópicos:

  • O Problema: Dois Bancos, Uma Aplicação
  • Repository Pattern: A Interface como Contrato
  • PostgreSQL para Dados Relacionais: Quando Usar
  • InfluxDB para Séries Temporais: Quando o Relacional Não Serve
  • Compondo Repositórios no Use Case: O Poder da Abstração
  • Connection Management: Singleton, Pool e Lifecycle
  • Considerações Finais
  • Links Indicativos

O Problema: Dois Bancos, Uma Aplicação

A maioria dos projetos começa com um banco só. PostgreSQL, MySQL, MongoDB, tanto faz. Você coloca tudo lá e funciona. Mas conforme o projeto cresce, você percebe que nem todo dado tem a mesma natureza. Dados de usuário são relacionais: tem foreign keys, constraints, joins. Dados de preço em tempo real são temporais: chegam em alta frequência, são consultados por range de data, e o volume cresce indefinidamente.

Se você colocar histórico de preços no PostgreSQL, vai funcionar no começo. Mas quando tiver 3 milhões de registros de preço (que é o que acontece quando você monitora 10 criptomoedas a cada 10 segundos por 30 dias), as queries de "me dá o preço do BTC nos últimos 7 dias" vão começar a demorar. Você vai criar índices, vai particionar tabelas, vai otimizar queries, e mesmo assim não vai ter a performance que um banco time-series te dá nativamente.

A solução é usar cada banco para o que ele faz melhor. PostgreSQL para dados relacionais (usuários, posições, profit takes). InfluxDB para séries temporais (histórico de preços, volume 24h). Mas aí surge o desafio: como organizar o código para que essa decisão de infraestrutura não vaze para a lógica de negócio?

Repository Pattern: A Interface como Contrato

O Repository Pattern resolve exatamente esse problema. A ideia é simples: você define uma interface no seu domínio que diz "eu preciso de alguém que saiba fazer X, Y e Z com esses dados". Quem implementa essa interface e qual banco usa por baixo é irrelevante para quem consome.

No projeto que uso como referência, o domínio define interfaces como PositionRepositorier (que sabe salvar, buscar e atualizar posições) e CryptoHistoryRepository (que sabe salvar e buscar histórico de preços). O domínio não sabe e não se importa que o primeiro usa PostgreSQL e o segundo usa InfluxDB. Ele só sabe que precisa dessas operações.

Isso é inversão de dependência na prática. O domínio (camada de alto nível) não depende da infraestrutura (camada de baixo nível). Ambos dependem de abstrações (as interfaces). Se amanhã eu decidir migrar o histórico de preços do InfluxDB para TimescaleDB, ou trocar o PostgreSQL por CockroachDB, a única coisa que muda é a implementação na pasta de infraestrutura. O domínio, os use cases, os controllers, nada disso é afetado.

PostgreSQL para Dados Relacionais: Quando Usar

O PostgreSQL brilha quando seus dados têm relacionamentos claros entre si. No projeto, um usuário tem N posições, cada posição pertence a uma criptomoeda, e cada posição pode ter N retiradas de lucro (profit takes). Isso é um grafo de relacionamentos que o PostgreSQL resolve com maestria usando foreign keys, joins e constraints.

A implementação do repositório de posições usa sqlx para fazer queries parametrizadas e scan automático para structs. O connection pool é configurado com 25 conexões abertas, 10 idle, e lifetime de 5 minutos. Isso garante que múltiplas goroutines podem acessar o banco simultaneamente sem criar conexões novas a cada request.

Um ponto importante: o repositório retorna a interface, não o struct concreto. A função construtora NewPositionRepository() retorna repositorier.PositionRepositorier, não *PositionRepository. Quem consome não sabe e não precisa saber o tipo real. Isso é o que permite trocar a implementação sem afetar nada acima.

Outro detalhe que vale mencionar: cada método do repositório pega a conexão do pool via connection.GetDB() e usa direto. Não fecha a conexão depois (isso era um bug que existia antes no projeto e causava crashes em concorrência). O pool gerencia o lifecycle das conexões automaticamente.

InfluxDB para Séries Temporais: Quando o Relacional Não Serve

O InfluxDB é um banco feito especificamente para dados que têm timestamp como dimensão principal. Ele é otimizado para writes em alta frequência e queries por range de tempo. No projeto, ele armazena dois tipos de dados: histórico de preços das criptomoedas (que chega a cada 10 segundos via WebSocket da Binance) e volume de negociação 24h (atualizado a cada minuto).

A diferença fundamental entre o InfluxDB e o PostgreSQL na forma de modelar dados é o conceito de tags vs fields. Tags são indexadas e usadas para filtrar (como o symbol da crypto). Fields são os valores que você quer armazenar (como o preço). O timestamp é implícito, todo ponto tem um. Não existe schema rígido, não existe migration, não existe ALTER TABLE. Você simplesmente escreve pontos e o banco organiza.

A implementação do repositório de histórico usa a API blocking do InfluxDB para escrita (garantindo que o dado foi persistido antes de retornar) e queries Flux para leitura. Um detalhe importante nas queries: como o InfluxDB retorna uma row por field, é necessário filtrar por _field == "price" para não misturar dados de fields diferentes na mesma consulta. Sem esse filtro, você recebe rows de "price" e "name" misturadas, e o cast para float64 falha silenciosamente nas rows de "name".

A conexão com o InfluxDB também usa singleton com sync.Once, similar ao PostgreSQL. E no graceful shutdown da aplicação, o client é fechado corretamente para garantir que writes pendentes são finalizados.

Compondo Repositórios no Use Case: O Poder da Abstração

Aqui é onde tudo se junta e você vê o valor real do Repository Pattern com múltiplos bancos. O PortfolioUseCase precisa calcular o lucro/prejuízo do portfólio do usuário. Para isso, ele precisa de: posições do usuário (PostgreSQL), profit takes de cada posição (PostgreSQL), preço atual de cada criptomoeda (InfluxDB), e o cadastro de criptomoedas para mapear ID para symbol (PostgreSQL).

São 4 repositórios, 2 bancos diferentes, e o use case não importa nenhum pacote de infraestrutura. Ele recebe 4 interfaces no construtor e trabalha com elas. O cálculo é: pega as posições do usuário, busca o preço atual de cada crypto no InfluxDB, multiplica quantidade pelo preço atual para ter o valor corrente, subtrai o valor investido para ter o lucro/prejuízo, e divide pelo investido para ter o percentual.

Esse use case é perfeitamente testável com mocks. Você cria 4 structs que implementam as interfaces com dados em memória, instancia o use case, e testa o cálculo de P&L sem nenhum banco rodando. Se o cálculo está errado, o problema está na lógica do use case, não na query SQL ou na query Flux. Debugging cirúrgico.

E se amanhã eu decidir que o preço atual não vem mais do InfluxDB mas sim de um cache Redis, eu crio um RedisCryptoHistoryRepository que implementa a mesma interface, troco no módulo de DI, e o use case continua funcionando sem mudar uma linha.

Connection Management: Singleton, Pool e Lifecycle

Um aspecto que muita gente ignora quando trabalha com múltiplos bancos é o gerenciamento de conexões. Cada banco tem seu próprio pool, seu próprio lifecycle, e precisa ser fechado corretamente quando a aplicação encerra.

No projeto, tanto o PostgreSQL quanto o InfluxDB usam o padrão singleton com sync.Once. Isso garante que a conexão é criada uma única vez, independente de quantas goroutines chamam GetDB() simultaneamente. O sync.Once é thread-safe por definição.

O PostgreSQL usa connection pool do sqlx com configuração explícita: 25 conexões máximas abertas, 10 conexões idle, e lifetime de 5 minutos por conexão. Isso evita tanto o problema de abrir conexões demais (que esgota o limite do banco) quanto o de manter conexões mortas (que desperdiça recursos).

O InfluxDB usa o client oficial que gerencia conexões internamente. A única preocupação é fechar o client no shutdown para garantir que writes em buffer são finalizados.

No graceful shutdown da aplicação, ambos os bancos são fechados na ordem correta: primeiro para de aceitar requests novos, depois fecha o PostgreSQL, depois fecha o InfluxDB. Isso garante que nenhum write fica pendente e nenhuma conexão fica aberta.

Considerações Finais

No enfrentamento da complexidade de projetos que precisam de múltiplas fontes de dados, o Repository Pattern é a ferramenta que mantém tudo organizado. Ele não é sobre "criar interfaces por criar". É sobre proteger sua lógica de negócio de decisões de infraestrutura que podem mudar.

O que eu quero que você leve deste artigo é que a escolha do banco de dados é uma decisão de infraestrutura, não de domínio. Seu use case não deveria saber se está falando com PostgreSQL, MongoDB ou um arquivo CSV. Ele deveria saber que precisa "salvar uma posição" e "buscar o preço atual". Como isso acontece por baixo é problema de outra camada.

E lembre-se: usar dois bancos não é complexidade gratuita. É usar a ferramenta certa para cada tipo de dado. Relacional para relacionamentos. Time-series para séries temporais. Cache para dados quentes. A abstração do Repository Pattern é o que permite fazer isso sem que o código vire uma bagunça.


Links indicativos: