# Notion: O Modelo de Blocos e o Sharding do Postgres

O Notion cresceu de um banco Postgres monolítico para uma arquitetura shardada horizontalmente, particionando dados por tenant para sustentar crescimento explosivo durante a pandemia. Este teardown reconstrói as decisões de modelagem, o processo de migração e os trade-offs reais de operar Postgres em escala.

- URL: https://fernando.moretes.com/studies/notion-sharding-postgres

- Markdown: https://fernando.moretes.com/studies/notion-sharding-postgres/study.md?lang=pt

- Type: Teardown

- Company: Notion

- Domain: Dados

- Date: 2021-10-05

- Tags: postgres, sharding, data-modeling, scalability, migration, multi-tenant, block-model, databases

- Reading time: 6 min

---

Em 2020, o Notion virou produto de massa. O que era um banco Postgres único e bem comportado começou a mostrar sinais sérios de estresse — conexões esgotadas, latências imprevisíveis, e uma tabela central de 'blocos' crescendo a um ritmo que tornava qualquer operação de manutenção um evento de risco. A resposta foi um sharding lógico por tenant, executado sem downtime sobre infraestrutura existente. Este teardown reconstrói a arquitetura, avalia as decisões e diz o que eu faria diferente.

## Ficha Técnica

- **Empresa:** Notion Labs, Inc.
- **Domínio:** Produtividade / Colaboração / SaaS
- **Período crítico:** 2020–2021 (crescimento pandêmico)
- **Stack de banco de dados:** PostgreSQL (RDS / instâncias gerenciadas), PgBouncer
- **Modelo de dados central:** Tabela universal de blocos (block) — tudo é um bloco
- **Estratégia de sharding:** Sharding lógico por workspace_id (tenant), depois físico
- **Impacto declarado:** Eliminação de gargalos de conexão, redução de latência p99, capacidade de escalar horizontalmente sem redesenho de aplicação
- **Migração:** Zero downtime, dupla escrita + backfill + cutover gradual

## O Problema: Tudo É Um Bloco, e Isso Tem Consequências

A decisão de modelagem mais fundamental do Notion é também a mais arriscada do ponto de vista de banco de dados: **tudo é um bloco**. Páginas, parágrafos, imagens, tabelas, bullet points, bancos de dados embutidos — todos são linhas na mesma tabela `block`. Cada bloco tem um `id` UUID, um `type`, um campo `properties` JSONB com conteúdo variável, e ponteiros para `parent_id` e `space_id` (o workspace). A hierarquia do documento inteiro é uma árvore de adjacência dentro dessa tabela única.

Essa escolha tem elegância real. O modelo de dados é uniforme, o código da aplicação lida com um único tipo de entidade, e funcionalidades como mover um bloco entre páginas ou tipos são triviais — você só atualiza o `parent_id`. Não há migrações de schema para novos tipos de conteúdo; você apenas adiciona um novo valor de `type` e popula `properties` de forma diferente.

Mas a elegância tem um custo direto em escala. Uma única tabela acumulando **todos os conteúdos de todos os usuários** cria uma superfície de contenção enorme. Índices crescem sem controle. O `autovacuum` do Postgres — o processo de fundo que recupera espaço de linhas mortas e atualiza estatísticas — começa a lutar contra o volume de writes. Operações DDL (como adicionar um índice) bloqueiam ou levam horas. E o mais crítico: o Postgres não tem sharding nativo. Toda a carga vai para uma instância primária, e escalar verticalmente tem um teto claro — tanto em custo quanto em latência de replicação.

## A Arquitetura Antes do Sharding: Um Monólito Sob Pressão

Antes da migração, o Notion operava com uma arquitetura relativamente convencional para um SaaS em crescimento: uma aplicação Node.js/TypeScript conectada a um Postgres primário via PgBouncer para pooling de conexões, com read replicas para queries de leitura intensiva. O PgBouncer era essencial — Postgres tem um custo não trivial por conexão (memória, contexto de processo), e aplicações modernas com dezenas de instâncias de servidor facilmente saturam o banco com milhares de conexões simultâneas.

