Intermediário6 min de leitura

Reference Counts no DynamoDB

Uma reference count é um número que você guarda em um item pai que rastreia quantos itens filhos apontam para ele — likes em um post, membros em um workspace, respostas em um comentário. Você o mantém porque contar os filhos a cada leitura é caro demais.

Como manter uma contagem no DynamoDB?

Guarde o total corrente como um número no item pai e atualize-o na mesma escrita que cria o filho. Um garante que os dois aconteçam juntos ou nenhum, e uma condição na escrita do filho impede que retentativas contem em dobro — assim um único GetItem retorna uma contagem precisa.

  • Não conte os filhos no momento da leitura. Um Query para contar likes paga por cada item de like que varre. Guarde o total no post e leia um item só.
  • Mantenha a contagem onde o filho é escrito, não depois. Incremente-a na mesma operação que cria o filho para que os dois nunca divirjam.
  • Use uma transação quando a escrita e o incremento tocam itens diferentes. Um like é um item, a contagem mora em outro — TransactWriteItems faz ambos acontecerem ou nenhum.
  • O footgun é a contagem dupla. Um like repetido ou duplicado que reexecuta o incremento infla o número. Proteja a escrita do filho com uma condição.

Por que contar afinal

Vindo do SQL, você nunca guardaria um like-count — você faria SELECT COUNT(*) FROM likes WHERE post_id = ? e deixaria um índice barateá-lo. O DynamoDB não tem COUNT(*) que pule a leitura dos itens.

Um Query sobre os likes de um post lê — e cobra por — cada item de like naquela partição, mesmo que você só queira o número. Em um post viral isso são milhares de RCUs para responder "quantos likes?" Esse é o footgun de leitura que reference counts existem para matar.

Então você desnormaliza: guarda o total corrente no próprio post. Ler a contagem vira um único GetItem. O custo é que agora é você quem é dono de mantê-la correta.

Modele os itens

Dois tipos de item compartilham uma partição para que o post e seus likes fiquem em uma coleção de itens. Chaves inventadas:

Post item
PKSKattributes
POST#a91fMETAlikeTally (Number), body, authorId, createdAt
Like item
PKSKattributes
POST#a91fLIKE#USER#7c20likedAt

O atributo likeTally no item META é a reference count. Cada item LIKE# é um filho. Colocar ambos sob PK = "POST#a91f" significa que um único Query pode buscar o post e seus likers juntos quando você quiser a lista.

Incremente a contagem atomicamente

O DynamoDB incrementa um número com uma update expression ADD (ou SET x = x + :n) — isto é um atomic counter: o DynamoDB aplica o delta no lado do servidor sem você ler o valor atual primeiro, então incrementos concorrentes não se atropelam. (AWS: atomic counters)

O problema: dar like em um post são duas escritas em dois itens — criar o item LIKE# e somar 1 ao likeTally no META. Se o like acontece mas o incremento falha, a contagem fica errada para sempre. Você precisa de ambos ou de nenhum.

É isso que TransactWriteItems garante — tudo-ou-nada entre múltiplos itens, e ele cancela a transação inteira se qualquer item for modificado concorrentemente (AWS: pessimistic locking com transações):

{
  "TransactItems": [
    {
      "Put": {
        "TableName": "Social",
        "Item": {
          "PK": {"S": "POST#a91f"},
          "SK": {"S": "LIKE#USER#7c20"},
          "likedAt": {"N": "1750636800"}
        },
        "ConditionExpression": "attribute_not_exists(SK)"
      }
    },
    {
      "Update": {
        "TableName": "Social",
        "Key": {
          "PK": {"S": "POST#a91f"},
          "SK": {"S": "META"}
        },
        "UpdateExpression": "ADD likeTally :one",
        "ExpressionAttributeValues": {":one": {"N": "1"}}
      }
    }
  ]
}

O Put e o Update comitam juntos. Se qualquer um falhar, o DynamoDB faz rollback de ambos e retorna um TransactionCanceledException.

Proteja-se contra contagem dupla

O bug de verdade não é um like escrito pela metade — a transação previne isso. É o mesmo usuário dando like duas vezes, ou um retry do cliente reexecutando a requisição. Cada replay adiciona outro 1, e o likeTally silenciosamente deriva acima da contagem verdadeira.

O ConditionExpression: attribute_not_exists(SK) no Put é a proteção. Se o item LIKE# daquele usuário já existe, a condição do Put falha, a transação inteira é cancelada e — crucialmente — o ADD nunca roda. Um like por usuário, garantido pela chave.

Monte e copie essas update e condition expressions — com os ExpressionAttributeValues certos e a proteção attribute_not_exists — no DynamoDB Expression Builder em vez de montar o JSON na mão.

Unlike, e o custo

Remover um like é a imagem espelhada: Delete o item LIKE# com ConditionExpression: attribute_exists(SK), e ADD likeTally :minusOne na mesma transação. A condição impede que um unlike duplo leve a contagem ao negativo.

Saiba o preço. Uma escrita transacional custa 2 WCUs por item para itens de até 1 KB — uma para preparar, uma para comitar — versus 1 WCU para uma escrita simples. Um like são dois itens, então cada like é cerca de quatro WCUs. Barato por ação, mas vale saber antes de um post de celebridade levar uma tempestade de likes.

Veja no DynoTable

Quando você suspeita que uma contagem derivou, você quer comparar o likeTally guardado contra o número real de filhos LIKE# — sem rodar uma query de contagem em produção.

O item META do post ao lado dos seus filhos LIKE# em uma coleção de itens, para você comparar a olho a contagem guardada contra a contagem real de filhos.
O item META do post ao lado dos seus filhos LIKE# em uma coleção de itens, para você comparar a olho a contagem guardada contra a contagem real de filhos.

Para uma reconciliação de verdade sobre um conjunto delimitado de posts — "quais contagens não batem com suas contagens de filhos?" — a SQL Workbench do DynoTable roda o GROUP BY e o join no cliente sobre as linhas que você carregou, o que o PartiQL puro não consegue expressar.

Armadilhas e próximos passos

  • Não mantenha a contagem fora de banda (um Lambda que reconta toda noite). É um band-aid sobre um caminho de escrita que deveria ter sido transacional desde o começo.
  • Cuidado com hot partitions. Um único post extremamente popular concentra todo like — e todo incremento de contagem — em uma única partition key. A contagem está correta; a partição ainda pode sofrer throttling.
  • Reconcilie raramente, repare cirurgicamente. A derivação deveria ser próxima de zero se toda mutação for condicionada. Trate uma divergência como um bug para encontrar, não um número para sobrescrever.

Leitura relacionada: single-table design para entender por que o post e os likes compartilham uma partição, e Query vs Scan para entender por que contar filhos no momento da leitura é o padrão que você está evitando.

Depois baixe o DynoTable para inspecionar essas coleções de itens e verificar suas contagens contra suas próprias tabelas.

Atualizado