Pular para o conteúdo

Arquitetura real · Pleno

O cache "sempre-verde": saldo por event sourcing sem estourar a memória

O problema é concreto: qualquer instância atendia qualquer cliente, então cada uma ocupava cerca de 100 GB e sofria pausas de GC. A solução usa a chave account-id + event-counter, com o contador incrementado na mesma transação. O caso explica por que o basis-t global foi rejeitado.

15 min de leitura · Fonte: Nubank · Conteúdo revisado em 23 de outubro de 2026

Resposta curta

O problema é concreto: qualquer instância atendia qualquer cliente, então cada uma ocupava cerca de 100 GB e sofria pausas de GC. A solução usa a chave account-id + event-counter, com o contador incrementado na mesma transação. O caso explica por que o basis-t global foi rejeitado.

01 · CONTEXTO

Saldo como soma de eventos

Em event sourcing a ideia é, em vez de guardar o estado atual do sistema num banco, guardar cada passo da história até agora. Para o saldo de uma conta, isso significa que o saldo numa data é a soma de todos os depósitos até ela menos a soma de todos os saques até ela.

A pergunta previsível sobre essa abordagem era se o serviço percorre a lista inteira a cada consulta de saldo. Com o tempo, o que se mostrou um problema foi o requisito de memória.

O Datomic, banco usado pelo serviço, separa as tarefas de escrever, armazenar e consultar dados. Todas as escritas passam por um único processo, o transactor, que as serializa e valida. Os processos de consulta são os peers, e no Nubank os peers são as próprias instâncias dos microsserviços, que carregam dados do DynamoDB para um grande cache em memória. O Nubank também lista Datomic e DynamoDB entre as tecnologias em que guarda dados.

02 · O PROBLEMA

Instâncias que não cabiam na memória

As máquinas começaram a travar: ficavam sem memória e paravam por pausas de coleta de lixo que duravam minutos. A primeira resposta foi aumentar a memória das instâncias e, no início, funcionou. Ao chegar a instâncias de 100GB penduradas por um fio, a equipe concluiu que não dava para continuar assim.

O problema real repousava em dois fatos sobre o sistema. O primeiro: a abordagem com event sourcing exigia carregar muitos dados do armazenamento para o cache em memória dos peers. O segundo: qualquer instância do serviço podia ser chamada para responder a qualquer pedido.

  1. Um cliente que faz quatro pedidos de saldo ao navegar pelo app pode forçar quatro instâncias diferentes a carregar os mesmos dados.

    Diagrama da arquitetura: Saldo: qualquer instância atende qualquer pedido
    CLApp do cliente
    NWBalanceador
    CPInstância A (peer)
    CPInstância B (peer)
    CPTransactor
    DBArmazenamento (DynamoDB)
  2. Cada instância carrega dados do armazenamento para um grande cache em memória, e qualquer uma pode ser chamada para responder a qualquer pedido.

    Diagrama da arquitetura: Saldo: qualquer instância atende qualquer pedido
    CLApp do cliente
    NWBalanceador
    CPInstância A (peer)cache próprio
    CPInstância B (peer)cache próprio
    CPTransactor
    DBArmazenamento (DynamoDB)
  3. Nenhuma instância poderia guardar o banco inteiro no cache em memória, mas todas tinham de guardar dados de todos os clientes ativos a cada momento.

    Diagrama da arquitetura: Saldo: qualquer instância atende qualquer pedido
    CLApp do cliente
    NWBalanceador
    CPInstância A (peer)
    CPInstância B (peer)
    CPTransactor
    DBArmazenamento (DynamoDB)
  4. Com instâncias de 100GB penduradas por um fio, as máquinas ficavam sem memória e paravam por pausas de coleta de lixo de minutos.

    Diagrama da arquitetura: Saldo: qualquer instância atende qualquer pedido
    CLApp do cliente
    NWBalanceador
    CPInstância A (peer)pausas de GC
    CPInstância B (peer)pausas de GC
    CPTransactor
    DBArmazenamento (DynamoDB)

Na prática, todas as instâncias gastavam tempo e memória refazendo trabalho que outra máquina já tinha feito.

03 · A IDEIA

Um cache que nunca precisa ser invalidado

O autor observa que o difícil em cache não é cachear, e sim invalidar. A saída foi contornar o difícil em vez de resolvê-lo: um cache em que toda referência aponta para nada (um miss) ou para algo imutável, portanto válido para um instante. Quando o mundo muda, muda-se a referência, não o valor.

A inspiração veio do próprio Datomic, que guarda dados como uma grande árvore de nós imutáveis. Se um nó já está no cache em memória, ele é válido por ser um valor imutável.