O problema de conexões foi o primeiro sinal visível de crise. Com crescimento acelerado em 2020, o número de instâncias de aplicação aumentou, e mesmo com PgBouncer, o banco primário começou a atingir limites. Mas o problema mais profundo era de **throughput e latência de escrita**. A tabela `block` recebia writes de altíssima frequência — cada keystroke de cada usuário pode gerar uma ou mais linhas novas ou atualizações. Com milhões de usuários ativos, isso se traduz em dezenas de milhares de writes por segundo, todos disputando locks na mesma tabela, nos mesmos índices.

O autovacuum é um ponto que merece atenção especial. No Postgres, quando você atualiza uma linha, a versão antiga não é deletada imediatamente — ela fica como 'dead tuple' até o vacuum passar. Em tabelas de alta escrita, o acúmulo de dead tuples degrada performance de leitura (os índices ficam inchados, os sequential scans ficam mais lentos) e pode, em casos extremos, causar **transaction ID wraparound** — um evento catastrófico que força o banco a parar de aceitar writes até que o vacuum complete. O Notion claramente estava se aproximando de condições onde o autovacuum não conseguia manter o ritmo.

## Arquitetura Reconstruída: Antes e Depois do Sharding

Diagrama reconstruído com base no post oficial do Notion. Mostra o fluxo de dados do modelo de blocos, a camada de roteamento de shards, e a distribuição por tenant.

### 👤 Clients

- Notion Client (Web / Desktop / Mobile) (user)

### ⚙️ Application Layer

- API Server (Node.js / TypeScript) (compute)
- Shard Router (workspace_id → shard) (compute)

### 🔌 Connection Layer

- PgBouncer Shard 0 (network)
- PgBouncer Shard 1 (network)
- PgBouncer Shard N (network)

### 🗄️ Physical Shards (Postgres)

- Postgres Primary Shard 0 (workspaces 0..k) (data)
- Postgres Replica Shard 0 (data)
- Postgres Primary Shard 1 (workspaces k+1..m) (data)
- Postgres Replica Shard 1 (data)
- Postgres Primary Shard N (data)
- Postgres Replica Shard N (data)

### 🗺️ Shard Mapping

- Shard Map (workspace_id → shard_id) Postgres / in-memory cache (data)

### Fluxos

- client -> api: requisição
- api -> router: workspace_id
- router -> shardmap: lookup
- router -> pgb1: shard 0
- router -> pgb2: shard 1
- router -> pgbn: shard N
- pgb1 -> pg0
- pgb2 -> pg1
- pgbn -> pgn
- pg0 -> pg0r: replicação
- pg1 -> pg1r: replicação
- pgn -> pgnr: replicação

## Como Funciona: Sharding Lógico Primeiro, Físico Depois

A estratégia do Notion foi inteligente na sequência: **primeiro introduzir sharding lógico sem mover dados, depois migrar fisicamente**. Isso é o padrão correto para migrações de banco de dados em produção — você separa o problema de modelagem do problema de migração.

**Fase 1 — Sharding Lógico:** A aplicação passa a tratar cada `workspace_id` como pertencente a um shard lógico. Um mapa de shards (uma tabela simples, cacheada em memória) mapeia `workspace_id → shard_id`. Toda query passa pelo shard router, que resolve o shard correto antes de abrir conexão. Nesse ponto, todos os shards lógicos ainda apontam para o mesmo banco físico — o comportamento muda, mas os dados não se movem. Isso permite testar o roteamento em produção sem risco de perda de dados.

**Fase 2 — Migração Física com Dupla Escrita:** Para mover workspaces para novos shards físicos, o Notion usou o padrão clássico de dupla escrita (dual-write) com backfill. O processo: (a) começar a escrever em ambos os bancos (origem e destino) para novos eventos; (b) backfill dos dados históricos no destino; (c) verificar consistência; (d) redirecionar leituras para o destino; (e) parar de escrever na origem. Cada etapa é reversível. O risco de cada cutover é limitado ao conjunto de workspaces sendo migrados, não ao sistema inteiro.

**A chave da escolha de `workspace_id` como shard key:** Toda query do Notion inclui o contexto do workspace — um usuário sempre está operando dentro de um workspace específico. Isso significa que o shard key está naturalmente presente em todas as operações, e **não há queries cross-shard no caminho crítico**. Isso é fundamental: sharding só funciona bem quando você consegue garantir que a grande maioria das queries toca um único shard. O modelo de blocos do Notion, com sua hierarquia contida dentro de um workspace, torna isso natural.

