Em 2008, Werner Vogels — CTO da Amazon — publicou um artigo no ACM Queue com o título "Eventually Consistent". O artigo começava com uma provocação: a propriedade ACID que todos aprendemos na faculdade, em particular a consistência, tinha um custo enorme em disponibilidade e latência em sistemas distribuídos. A Amazon tinha aprendido isso da forma difícil. O carrinho de compras, um dos sistemas mais críticos, precisava funcionar sempre — mesmo durante partições de rede, mesmo com servidores offline, mesmo durante picos de 10× o tráfego normal. Isso era impossível com consistência forte. A solução era aceitar que dois nós poderiam ter versões diferentes do carrinho ao mesmo tempo, e definir uma regra de convergência: "se dois nós têm versões diferentes do carrinho, a versão final é a união de ambos — nunca perdemos um item." Isso é eventual consistency com semântica de domínio bem definida.
O artigo de Vogels introduziu um vocabulário que ainda usamos: garantias de sessão — contratos entre o sistema e o cliente individual que tornam eventual consistency utilizável. Read-your-writes, monotonic reads, writes follow reads, monotonic writes. Esses contratos não são "eventual consistency vs strong consistency" — são propriedades ortogonais que podem ser combinadas. Um sistema pode ser eventualmente consistente globalmente mas garantir read-your-writes por sessão. Entender esses contratos é o que separa "usou Cassandra e ficou com dados inconsistentes sem entender por quê" de "usou Cassandra sabendo exatamente que garantias cada operação tem, e designou o sistema para que as anomalias possíveis fossem aceitáveis pelo produto".
O espectro de consistência
Consistência não é binária. Existe um espectro de modelos, cada um com tradeoffs de performance, disponibilidade e complexidade de raciocínio para o developer.
# Do mais forte (mais caro) ao mais fraco (mais performático):
# 1. LINEARIZABILITY (Strict Consistency)
# Definição: cada operação parece ser executada atomicamente num ponto único no tempo.
# Todo read retorna o valor do write mais recente globalmente.
# Equivalente a um sistema de processo único.
# Implementação: consenso (Raft, Paxos) ou single-threaded sequencial
# Custo: toda operação precisa coordenar com maioria dos nós
# Latência: ~2× RTT inter-nó mínimo (propose + ack)
# Quem oferece: etcd, ZooKeeper, CockroachDB (por chave), Spanner
# 2. SEQUENTIAL CONSISTENCY (Lamport, 1979)
# Definição: existe uma ordem global das operações que é consistente com
# a ordem de cada processo individualmente — mas pode não refletir tempo real.
# Dois processos podem ver writes em ordens diferentes, desde que cada um
# veja todos os writes de cada processo em ordem.
# Custo: menor que linearizability; não precisa de timestamp de tempo real
# Quem oferece: alguns bancos com total ordering mas sem real-time guarantees
# 3. CAUSAL CONSISTENCY
# Definição: operações causalmente relacionadas são vistas na mesma ordem
# por todos os nós. Operações concorrentes (sem relação causal) podem ser
# vistas em qualquer ordem.
# A → B (A causou B): todos os nós veem A antes de B
# A ∥ B (A e B concorrentes): diferentes nós podem ver A antes de B ou B antes de A
# Custo: rastrear causalidade (vector clocks) mas sem coordenação global
# Quem oferece: COPS, Eiger, alguns sistemas acadêmicos; MongoDB com certas configs
# 4. PRAM / FIFO CONSISTENCY (Pipeline RAM)
# Definição: writes de um único processo são vistos em ordem por todos os outros.
# Writes de processos diferentes podem chegar em qualquer ordem a cada observer.
# Custo: muito baixo; requer apenas ordering por fonte
# Uso: logs de replicação, Kafka (ordering por partition + producer ID)
# 5. EVENTUAL CONSISTENCY
# Definição: se não houver novos writes, eventualmente todos os nós convergem
# para o mesmo valor. SEM garantias sobre ordem ou timing.
# Subtipos (importantes para entender contratos específicos):
# - Strong eventual: convergência garantida após receber os mesmos updates
# (mesmo que em ordem diferente) → CRDTs
# - Weak eventual: convergência possível, mas pode precisar de resolução manual
# 6. SESSION GUARANTEES (ortogonais ao espectro acima)
# Aplicadas por sessão/cliente; NÃO são modelos globais do sistema
# Read Your Writes (RYW): você sempre vê seus próprios writes
# Monotonic Reads (MR): se você leu valor V, futuras leituras retornam V ou mais recente
# Writes Follow Reads (WFR): writes propagam após reads na mesma sessão
# Monotonic Writes (MW): seus writes são aplicados na ordem que você os fez
# Comparação prática de latência (sistema 3 DCs, ~100ms inter-DC RTT):
# Linearizability: ~200ms (round-trip para quorum global)
# Sequential: ~200ms (similar, coordenação necessária)
# Causal: ~1ms (local write + tag; sem espera)
# Eventual: <1ms (local write; fire-and-forget)
# Eventual + RYW: <1ms local; reads do mesmo nó que escreveu
Happens-before e relógios lógicos de Lamport
Para raciocinar sobre causalidade em sistemas distribuídos sem um relógio global, Leslie Lamport definiu em 1978 a relação happens-before (→). É a fundação matemática de tudo que discutiremos depois.
# Definição happens-before (Lamport, 1978):
# A → B se:
# 1. A e B são eventos no mesmo processo e A ocorre antes de B (program order)
# 2. A é o envio de uma mensagem e B é o recebimento dessa mensagem
# 3. (Transitividade) Existe C tal que A → C e C → B
# Eventos são CONCORRENTES (A ∥ B) se NÃO (A → B) E NÃO (B → A)
# Causalidade ≠ tempo real: se A → B, A causou (ou poderia ter causado) B.
# Concorrência ≠ simultâneo: significa que nenhum dos dois causou o outro.
# RELÓGIO LÓGICO DE LAMPORT:
# Cada processo P tem um contador C(P), inicialmente 0.
# Regras:
# - Antes de cada evento local: C(P)++
# - Ao enviar mensagem: incluir C(P) na mensagem
# - Ao receber mensagem com timestamp T: C(P) = max(C(P), T) + 1
# Propriedade: se A → B, então C(A) < C(B)
# LIMITAÇÃO: o inverso NÃO é verdadeiro!
# C(A) < C(B) NÃO implica A → B (podem ser concorrentes)
# Lamport clocks não distinguem concorrência de causalidade
# Exemplo:
# P1: evento a (C=1) → envia msg para P2 (C=1 na msg)
# P2: recebe msg (C = max(0,1)+1 = 2) → evento b (C=3)
# P3: evento c (C=1) — CONCORRENTE com tudo em P1 e P2
#
# a → b (causal; msg transmitiu causalidade)
# c ∥ a e c ∥ b (P3 não tem conexão causal com P1/P2)
# Mas C(c)=1 < C(b)=3, e isso não significa c → b
# POR QUE ISSO IMPORTA:
# Em bancos distribuídos, timestamps de sistema (wall clock) são usados para
# Last Write Wins (LWW). Mas relógios de sistema driftam (Network Time Protocol
# tem precisão de ~100ms; em sistemas de alta taxa isso é catastrófico).
# Dois writes "simultâneos" com diferentes wall clocks podem ordenar incorretamente.
# Amazon DynamoDB original usava vector clocks para evitar esse problema.
# CassandraDB usa timestamps de cliente (timestamps do processo que escreveu) com LWW
# — é por isso que relógios desajustados em Cassandra perdem dados silenciosamente.
Vector clocks: capturando causalidade com precisão
Vector clocks resolvem a limitação dos relógios de Lamport: com eles, é possível determinar se dois eventos são causalmente relacionados ou concorrentes. Cada nó rastreia um vetor de contadores — um por participante no sistema.
# VECTOR CLOCK:
# Sistema com N processos. Cada processo Pi mantém V[i], um vetor de N inteiros.
# V[i][j] = "quantos eventos de Pj o processo Pi conhece"
# Regras:
# - Evento local em Pi: V[i][i]++
# - Enviar mensagem de Pi: incluir V[i] na mensagem
# - Receber mensagem de Pj com vetor Vm em Pi:
# V[i][k] = max(V[i][k], Vm[k]) para todo k
# V[i][i]++ (evento de recebimento conta como evento local)
# Comparação entre vector clocks:
# V ≤ V' se V[k] ≤ V'[k] para todo k (V "aconteceu antes" ou igual)
# V < V' se V ≤ V' e V ≠ V' (V happened-before V')
# CONCORRENTE se NÃO (V ≤ V') e NÃO (V' ≤ V)
# Exemplo (3 processos A, B, C):
# Estado inicial: A=[0,0,0], B=[0,0,0], C=[0,0,0]
#
# A grava X=1: A=[1,0,0]
# A envia para B: mensagem carrega [1,0,0]
# B recebe: B=[1,1,0] (max([0,0,0],[1,0,0]) + B incrementa próprio)
# B grava Y=2: B=[1,2,0]
#
# C grava Z=3: C=[0,0,1] (CONCORRENTE — C não sabe nada de A ou B ainda)
#
# B envia para C: mensagem carrega [1,2,0]
# C recebe: C=[1,2,2] (max([0,0,1],[1,2,0]) = [1,2,1] + C incrementa)
#
# Comparações:
# A=[1,0,0] < B=[1,2,0]: A → B (A causou B, via mensagem) ✓
# C=[0,0,1] ∥ A=[1,0,0]: C e A são concorrentes (nenhum causou o outro) ✓
# C=[0,0,1] ∥ B=[1,2,0]: C e B são concorrentes (antes de B enviar para C) ✓
# C=[1,2,2] > B=[1,2,0]: B → C (B causou o C final, via mensagem) ✓
# CUSTO DOS VECTOR CLOCKS:
# O vetor cresce com o número de participantes.
# Para sistema com N participantes: O(N) espaço por evento e por mensagem.
# Em sistemas com milhares de nós, isso se torna proibitivo.
# Solução 1: plausible clocks (truncar após K eventos mais recentes)
# Solução 2: dot notation (Riak) — versão compacta dos vector clocks
# Solução 3: CRDTs que não precisam de vector clocks para convergir
# RIAK E DYNAMO:
# Riak usava vector clocks explícitos; migrou para "dotted version vectors" em 2014
# — mais compactos e sem o problema de "sibling explosion"
# DynamoDB original usava vector clocks; migrou para LWW com timestamps
# — tradeoff: simplicidade vs precisão de causalidade
# CASSANDRA:
# Não usa vector clocks. Usa timestamps de cliente (write timestamp).
# LWW: write com maior timestamp vence.
# Consequência: se dois writes chegam com o mesmo timestamp, um é perdido
# silenciosamente (baseado em ordering de bytes do valor).
# Consequência crítica: clock skew entre clientes = perda de dados.
Conflitos e estratégias de resolução
# Quando dois writes concorrentes (sem relação happens-before) chegam ao mesmo nó,
# o sistema precisa decidir o que fazer. Opções:
# 1. LAST WRITE WINS (LWW)
# Usar o write com maior timestamp. O outro é descartado.
# Implementação: cada write carrega um timestamp; ao merge, mantém o maior.
# Vantagem: trivialmente simples; sem conflito visível para a aplicação
# Desvantagem: PERDE DADOS SILENCIOSAMENTE.
# Alice adiciona produto A no carrinho (t=100).
# Bob (sessão diferente) remove produto B do carrinho (t=101).
# LWW: Bob "vence" e o estado é o carrinho sem B, MAS sem A também.
# Se o write de Bob não incluía o produto A (escreveu estado parcial),
# o produto A é perdido sem nenhum erro.
# Quem usa: Cassandra (timestamp de cliente), muitos bancos em modo "eventual"
# Mitigação: usar timestamps de alta resolução (UUID v1, Snowflake ID) e
# escrever sempre o objeto completo (não deltas parciais)
# 2. MULTI-VALUE (Siblings)
# Preservar ambos os valores concorrentes; expor o conflito para a aplicação.
# Implementação: ao detectar versões concorrentes (vector clock comparison),
# armazenar ambas como "siblings". A aplicação resolve.
# Vantagem: zero perda de dados; a aplicação decide a semântica correta
# Desvantagem: a aplicação PRECISA lidar com conflitos (overhead de dev)
# Quem usa: Riak (siblings explícitos), DynamoDB original (versões concorrentes)
# Exemplo Dynamo shopping cart:
# Duas versões concorrentes do carrinho → merge = UNIÃO dos itens
# Possível ter duplicatas (item adicionado por ambos): remove duplicatas
# 3. MERGE CUSTOMIZADO (domain-specific)
# Definir uma função de merge que sempre converge e respeita semântica do domínio.
# Exemplo 1 — contador de likes: merge = soma dos deltas (não o valor atual)
# Exemplo 2 — carrinho: merge = união dos conjuntos de itens
# Exemplo 3 — documento colaborativo: merge = algoritmo OT ou CRDT de texto
# Vantagem: preserva dados + semântica correta do produto
# Desvantagem: requer design cuidadoso por tipo de dado
# Isso é a essência dos CRDTs: estruturas de dados onde merge é sempre definido
# e associativo/comutativo/idempotente
# 4. CONFLICT AVOIDANCE (sharding por entidade)
# Em vez de conflitos, garantir que só UM nó processa writes para cada entidade.
# Single-writer per entity: usuário X sempre escreve no shard S.
# Vantagem: zero conflitos por design; consistência forte por entidade
# Desvantagem: hotspot se entidade é popular; failover precisa reatribuição
# Quem usa: Kafka (partition ownership), Redis Cluster (hash slot ownership)
CRDTs: estruturas que convergem por design
CRDT (Conflict-free Replicated Data Type) é uma estrutura de dados projetada matematicamente para que qualquer conjunto de updates, aplicados em qualquer ordem, produza o mesmo resultado final. Não há conflitos porque a semântica é definida para não ter. São a base de Google Docs, Figma, e qualquer editor colaborativo moderno.
# Propriedades matemáticas de um CRDT (CvRDT — state-based):
# A operação merge deve ser:
# - Comutativa: merge(A, B) = merge(B, A) (ordem não importa)
# - Associativa: merge(A, merge(B, C)) = merge(merge(A, B), C)
# - Idempotente: merge(A, A) = A (aplicar duas vezes = uma vez)
# Se merge satisfaz essas propriedades, o sistema converge automaticamente.
# G-COUNTER (Grow-Only Counter):
# Problema: contar eventos em sistema distribuído sem coordinator.
# Estrutura: cada nó i tem um contador local C[i]
# Increment: C[myNodeId]++ (apenas meu próprio counter)
# Value: sum(C[i] for all i)
# Merge: max(C[i], C'[i]) element-wise
class GCounter:
def __init__(self, node_id, num_nodes):
self.node_id = node_id
self.counters = [0] * num_nodes # um counter por nó
def increment(self):
self.counters[self.node_id] += 1
def value(self):
return sum(self.counters)
def merge(self, other):
# Merge: max element-wise (comutativo, associativo, idempotente)
merged = GCounter(self.node_id, len(self.counters))
merged.counters = [max(a, b) for a, b in zip(self.counters, other.counters)]
return merged
# Exemplo:
# Node A: [3, 0, 0] (incrementou 3x)
# Node B: [0, 5, 0] (incrementou 5x)
# Node C: [0, 0, 2] (incrementou 2x)
# Merge de A e B: [3, 5, 0], valor = 8
# Merge final: [3, 5, 2], valor = 10 ← valor correto global
# PN-COUNTER (Positive-Negative Counter — para incremento e decremento):
# Dois G-Counters: P (incrementos) e N (decrementos)
# Value: P.value() - N.value()
# Merge: merge P e N separadamente
# LWW-REGISTER (Last Write Wins Register):
# Estrutura: (value, timestamp)
# Merge: o par com maior timestamp vence
# Propriedades: comutativo e associativo MAS resolve conflito por timestamp
# Problema: se dois writes têm mesmo timestamp exato, é não-determinístico
# Solução: usar timestamp de alta precisão + node ID como desempate
# OR-SET (Observed-Remove Set — conjunto com add/remove):
# O desafio: em conjunto distribuído, como garantir que remove desfaz add?
# Problema clássico: A adiciona X (timestamp T1); B remove X (sem saber de T1).
# Se B remove "todo X", remove o X que A adicionou — perda de dado.
# OR-Set solução: cada ADD cria uma tag única. REMOVE remove tags específicas.
# Add "maçã" → cria tag (maçã, uuid4())
# Remove "maçã" → remove TODAS as tags de "maçã" que o nó CONHECE
# Se o add de A ainda não chegou em B quando B fez remove → a tag de A persiste!
# Quando B finalmente recebe o add de A → tag existe, maçã está no set
class ORSet:
def __init__(self):
self.elements = {} # element → set of tags
def add(self, element, unique_tag=None):
import uuid
tag = unique_tag or str(uuid.uuid4())
if element not in self.elements:
self.elements[element] = set()
self.elements[element].add(tag)
return tag
def remove(self, element):
# Remove todas as tags conhecidas (observadas) para este elemento
if element in self.elements:
del self.elements[element]
def contains(self, element):
return element in self.elements and len(self.elements[element]) > 0
def merge(self, other):
result = ORSet()
all_elements = set(self.elements) | set(other.elements)
for e in all_elements:
my_tags = self.elements.get(e, set())
other_tags = other.elements.get(e, set())
merged_tags = my_tags | other_tags # união das tags
if merged_tags:
result.elements[e] = merged_tags
return result
# Semântica: add-wins (se há conflito simultâneo add+remove, add vence)
# Por quê: a tag do add persiste; o remove só elimina tags que já conhecia
# MV-REGISTER (Multi-Value Register — o "carrinho" do Dynamo):
# Armazena múltiplos valores concorrentes como "siblings" (como Riak)
# Merge: união de todos os valores com versão dominada removida
# A aplicação recebe todos os siblings e decide a semântica de merge
# Ex Dynamo: carrinho com items {A,B} e carrinho com items {A,C}
# Merge → siblings: [{A,B}, {A,C}]
# Aplicação: união = {A,B,C}
# TEXTO COLABORATIVO (RGA, LSEQ, Logoot):
# Google Docs, Figma, Notion — todos usam alguma variante
# O desafio: inserir caractere na posição 5 enquanto outro insere na posição 5
# RGA (Replicated Growable Array): cada caractere tem ID único e ponteiro para anterior
# LSEQ: cada posição tem identificador fracionário (1.3.7 fica entre 1.3 e 1.4)
# Merge: os IDs únicos resolvem o conflito determinísticamente
# Qualquer ordem de entrega das operações produz o mesmo documento final
# Quando NÃO usar CRDTs:
# - Operações que precisam de coordenação (transferência de dinheiro: A - 10, B + 10)
# - Conjuntos onde o tamanho importa (inventário: não pode vender o que não tem)
# - Leituras que precisam de snapshot consistente de múltiplos valores
# Para esses casos, volte a strong consistency ou use coordenação explícita.
Garantias de sessão: tornando eventual consistency utilizável
Terry et al. (1994) definiram quatro garantias de sessão que permitem ao developer raciocinar sobre o que ele vê, mesmo em sistemas eventualmente consistentes. São ortogonais ao modelo de consistência global — um sistema pode ser AP/eventual globalmente mas oferecer garantias fortes por sessão.
# As quatro garantias de sessão (Terry et al., 1994):
# 1. READ YOUR WRITES (RYW)
# Definição: após um write completar, reads subsequentes NESSA SESSÃO
# sempre retornam o valor escrito ou algo mais recente.
# Problema resolvido: usuário cria post → recarrega página → post não aparece.
# (Read foi para replica com lag; write estava no primary)
# Implementação A — sticky session: reads da mesma sessão sempre vão ao mesmo
# nó que processou o write. Simples, mas cria hotspot e falha em failover.
# Implementação B — write token: write retorna um token (LSN/timestamp do write).
# Reads subsequentes enviam o token; servidor só serve se replicou até esse LSN.
# Se não replicou, serve do primary OU aguarda o lag.
# Implementação C — client-side clock: client rastreia o seu próprio "write_at"
# timestamp. Reads especificam "quero valor mais recente que write_at".
# Quem oferece: DynamoDB (strongly consistent reads são RYW + MR),
# Cassandra com SERIAL consistency em LWT, Cosmos DB com "Session" consistency.
# 2. MONOTONIC READS (MR)
# Definição: se você leu valor V da chave X, todas as leituras futuras
# de X retornam V ou algo mais recente. Nunca "mais antigo".
# Problema resolvido: usuário vê 100 likes → recarrega → vê 95 likes.
# (Round-robin entre replicas com lags diferentes)
# Implementação: track do LSN/timestamp da última leitura por chave.
# Reads futuros especificam "mínimo este LSN". Replica que não atingiu
# esse LSN não pode servir o request.
# Custo: o client (ou session layer) precisa manter state.
# 3. MONOTONIC WRITES (MW)
# Definição: writes de uma sessão são aplicados na ordem que foram feitos.
# Problema resolvido: banco de dados processa write B antes do write A
# porque chegaram em nós diferentes e foram replicados out-of-order.
# Implementação: sequence number por sessão. Writes carregam o número de
# sequência. Cada nó rejeita write fora de ordem (buffer até receber anterior).
# Quem precisa: sistemas de chat onde ordem das mensagens importa.
# 4. WRITES FOLLOW READS (WFR, Causal Writes)
# Definição: se você leu X=V e então escreveu Y, o write de Y é aplicado
# em nós que já viram a versão V de X (ou mais recente).
# Problema resolvido: você lê "produto está disponível" (no nó A) e então
# faz pedido (vai para nó B). Nó B ainda não viu que o produto estava disponível
# → o write (pedido) chega a nó B antes do estado que o motivou.
# Implementação: reads retornam um "read token". Writes subsequentes levam
# o token. O nó que processa o write verifica se conhece o estado referenciado.
# Mais complexo de implementar; raramente ofertado explicitamente.
# Causal consistency (nível de sistema) satisfaz WFR por design.
# COMBINAÇÕES COMUNS:
# RYW + MR: o par mais comum e mais útil. Evita o problema do "apareceu e sumiu".
# MW sozinho: sistemas de mensagens onde ordem interessa.
# Todos os quatro: causal consistency por sessão (Cosmos DB "Session" level).
# IMPLEMENTAÇÃO PRÁTICA EM CASSANDRA:
# Cassandra não oferece garantias de sessão nativas além do que o consistency
# level implica. Para RYW, o padrão é:
# - Escrever com QUORUM
# - Ler com QUORUM
# - QUORUM + QUORUM garante que reads veem writes recentes (W + R > N com N=3, W=2, R=2)
# Mas isso é mais caro que "eventual" (ONE). O tradeoff está sempre presente.
# DYNAMO / DYNAMODB:
# Consistency level por operação:
# - Eventually consistent reads: vão para qualquer replica, mais barato
# - Strongly consistent reads: vão para o leader, garantem RYW + MR
# - DynamoDB Transactions: serializable para escritas atômicas multi-item
# O desenvolvedor escolhe por operação — não é binário para o sistema inteiro.
Consistency models em sistemas reais
# CASSANDRA — tunable consistency por operação
# Consistency levels (CL):
# ONE: resposta de 1 replica → fastest, sem garantias de freshness
# TWO: resposta de 2 replicas
# QUORUM: resposta de maioria (ceil(RF/2) + 1, onde RF = replication factor)
# LOCAL_QUORUM: quorum dentro do DC local (multi-DC setup)
# ALL: resposta de todas as replicas → mais forte, qualquer falha bloqueia
# SERIAL/LOCAL_SERIAL: para Lightweight Transactions (compare-and-set)
# Com RF=3:
# Write QUORUM + Read QUORUM: W=2, R=2, W+R=4 > 3 → sempre vê write recente
# Write ONE + Read ONE: fastest, pode retornar stale data até segundos
# Write ALL + Read ONE: durável, mas write falha se qualquer replica offline
# RIAK — conflict handling explícito
# Riak expõe "siblings": múltiplas versões concorrentes do mesmo objeto.
# A aplicação recebe todos os siblings e implementa merge customizado.
# Riak Data Types (RDTs): CRDTs built-in — counters, sets, maps, flags, registers.
# Com RDTs: merge é automático e sem siblings. Sem RDTs: app lida com conflitos.
# COUCHDB / POUCHDB — sync primeiro (offline-first)
# CouchDB foi projetado para sync em ambientes offline (ex: laptops desconectados).
# Revision IDs (_rev): cada documento tem árvore de revisões. Conflitos são
# "folhas" dessa árvore. A aplicação escolhe a "vencedora" e resolve o conflito.
# PouchDB (browser) sincroniza com CouchDB: o mesmo modelo funciona para apps
# mobile/web que precisam funcionar offline.
# SPANNER (Google) — consistência externa sem abrir mão de SQL
# TrueTime + serializable transactions: strong consistency com SQL completo.
# O "truque" do Spanner (discutido no C09) é usar GPS e relógios atômicos
# para dar timestamps com margem de erro conhecida — suficiente para
# serializabilidade externa sem necessitar consenso por transação.
# Custo: latência 10-20ms mesmo em operações locais (TrueTime commit wait).
# REDIS — linearizável no single-threaded principal
# Redis core: single-threaded. Comandos são executados atomicamente.
# Linearizável por definição dentro de uma única instância.
# Redis Cluster: eventual entre réplicas de um master (async replication).
# WAIT command: permite aguardar propagação para N replicas antes de confirmar.
# RedLock: lock distribuído usando múltiplos instâncias Redis — há debate
# acadêmico se é seguro (Antirez vs Kleppmann, 2016); para uso crítico,
# preferir Redisson + fencing token ou ZooKeeper + ephemeral nodes.
# MONGODB — evoluiu de eventual para configurável
# Historicamente: write concern default era não esperava durabilidade.
# Atual: write concern majority + read concern majority = strong consistency.
# linearizable read concern: garante linearizabilidade mas com custo alto.
# Change Streams: permite reagir a writes em real-time (base de invalidação de cache).
# POSTGRES — ACID + MVCC; não é distributed por padrão
# Dentro de uma instância: serializable isolamento disponível.
# Com streaming replication: reads de réplica têm lag (eventual).
# Synchronous_commit: wait para WAL chegar na réplica antes de ack → RYW garantido.
# Caveats do serializability: predicate locking; cuidado com long-running TX.
Arquitetura comparativa: modelos de consistência lado a lado
SISTEMA CONSISTÊNCIA MECANISMO TRADEOFF
──────────────────────────────────────────────────────────────────────────
Postgres Serializable MVCC + predicate Latência baixa (local)
(single) (ACID) locking Não distribui por padrão
Spanner Ext. Serlizble TrueTime + Paxos 10-20ms min latency
(CP) por grupo GPS hardware req.
CockroachDB Serializable Raft por range 2-5ms overhead vs Postgres
(CP) + 2PC cross-range Distributed SQL
etcd Linearizable Raft + quorum Throughput ~10k ops/s
(CP) log replication Feito para config, não data
Cassandra Tunable Quorum votes Tradeoff por operação
(PA/EL default) Leaderless Sem transações multi-row
DynamoDB Eventual Leaderless + Fortemente gerenciado
(opt-in strong) vector-clock Custo RCU/WCU
Riak Eventual + Vector clocks CRDT-native (RDTs)
CRDT-native Siblings para rest Menos adotado hoje
Redis Linearizable Single-thread In-memory; cluster eventual
(instance) Async replica Não para source-of-truth
MongoDB Configurável MVCC + Raft Flexível; defaults mudaram
(up to lineariz) (desde 4.0) muito no histórico
Kafka Sequential Partition leader Ordering por partition
(por partition) + ISR replicas Consumer group coordination
──────────────────────────────────────────────────────────────────────────
SESSION GUARANTEES: o que cada sistema oferece "out of the box"
Cassandra QUORUM: RYW + MR implícito (se W+R > N)
DynamoDB strong: RYW + MR (líder serve leitura)
DynamoDB eventual: Nenhuma garantia de sessão
Cosmos DB Session: RYW + MR + MW + WFR (tudo, por sessão)
Cosmos DB Bounded: Leitura pode atrasar no máximo K versões ou T segundos
Cosmos DB Eventual: Sem garantias
Postgres (read primary): RYW + MR + MW + WFR (single-writer)
Postgres (read replica): Eventual (lag existente)
NOTA: nenhuma das garantias acima é gratuita.
Cada uma troca latência, disponibilidade, ou custo de infraestrutura.
A escolha correta depende de qual anomalia o produto tolera.
A pergunta "seu sistema é consistente ou eventualmente consistente?" é quase sempre mal formulada — implica que há dois estados binários, quando na realidade são espectros e dimensões ortogonais. A pergunta certa é: "para operação X, qual é a anomalia mais custosa: o usuário ver dados stale por Y segundos, ou pagar Z ms a mais de latência para garantia de freshness?" Para contagem de likes em um post viral: stale de 2 segundos é aceitável, salvar 5ms de latência vale muito. Para saldo de conta bancária: stale de 100ms pode custar dinheiro real, pagar 50ms de latência é barato. Para perfil de usuário após atualização: stale por 30 segundos causa confusão de UX, vale pagar RYW. Sistemas maduros definem consistência por operação — não por sistema. O artefato arquitetural que documenta isso: ADRs especificando "para endpoint X, usamos consistência Y porque anomalia Z é [aceitável/inaceitável] para o produto".
Decisões de engenharia
LWW (Last Write Wins com timestamps) é a escolha default de quase todos os bancos em modo eventual — simples, sem overhead, sem código adicional. O problema: silenciosamente perde dados em writes concorrentes. Aceitável para dados onde "o mais recente sempre substitui o antigo" (preferências de usuário, configurações, perfis). Inaceitável para dados cumulativos (contadores, carrinhos, conjuntos).
CRDTs são a escolha correta quando: a semântica do domínio cabe num dos tipos (counter, set, flag, map, register). Counters de eventos, conjuntos de itens, flags de feature — todos mapeiam naturalmente. Quando a semântica não cabe num CRDT padrão, a alternativa é expor siblings (conflitos) para a aplicação resolver, como Riak faz.
Regra prática: use LWW para entidades onde "última escrita ganha" é a semântica correta do produto. Use G-Counter/PN-Counter para métricas e contagens. Use OR-Set para carrinhos, tags, memberships. Use MV-Register + merge customizado para objetos complexos onde a semântica de merge é específica do domínio. Evite CRDTs quando há invariantes que precisam ser preservadas globalmente (ex: saldo nunca negativo).
Em Cassandra (RF=3), a escolha de consistency level é por operação. Write QUORUM + Read QUORUM garante que leituras sempre veem o write mais recente (W+R=4 > N=3). Esse é o ponto de partida seguro para qualquer dado onde freshness importa. Write ONE + Read ONE é para dados onde alguns segundos de stale são aceitáveis e throughput é mais importante que freshness — logs, métricas, telemetria. Write ALL é raramente correto — qualquer nó offline bloqueia writes; só faz sentido para dados ultra-críticos com RF alto.
Regra prática: definir consistency level explicitamente por query, não usar o default do driver. Para dados financeiros ou de estado crítico: QUORUM/QUORUM. Para contadores de eventos e métricas: ONE/ONE com CRDT (counter). Para dados de usuário autenticado: LOCAL_QUORUM (multi-DC) ou QUORUM. Nunca usar SERIAL/LOCAL_SERIAL em hot paths — é ~3-5× mais caro por envolver Paxos leve.
A maioria dos problemas de "eventual consistency na prática" não é do banco — é de não implementar session guarantees no application layer. Read-your-writes é o mais crítico: resolver com sticky reads (após write, próximas N leituras vão ao primary/coordinator) ou write tokens (write retorna LSN, reads aguardam replica atingir esse LSN). Monotonic reads: rastrear no cliente o maior timestamp visto; enviar em reads subsequentes.
Regra prática: adicionar um "consistency layer" no gateway ou service layer. Após write, armazenar na sessão do usuário: session.last_write = {entity_id, timestamp}. Reads que batem nessa entidade dentro de X segundos são roteados ao primary. Simples, sem mudança no banco, resolve 90% das reclamações de "vi e desapareceu". Para sistemas críticos, usar Cosmos DB "Session" consistency que oferece RYW+MR nativamente por sessão.
Eventual consistency é tentador por ser barato e escalável, mas há operações onde o custo de anomalias é maior que o custo de consistência forte. Inventário: vender o último item para dois compradores simultaneously é catastrophic — use compare-and-set (LWT em Cassandra, conditional writes em DynamoDB, SELECT FOR UPDATE em Postgres). Saldo financeiro: saldo negativo por divergência de réplicas pode ser fraude ou prejuízo real. Deduplicação: processar o mesmo evento duas vezes pode ter efeitos irreversíveis (email enviado duas vezes, cobrança duplicada).
Regra prática: para operações com invariantes de negócio (não pode ser negativo, não pode duplicar, não pode vender o que não tem), use consistência forte ou coordenação explícita — mesmo que seja mais lento. O custo de "fazer errado" é muito maior que o custo de latência adicional. Eventual consistency é a escolha certa para reads frequentes com tolerância a stale; errada para writes com invariantes de negócio críticas.
Perguntas de entrevista
O que é eventual consistency na prática? O que "eventual" garante — e o que não garante?
Eventual consistency é frequentemente mal entendida como "dados podem estar errados por algum tempo". A definição correta é mais precisa: se não ocorrerem novos writes, eventualmente todos os nós convergem para o mesmo valor. O que essa definição garante — e o que não garante — é crucial.
O que garante: convergência. Dado tempo suficiente sem writes, todos os nós terão o mesmo estado. Isso é uma propriedade matemática dos sistemas que implementam eventual consistency corretamente (particularmente os que usam CRDTs ou monotônica reconciliação).
O que NÃO garante: (1) Quanto tempo é "eventual" — pode ser milissegundos (replicação local) ou segundos (replicação cross-DC); (2) O que acontece durante o período de divergência — reads podem retornar valores diferentes de réplicas diferentes; (3) Qual versão "ganha" quando há conflito — depende da estratégia (LWW, merge, CRDT); (4) Que você verá seus próprios writes — RYW é uma garantia separada que precisa ser implementada explicitamente.
Variantes importantes: "Strong eventual consistency" (Shapiro et al., 2011) é mais forte: garante que se dois nós receberam o mesmo conjunto de updates (em qualquer ordem), eles terão o mesmo estado — sem coordenação adicional. CRDTs satisfazem strong eventual consistency por design.
Na prática: sistemas que afirmam ser "eventualmente consistentes" têm comportamentos muito diferentes. DynamoDB eventual reads podem ter lag de milissegundos a segundos. Cassandra com ONE consistency pode servir dados de segundos atrás. Riak com siblings pode expor conflitos para a aplicação resolver. Não basta saber "eventual consistency" — é preciso saber qual o lag típico, qual a estratégia de resolução de conflito, e quais session guarantees são oferecidas.
Como você implementa read-your-writes em um sistema com múltiplas replicas e sem mudar o banco?
Read-your-writes (RYW) é a garantia de que após você escrever algo, você sempre lê sua própria escrita — mesmo que o sistema seja eventualmente consistente globalmente. Há várias formas de implementar sem mudança no banco.
Opção 1 — Sticky reads (simples, mas limitada): após um write bem-sucedido, marcar na sessão do usuário wrote_at = now(). Por X segundos após o write (ex: 30s, maior que o lag máximo esperado), todos os reads desse usuário são roteados ao primary/coordinator — não às replicas. Depois do timeout, volta ao comportamento normal. Implementação: middleware de sessão no API gateway. Limitação: aumenta carga no primary; falha se o primary cair durante o período.
Opção 2 — Write tokens com LSN (mais robusta): quando o write completa, o banco (ou a aplicação) retorna o Log Sequence Number (LSN) ou timestamp do write. A sessão do usuário armazena este token. Reads subsequentes enviam o token no header. O servidor de aplicação: (a) verifica se a replica já aplicou até aquele LSN; (b) se sim, pode servir da replica; (c) se não, serve do primary. Em Postgres: pg_current_wal_lsn() para obter o LSN do write; replicas expõem pg_last_wal_replay_lsn(). Comparar antes de rotear.
Opção 3 — Optimistic UI com eventual catch-up: após o write retornar sucesso, o client atualiza a UI localmente sem recarregar do servidor. A "próxima" leitura pode ser eventual — o usuário já viu o dado na UI (otimisticamente). Se o servidor retornar valor diferente, UI reconcilia silenciosamente. Resolve a percepção de "apareceu e sumiu" mesmo sem garantia técnica de RYW. Twitter, Facebook usam isso extensivamente.
Opção 4 — Cache de writes recentes (client-side): após o write, armazenar o valor escrito em cache local (localStorage, Redis por usuário) com TTL curto (ex: 60s). Reads verificam o cache primeiro. Se cache tem versão mais recente (baseado em timestamp), retorna do cache. Quando o TTL expira, leitura normal. Mais simples que LSN, funciona client-side, mas não protege contra outros clients do mesmo usuário.
Qual escolher: para a maioria das aplicações web, Opção 1 (sticky reads por 5-30s) + Opção 3 (optimistic UI) resolve 95% dos casos de forma simples. Para sistemas financeiros ou onde a precisão é crítica, Opção 2 (LSN-based) é mais robusta. Opção 4 é boa para aplicações mobile onde a latência de rede é alta.
O que são CRDTs e quando você os usaria em vez de strong consistency?
CRDT (Conflict-free Replicated Data Type) é uma estrutura de dados cujas operações de merge são comutativas, associativas e idempotentes — o que garante que qualquer conjunto de updates, aplicado em qualquer ordem a qualquer subconjunto de réplicas, converge para o mesmo estado final sem coordenação. Não há "conflitos" porque a semântica define como resolver qualquer divergência.
Quando usar CRDTs em vez de strong consistency:
1. Contadores distribuídos: contador de visualizações de vídeo, likes, retweets. Strong consistency para contar 1M likes/s em YouTube é proibitivamente caro (requer coordenação por operação). G-Counter CRDT permite cada DC incrementar localmente; o valor global é a soma — convergente, sem coordenação, sem conflito. Margem de erro de segundos é aceitável para "7.3M vs 7.3M+1" visualmente.
2. Presença e memberships: quais usuários estão online? Quais tags um post tem? OR-Set CRDT garante que adds e removes convergem corretamente, mesmo concorrentes. Strong consistency para "está online?" requer quorum para cada heartbeat — impossível em escala.
3. Editors colaborativos: Google Docs, Figma, Notion. Múltiplos usuários editando simultaneamente. Strong consistency requer lock por cursor — não escala, mata a experiência colaborativa. CRDTs de texto (RGA, LSEQ) permitem edição concorrente que converge deterministicamente sem lock.
4. Configurações distribuídas com feature flags: cada DC tem sua cópia do mapa de configuração. Updates propagam de forma eventual. Map CRDT garante que todos os DCs convergem para o mesmo estado de configuração.
Quando NÃO usar CRDTs: quando há invariantes de negócio que precisam ser globalmente preservadas. "Saldo não pode ser negativo" — PN-Counter pode resultar em negativo durante divergência. "Só pode vender até o estoque" — OR-Set não pode impedir duas réplicas de "adicionar ao carrinho" o último item simultaneamente. Para esses casos, strong consistency ou coordenação explícita é necessária — o custo extra de latência é justificado pelo custo do erro.
Sistemas que usam CRDTs em produção: Redis Enterprise (CRDT cluster), Riak (Data Types), Akka Distributed Data, Azure Cosmos DB (alguns tipos), Figma (texto colaborativo), Notion (blocks).
Como você projeta um sistema de carrinhos de compra que funciona mesmo durante partições de rede?
O carrinho de compras da Amazon é o exemplo histórico de design para disponibilidade sobre consistência. A filosofia original (paper Dynamo, 2007): "é melhor ter dois itens duplicados no carrinho do que perder um item". O carrinho nunca pode falhar na operação de adicionar um item — mesmo se metade dos servidores estiver offline.
Design com MV-Register (estilo Dynamo original):
Cada write ao carrinho é armazenado com um vector clock. Em caso de partição, dois DCs aceitam writes independentemente, criando versões divergentes (siblings). Quando a partição se cura e os DCs sincronizam, o sistema detecta as versões concorrentes (via vector clock comparison) e faz merge: a versão final do carrinho é a UNIÃO de todos os items em todos os siblings.
Consequência: se o usuário removeu item X em um DC durante a partição, e o item X foi re-adicionado em outro DC, após merge o item X volta ao carrinho. Isso é uma escolha de produto explícita: "preferimos mostrar item que o usuário pode não querer a não mostrar item que ele queria".
Design moderno com OR-Set CRDT:
Modelar o carrinho como OR-Set onde cada ADD cria uma tag única. Remove elimina tags específicas. Merge é a união de tags. Resultado: se A adicionou item X (tag T1) e B removeu item X (eliminou tags conhecidas — T1 ainda não chegou em B), após merge tag T1 existe → item X aparece. Semântica add-wins.
Se você quer remove-wins: usar LWW-Element-Set com timestamp. O add e remove mais recente por timestamp determina presença.
Para inventory enforcement (não vender o que não tem):
O carrinho pode ser eventual; o checkout NÃO pode. O fluxo: (1) usuário adiciona ao carrinho — eventual consistency, OR-Set ou LWW; (2) no checkout, fazer reserva de inventory com strong consistency (SELECT FOR UPDATE ou conditional write em DynamoDB); (3) se reserva falha (item esgotado), mostrar erro no checkout; (4) pagamento completou → confirmar reserva; (5) timeout de reserva (ex: 15 min) → libera para outros.
O checkout é o único ponto de strong consistency; tudo antes é eventual. Isso dá a experiência de "você pode adicionar ao carrinho sempre" enquanto ainda previne overselling.
Session guarantees para UX: após adicionar ao carrinho, implementar RYW para que a lista do carrinho reflita imediatamente o item adicionado (sticky reads por 10s ou optimistic UI update). O usuário nunca vê "adicionei e não apareceu".
Você tem um sistema com Cassandra e os usuários reclamam de inconsistência. Como você diagnostica e resolve?
Reclamações de inconsistência em Cassandra geralmente se enquadram em três categorias: dados stale (lag de replicação), dados perdidos (LWW com clock skew), ou dados conflitantes (writes concorrentes mal resolvidos). Cada um tem diagnóstico diferente.
Passo 1 — Identificar o tipo de anomalia:
Coletar exemplos concretos: "escrevi X e li Y", "escrevi mas não encontrei", "encontrei valor antigo". Verificar o timestamp dos dados via SELECT ... USING TIMESTAMP em CQL ou cassandra-cli com get. Comparar timestamps das réplicas: nodetool getendpoints keyspace table pk para saber quais nós têm o dado.
Cenário A — Dados stale (leu replica com lag):
Sintoma: usuário escreve e imediatamente lê o valor antigo. Causa: write com QUORUM mas read com ONE, e o nó que serviu o read ainda não recebeu o write. Diagnóstico: verificar nodetool tpstats e nodetool compactionstats; verificar replication lag com nodetool netstats. Fix de curto prazo: aumentar CL de reads para QUORUM. Fix de longo prazo: adicionar RYW na camada de aplicação (sticky reads ou write tokens).
Cenário B — Dados perdidos (clock skew + LWW):
Sintoma: escrita aconteceu mas dado sumiu. Causa: outro write com timestamp maior (relógio adiantado) sobrescreveu silenciosamente. Diagnóstico: verificar drift de relógio nos nós com ntpq -p ou chronyc tracking; verificar se o drift está dentro de ~100ms (Cassandra recommendation). Fix: sincronizar relógios (NTP/chrony), monitorar drift continuamente, e para operações críticas usar Lightweight Transactions (LWT) com INSERT IF NOT EXISTS ou UPDATE IF — LWT usa Paxos e é imune a clock skew.
Cenário C — Conflitos de writes concorrentes:
Sintoma: dois writes simultâneos para mesma chave; um é perdido. Diagnóstico: revisar o modelo de dados — se duas fontes podem escrever a mesma partition key simultaneamente, há um problema de design. Fix: usar LWT para writes que precisam de compare-and-swap (UPDATE t SET x=new WHERE pk=? IF x=old). Ou redesenhar para single-writer (um processo por partition key). Ou usar CRDTs do Cassandra: Cassandra 3.x+ tem Counter type (PN-Counter) nativo.
Passo final — Monitoramento preventivo:
Adicionar métricas explícitas de consistência: comparar o que foi escrito vs o que foi lido (sampling de 1% das operações); alertar em inconsistências detectadas; monitorar read repair rate (alta taxa indica replicas frequentemente divergentes — pode significar problema de hardware ou compaction atrasada).
Exercícios práticos
Implemente G-Counter (grow-only) e PN-Counter (incremento/decremento) como CRDTs state-based. Para G-Counter: cada nó tem um array de N inteiros. Increment incrementa apenas o índice do nó local. Value = sum. Merge = max element-wise. Para PN-Counter: dois G-Counters (P e N). Value = P.value() - N.value(). Simular: 3 nós fazendo incrementos/decrementos independentes, depois merge. Verificar que o valor final é correto independentemente da ordem das mensagens de merge.
Critério: G-Counter converge para a soma correta de todos os incrementos, independente da ordem de merge. PN-Counter converge para a diferença correta. Provar comutatividade: merge(A, B) == merge(B, A). Provar idempotência: merge(A, A) == A. Bonus: implementar CmRDT (operation-based): cada operação increment() é enviada como mensagem; no recebimento, aplicar increment local. Mostrar que ambas as implementações (state-based e op-based) convergem para o mesmo resultado.
Implemente vector clocks para 3 processos (P1, P2, P3). Operações: increment (evento local), send(dest, msg), receive(src, msg). Cada evento retorna o vector clock no momento do evento. Implementar comparação: is_concurrent(vc1, vc2), happened_before(vc1, vc2). Simular: (1) P1 escreve X → envia para P2; (2) P2 lê X → escreve Y; (3) P3 escreve Z independentemente; (4) P1 e P3 se comunicam. Verificar: X happened-before Y ✓; Z concurrent com X ✓; Z concurrent com Y ✓ (antes de P3 receber de P2).
Critério: comparações de causalidade corretas para todos os pares de eventos no cenário. Provar: se A happened_before B, então B não happened_before A. Detectar todos os pares concorrentes corretamente. Bonus: implementar "plausible clocks" — truncar o vetor para os K eventos mais recentes e avaliar o impacto na precisão da detecção de causalidade.
Implemente OR-Set e use para modelar um carrinho de compras distribuído. Operações: add(item) cria tag UUID único; remove(item) remove todas as tags conhecidas do item; contains(item); merge(other_set). Simular o cenário clássico: (1) Node A adiciona "maçã" (tag T1); (2) Partição de rede; (3) Node B remove "maçã" (não sabe de T1, remove tags que conhece = nenhuma); (4) Node A adiciona "banana"; (5) Partição se cura → merge. Verificar que "maçã" está presente (add-wins), "banana" está presente, e o merge é determinístico.
Critério: merge é comutativo (A.merge(B) == B.merge(A)). Add-wins em conflito: item adicionado por A durante partição persiste após merge com B que fez remove. Remove de item que nunca foi adicionado não causa erro. Bonus: implementar versão remove-wins usando LWW timestamps (o add e remove com maior timestamp determina presença). Comparar os dois em casos de uso reais — quando cada semântica faz sentido?
Construa um sistema mock com um "primary" e duas "replicas" com lag simulado. O primary aceita writes e atribui um LSN (monotônico). Replicas atualizam seu LSN com delay aleatório de 0-500ms. Implementar write token: após write, cliente recebe o LSN. Reads subsequentes: se replica.current_lsn >= token_lsn, pode servir; senão, redireciona para o primary. Simular: escrever X, ler X imediatamente (deve ver X), ler X 1s depois (pode ser de replica). Medir: % de reads que vão ao primary em função do delay de replicação.
Critério: RYW garantido — após write de X, leitura com token SEMPRE retorna X (ou mais recente). Sem token: reads podem retornar valor stale. Medir: com lag de 100ms, quanto % dos reads nas primeiras 200ms vai ao primary vs replica. Bonus: implementar monotonic reads — o cliente rastreia o maior LSN visto; reads futuros especificam esse LSN como mínimo, garantindo que o valor nunca "regride".
Simule um cenário de conflito real: dois clientes (C1 e C2) escrevem para a mesma chave simultaneamente em dois nós diferentes, com comunicação assíncrona. Implemente dois resolvers: (a) LWW com timestamp de sistema (wall clock): o write com maior timestamp vence; (b) G-Counter CRDT: incrementos de ambos os lados são preservados. Usar a chave como "contador de likes" de um post. Executar 1000 rounds de writes concorrentes e medir: quantas operações são perdidas em LWW vs CRDT. Introduzir drift de clock (±50ms) e medir o impacto.
Critério: LWW perde operações em proporção ao nível de concorrência e clock drift. CRDT perde zero operações — o valor final é sempre a soma de todos os incrementos de ambos os lados. Quantificar: com 10% de writes concorrentes e 50ms de clock drift, LWW perde X% das operações. Bonus: implementar um terceiro resolver — "last writer wins with fencing token" onde o token é gerado por um sequencer central. Comparar performance e durabilidade dos três.
Referências
- article Vogels, W. — Eventually Consistent
- paper Shapiro et al. — A Comprehensive Study of Convergent and Commutative Replicated Data Types
- paper Lamport, L. — Time, Clocks, and the Ordering of Events in a Distributed System
- paper Terry et al. — Session Guarantees for Weakly Consistent Replicated Data
- paper DeCandia et al. — Dynamo: Amazon's Highly Available Key-value Store
- book Kleppmann, M. — Designing Data-Intensive Applications
- article Kleppmann, M. vs Antirez — Is Redlock Safe?
- docs Apache Cassandra — Consistency Levels
- docs Amazon DynamoDB — Read/Write Capacity and Consistency
- article Bailis, P. & Ghodsi, A. — Eventual Consistency Today: Limitations, Extensions, and Beyond
- paper Bailis et al. — Highly Available Transactions: Virtues and Limitations
- article Kingsbury, K. — Jepsen Analysis Series