A receita tem três peças: um armazenamento imutável que mapeia referências a valores, um jeito de derivar o valor atual do banco e empurrá-lo ao armazenamento, e um jeito de obter uma referência válida para o valor mais recente. Qualquer armazenamento chave-valor serve como armazenamento imutável, e a equipe escolheu o DynamoDB.

A estratégia de leitura é read-through: procura-se no cache; se o dado falta, calcula-se do zero, grava-se no cache e devolve-se.

04 · A CHAVE

Da versão global à versão por conta

O Datomic tem um número de versão que a aplicação pode usar, o basis-t, uma referência a um ponto no tempo do banco. A equipe considerou usá-lo como referência da chave, mas toda transação incrementa esse número. Perder o cache de uma conta por causa de um depósito em outra conta é tolice, diz o autor.

A solução foi adicionar um atributo à entidade da conta no Datomic. Ele começa em zero e, a cada ação relacionada àquela conta, é incrementado em 1 na mesma transação. Como conta cada evento da conta, ganhou o nome event-counter, e dá um número de versão dos dados da conta, usável como chave de cache.

Basis-t globalContador por conta
Versão que entra na chaveRelógio global do DatomicAtributo na entidade da conta
O que a faz mudarToda transaçãoToda ação da conta, na mesma transação
Efeito no cachePerde o cache por depósito em outra contaSem motivos bobos: o saldo da conta de fato mudou
  1. O app pede o saldo de uma conta e o pedido pode chegar a qualquer instância do serviço.

    Diagrama da arquitetura: Saldo com cache sempre-verde
    CLApp do cliente
    NWBalanceador
    CPInstância do serviço
    DBDatomic (dados e contador)
    DBArmazém imutável de saldos
  2. A instância pergunta ao Datomic o valor atual do event-counter da conta, e esse número serve de versão para a chave de cache.

    Diagrama da arquitetura: Saldo com cache sempre-verde
    CLApp do cliente
    NWBalanceador
    CPInstância do serviço
    DBDatomic (dados e contador)versão da conta
    DBArmazém imutável de saldos
  3. Se a chave não está no armazém imutável, é miss: a instância consulta o Datomic, calcula o saldo e o grava no armazém sob a chave.

    Diagrama da arquitetura: Saldo com cache sempre-verde
    CLApp do cliente
    NWBalanceador
    CPInstância do serviço
    DBDatomic (dados e contador)
    DBArmazém imutável de saldossaldo gravado
  4. Em outro pedido do saldo da mesma conta, o event-counter continua o mesmo e a chave de cache também.

    Diagrama da arquitetura: Saldo com cache sempre-verde
    CLApp do cliente
    NWBalanceador
    CPInstância do serviço
    DBDatomic (dados e contador)
    DBArmazém imutável de saldosmesma chave
  5. Quando a conta recebe um novo depósito ou saque, o event-counter aumenta e o pedido seguinte cai em miss.

    Diagrama da arquitetura: Saldo com cache sempre-verde
    CLApp do cliente
    NWBalanceador
    CPInstância do serviço
    DBDatomic (dados e contador)contador aumenta
    DBArmazém imutável de saldos

05 · TRADE-OFFS E RESULTADO

O banco primeiro, o cache depois

Num CDN, consulta-se o cache primeiro e só depois o servidor real. No esquema descrito, a primeira coisa é consultar o banco para obter o event-counter. Só então se procura no cache e, depois, volta-se ao banco para buscar o restante.

Para o autor, a escolha faz sentido porque disponibilidade a qualquer custo não ajuda o cliente: de que serve ver um saldo velho se ele não consegue agir porque os servidores estão fora do ar?

O objetivo principal do cache era permitir que o serviço rodasse com menos recursos. Cada instância precisava de 90GB de RAM e passou a precisar de 55GB. O serviço também passou a escalar horizontalmente, e a latência caiu para a maioria dos pedidos, tudo sem abrir mão da consistência.

Perguntas frequentes

O que é O cache "sempre-verde": saldo por event sourcing sem estourar a memória?

O problema é concreto: qualquer instância atendia qualquer cliente, então cada uma ocupava cerca de 100 GB e sofria pausas de GC. A solução usa a chave account-id + event-counter, com o contador incrementado na mesma transação. O caso explica por que o basis-t global foi rejeitado.

O que o design de O cache "sempre-verde": saldo por event sourcing sem estourar a memória cobre?

Ele está organizado nestas seções: Saldo como soma de eventos, Instâncias que não cabiam na memória, Um cache que nunca precisa ser invalidado, Da versão global à versão por conta e O banco primeiro, o cache depois.

Quanto tempo leva O cache "sempre-verde": saldo por event sourcing sem estourar a memória e qual é o nível?

Leva cerca de 15 minutos para ler e tem nível Pleno.

Agora projete você mesmo

Rode este caso como uma entrevista na escola de System Design: você toma as decisões, o tutor as questiona e você vê a nota do seu desenho.