O que **não** é resolvido pelo sharding: queries analíticas que precisam agregar dados de múltiplos workspaces (ex: métricas de produto, dashboards internos) agora requerem fan-out para todos os shards ou uma pipeline de dados separada. Esse é o trade-off aceito — você otimiza para o caminho crítico transacional e aceita complexidade no analítico.

## Matriz de Decisões: Alternativas ao Sharding por Tenant

### Sharding por workspace_id (escolha do Notion)

**Pros**
- Shard key sempre presente nas queries — zero cross-shard no caminho crítico
- Isolamento natural de tenant — workspaces grandes não impactam pequenos
- Migração incremental possível (workspace a workspace)
- Compatível com o modelo mental da aplicação

**Cons**
- Workspaces muito grandes (hot tenants) ainda podem sobrecarregar um shard
- Queries analíticas cross-workspace requerem fan-out ou pipeline separada
- Rebalanceamento de shards é operacionalmente complexo

**Verdict:** Decisão correta dado o modelo de dados e padrões de acesso

### Escala vertical (upgrade de instância)

**Pros**
- Zero mudança de código ou arquitetura
- Simples operacionalmente no curto prazo

**Cons**
- Teto físico de hardware — não escala indefinidamente
- Custo cresce de forma não linear com o tamanho da instância
- Não resolve contenção de locks ou autovacuum lag
- Adiou o problema, não o resolve

**Verdict:** Paliativo — adequado como medida temporária, não como estratégia

### Migrar para banco distribuído (Vitess, CockroachDB, Spanner)

**Pros**
- Sharding gerenciado pela plataforma, não pela aplicação
- Rebalanceamento automático de dados
- Escala horizontal nativa

**Cons**
- Migração de banco em produção é de altíssimo risco
- Semântica SQL pode diferir — requer validação extensa
- Curva de aprendizado operacional significativa
- Custo de licença ou operacional potencialmente alto

**Verdict:** Válido para greenfield ou com muito mais tempo disponível; risco excessivo em crise

### Read replicas + caching agressivo

**Pros**
- Reduz carga de leitura no primário
- Relativamente simples de implementar

**Cons**
- Não resolve o problema de writes — o gargalo principal
- Cache invalidation é complexo em modelo colaborativo em tempo real
- Replication lag pode causar leituras inconsistentes

**Verdict:** Complementar, não substituto — não endereça o problema fundamental

## Leitura Well-Architected

- **security**: **Adequado, com ressalvas.** O isolamento por tenant no nível de shard é uma melhoria de segurança real — um vazamento de dados em um shard não expõe automaticamente todos os workspaces. Porém, o shard map em si é um componente crítico de segurança: se comprometido ou corrompido, queries podem ser roteadas para o shard errado, expondo dados de outro tenant. O shard map precisa de controles de acesso rígidos, auditoria e validação de integridade. Não há evidência pública de como o Notion trata isso.
- **reliability**: **Forte.** A migração incremental por workspace minimizou o blast radius de cada operação. A dupla escrita com verificação de consistência antes do cutover é o padrão correto. Cada shard tem réplica, mantendo RTO/RPO razoáveis. O risco residual é o hot tenant — um workspace enterprise muito grande ainda pode degradar um shard inteiro. Mitigação recomendada: circuit breakers por workspace e monitoramento de shard imbalance.
- **performance**: **Forte para o caminho transacional.** Distribuir writes por múltiplos primários resolve o gargalo central. Cada shard tem uma fração da carga de autovacuum, tornando a manutenção gerenciável. O PgBouncer por shard mantém o pooling eficiente. O ponto fraco é o overhead de latência do lookup no shard map — mitigado por cache em memória, mas que precisa de invalidação cuidadosa quando workspaces são migrados entre shards.
- **cost**: **Trade-off consciente.** Múltiplas instâncias Postgres (primário + réplica por shard) multiplicam o custo de infraestrutura. Porém, instâncias menores e mais especializadas são geralmente mais eficientes em custo/performance do que uma única instância monstruosa. O custo operacional de gerenciar N shards é real — mais superfície para monitoramento, backups, upgrades de versão. Esse custo operacional é frequentemente subestimado.
- **sustainability**: **Neutro.** Instâncias menores e mais eficientes podem ser mais sustentáveis do que uma instância superdimensionada. Porém, a multiplicação de instâncias idle (shards com pouca carga) pode desperdiçar recursos. Auto-scaling de instâncias Postgres ainda é limitado — você não liga e desliga primários facilmente.

> **O Que Eu Faria Diferente:** A decisão de sharding por `workspace_id` é correta — não mudaria isso. O que eu questionaria e reforçaria são três pontos específicos:

**1. Shard map como componente de primeira classe.** O post do Notion trata o shard map quase como detalhe de implementação, mas ele é o componente mais crítico da arquitetura. Uma corrupção silenciosa no shard map pode rotear writes de um tenant para o banco de outro — um incidente de segurança e consistência de dados ao mesmo tempo. Eu trataria o shard map com o mesmo rigor que um serviço de autenticação: versionamento de esquema explícito, auditoria de todas as mutações, checksums de integridade, e testes de chaos que simulam corrupção do mapa.

**2. Estratégia explícita para hot tenants.** O sharding por tenant resolve o problema de escala geral, mas não resolve o problema do tenant gigante — uma empresa Fortune 500 com 50.000 usuários ativos em um único workspace. Esse workspace vai inevitavelmente dominar o shard em que está. A solução é ter uma estratégia explícita: identificar tenants acima de um threshold de escrita, e para esses, implementar sharding intra-tenant (por exemplo, por `block_id` range ou por sub-espaço). Isso é complexo, mas necessário para um produto que serve enterprise.

**3. Pipeline analítica desde o início.** O sharding transacional e a análise de dados são problemas fundamentalmente diferentes. Eu estabeleceria desde o início uma pipeline de CDC (Change Data Capture) — por exemplo, com Debezium ou pglogical — que captura todos os eventos de todos os shards e os materializa em um data warehouse (Snowflake, BigQu

## Veredicto

O Notion fez a coisa certa na ordem certa: identificou o shard key correto (workspace_id), introduziu sharding lógico antes de mover dados, e executou a migração física de forma incremental e reversível. Isso é engenharia de banco de dados madura — não é glamouroso, mas é o que separa sistemas que sobrevivem ao crescimento dos que colapsam sob ele.

O modelo de blocos é uma aposta de modelagem de dados que funcionou para o produto mas criou pressão estrutural no banco. A elegância de 'tudo é um bloco' tem um preço que só aparece em escala — e o Notion pagou esse preço com complexidade operacional, não com redesenho do produto. Essa é uma troca razoável.

O que fica como lição central: **a escolha do shard key é irreversível na prática**. Uma vez que você tem milhões de workspaces distribuídos por shards, mudar a estratégia de particionamento é uma migração de dados de escala total. O Notion acertou nessa escolha porque o modelo de dados do produto (tudo contido dentro de um workspace) e o padrão de acesso (usuários operam dentro de um workspace por vez) se alinharam perfeitamente com o shard key. Quando você está projetando um sistema multi-tenant, a pergunta 'qual é o meu shard ke

## Referências

- [Notion — Sharding Postgres at Notion (Official Blog Post)](https://www.notion.so/blog/sharding-postgres-at-notion)
- [PostgreSQL Documentation — Autovacuum](https://www.postgresql.org/docs/current/routine-vacuuming.html)
- [PgBouncer — Connection Pooler for PostgreSQL](https://www.pgbouncer.org/)
- [AWS Blog — Sharding with Amazon RDS for PostgreSQL](https://aws.amazon.com/blogs/database/sharding-with-amazon-relational-database-service/)
- [Designing Data-Intensive Applications — Martin Kleppmann (Partitioning chapter)](https://dataintensive.net/)
- [Debezium — CDC for PostgreSQL](https://debezium.io/documentation/reference/connectors/postgresql.html)

## Fontes do caso

- [Notion — Sharding Postgres at Notion](https://www.notion.so/blog/sharding-postgres-at-notion)
