Caso 6 · Collaboration

Google Docs: edición en tiempo real

Múltiples usuarios editan el mismo documento al mismo tiempo, con presence, cursores remotos, historial y sin pisar trabajo.

Recursos

Document, Operation, Snapshot, Permission, Presence.

Operaciones

Editar, sincronizar, versionar, compartir, ver historial.

Escala

Miles de operaciones por documento por minuto en sesiones activas.

Riesgo

Lost updates, conflictos, permisos rotos, audit débil.

Requisitos

Lo que distingue un editor colaborativo de un CRUD: concurrencia y trazabilidad.

Funcionales

  • Edición concurrente en vivo entre N usuarios.
  • Cursores remotos y avatares (presence).
  • Comentarios y suggestions con resolución.
  • Permisos granulares (viewer/commenter/editor).
  • Historial de versiones y revert.

No funcionales

  • Latencia op < 100 ms entre clientes.
  • Strong eventual consistency (OT / CRDT).
  • Disponibilidad 99.99%.
  • Durabilidad de cada op (append-only log).
  • Modo offline con resync automático.

Cómo aclarar requisitos en la entrevista

Casos de uso típicos. Reunión de equipo con 50 personas tomando notas, doc personal con 1 editor (la mayoría), code review en doc, encuesta interna estilo form, template legal con tracked changes, doc compartido por link público (read-only), copy collaborativa con stakeholders externos, edición offline durante vuelo y resync al aterrizar.

Preguntas que debes hacer:

• ¿Solo texto o también imágenes, tablas, sheets, slides? Cambia el modelo de datos drásticamente.
• ¿Soporte offline con resync automático?
• ¿Audit log granular: quién cambió qué y cuándo?
• ¿Track changes / suggesting mode con accept/reject?
• ¿Comentarios threaded con replies?
• ¿Embeds: drawings, charts, otros docs?
• ¿Public link con auth opcional o solo invitados?
• ¿Cuántos editores concurrentes deben funcionar bien? 5 vs 100 cambia diseño.
• ¿Extensión / API: third-party plugins?

Frase clave: "Asumo solo texto, máximo ~100 editores concurrentes en hot doc, comentarios simples, sin embeds. Si me pides shapes, tablas o suggesting mode, lo extiendo al final."

Estimaciones de capacidad

Resoluciones año/día/seg. Lo que justifica sticky session por documentId y snapshots periódicos.

100M
MAU
Suite Workspace.
~30M DAU · ~100K logins/s avg.
~10M
Editores concurrentes
En hora pico oficina (US/EU work hours).
~50K nuevas conexiones WS/min.
~10K ops/s
Doc caliente
Reunión grande con ~100 cursores.
Promedio doc normal: ~1 op/s.
~50–200 B
Tamaño de op
~10K docs activos × 1 op/s = 1 MB/s.
~80 GB/día · ~30 TB/año en op log.
~5 min
Snapshot interval
~12 snapshots/h × ~1M docs activos = 12M/h.
Compactación nocturna del log.
~10–50 MB
Doc + history
Para ~1 año de edición activa.
~50 PB total (Google Drive scale).
100M+
Permisos (Zanzibar)
~1B+ relaciones de acceso.
~100K lookups/s en pico.
99.99%
SLO disponibilidad
~52 min/año · ~4 min/mes.
~8 s/día permitidos.

Endpoints

WebSocket para edición; HTTP para CRUD y permisos.

MétodoPathDescripciónNotas
POST/api/documentsCrear documento.
GET/api/documents/{id}Snapshot inicial + version.
POST/api/documents/{id}/operationsAplica operación (insert/delete) sobre version base.OT/CRDT.
GET/api/documents/{id}/changes?since={v}Devuelve operaciones desde versión.Catch-up.
POST/api/documents/{id}/permissionsCompartir con usuario/grupo.RBAC/ACL.
GET/api/documents/{id}/historyLista versiones para revert.
WS/ws/documents/{id}Canal en vivo: ops + presence.WebSocket.

Cómo defiendo estas APIs en la entrevista

HTTP para CRUD, WebSocket para edición en vivo. Crear, listar, compartir, ver historial → REST (idempotente, cacheable, fácil de debuggear). Editar texto en tiempo real → WebSocket porque necesito latencia < 100ms y bidireccional. Esta separación es estándar en Notion, Figma, Quip.

Operations-based, no document-based. No envío "el documento entero" en cada save — envío operations incrementales (insert pos 27 "rapid"). Cada op tiene baseVersion; el server transforma contra ops concurrentes (OT) y appendea con nueva version. Esto permite collab real sin lost updates.

Permissions explícito. POST /documents/{id}/permissions con body que define subject (user/group), relation (viewer/editor) y opciones (link sharing, expiration). El check de acceso ocurre al conectar al WS y al hacer save — cache para evitar lookup recursivo en cada op.

Frase clave: "El doc es un append-only op log. El cliente envía ops; el server transforma y broadcastea. HTTP cubre CRUD y permisos; WebSocket cubre la conversación en tiempo real."

Ejemplo: operación

POST /api/documents/d_42/operations
{
  "baseVersion": 184,
  "operations": [
    { "op": "insert", "pos": 27, "text": "rápido " },
    { "op": "delete", "pos": 14, "len": 3 }
  ],
  "clientId": "c_alice"
}

200 OK
{ "appliedAt": 185, "transformed": true }

Arquitectura

flowchart LR Editor(["Editor Web/Móvil
local OT/CRDT"]) -- WS --> GW GW{{"🌐 API GATEWAY
sticky by docId"}} --> Doc["Doc Service
OT/CRDT engine"] Doc --> Log Doc --> Snap Doc --> Bcast["Broadcast
Pub/Sub"] Bcast --> Editor GW --> Auth["Permissions Service
ACL / Zanzibar"] Auth --> AuthDB Doc --> Search Editor -. presence .-> Pres["Presence Service"] subgraph Log["💾 Op Log · Spanner / Postgres (append-only)"] direction TB l1["operations"] l2["documents"] end subgraph Snap["🗄️ Snapshot Store · S3"] direction TB sn1["snapshots/doc_id/v"] end subgraph AuthDB["💾 ACL Store · Spanner / Zanzibar"] direction TB a1["permissions"] a2["groups"] a3["users"] end subgraph Search["🔍 Search Index · Elasticsearch"] direction TB s1["docs_fts"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#0c4a6e,stroke:#38bdf8,stroke-width:2px,color:#e0f2fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class GW gateway class Doc,Auth,Bcast,Pres service class Editor client

Cómo lo explico en la entrevista

Apertura del doc. User abre un documento. Cliente hace GET /api/documents/{id} que devuelve el snapshot más reciente y la versión actual (v1024). El cliente abre un WebSocket al Gateway con sticky session por documentId — esto es crítico, porque el Doc Service mantiene el estado del doc en memoria y todas las ops del mismo doc deben pasar por la misma instancia.

Edición concurrente. User escribe. El editor genera ops locales (insert pos 27 "rapid") y las envía vía WS con baseVersion=1024. El Doc Service: (1) recibe la op, (2) la transforma contra cualquier op concurrente que ya aplicó (Operational Transform) o la merge (CRDT), (3) la appendea al op log con version=1025, (4) la broadcastea a los demás clientes conectados al doc por Pub/Sub.

Resync de los demás. Cada cliente recibe la op, la transforma contra sus pendientes locales, la aplica al DOM. Esto da consistencia eventual fuerte: todos convergen al mismo estado independientemente del orden de llegada.

Snapshots y permisos. Cada ~5 min un job async genera un snapshot del estado y lo guarda. Esto acelera "abrir doc" — no necesitas replay completo del op log. Los permisos los chequeo al conectar y al hacer save (Zanzibar lookup), con cache para evitar el árbol completo.

Frase clave: el documento es un append-only op log con OT/CRDT. Sticky por docId garantiza un solo transformador secuencial; snapshots aceleran la lectura inicial; permisos granulares vía Zanzibar.

Por qué cada componente

Sticky session por docIdSin esto, dos pods aplicarían ops del mismo doc en paralelo y el log diverge.
WebSocketLatencia de ms para colaboración. Polling no escala ni da la sensación de "en vivo".
OT vs CRDTOT es lo de Google Docs original (transformaciones explícitas). CRDT es más distribuido pero merge complejo. Ambos válidos.
Op log append-onlyFuente de verdad. Permite revert, audit y reproducir cualquier versión.
Snapshots periódicosSin esto, abrir un doc con 10K ops lleva replay completo. Snapshot + delta es O(1) amortizado.
Pub/Sub broadcastFan-out a N clientes del doc sin que el Doc Service tenga que conocer cada conexión.
Zanzibar permissionsModelo basado en relaciones (viewer/editor/owner). Granular sin árboles imposibles.
Presence serviceCursores remotos, avatares y selección — UX que diferencia colaborativo real de "edición compartida".

Cuellos de botella

  • Hot doc con 100+ editores activos en una reunión grande.
  • Permission resolution recursivo en árboles de carpetas profundas.
  • WebSocket reconnection storm tras un deploy o falla parcial.
  • Op log size en docs largos sin compactación.
  • Sticky LB y migración cuando un pod falla con docs activos.

Mejoras / cómo escala

  • Compactación del op log con snapshots y truncate del prefijo aplicado.
  • Permissions cache con TTL e invalidación por path al cambiar herencia.
  • Heartbeat con backoff + jitter para evitar reconnect storms.
  • Sharding por doc range con migración en vivo (handoff state).
  • CRDT en lugar de OT si necesitas multi-master geográfico.
  • Local-first con sync diferido para offline-first.

Por qué estos servicios cloud (vs alternativas)

Spanner / Postgresmetadata + op log

Por qué: ACID en append del op log + actualización de current_version. Spanner si quieres multi-region con strong consistency; Postgres si single-region es suficiente.

vs DynamoDB: sin transacciones multi-row complejas, append + counter no son atómicos. vs Cassandra: last-write-wins rompe el OT. vs MongoDB: aceptable pero menos garantías ACID históricamente.

Pub/Sub / Kafkabroadcast de ops

Por qué: fan-out de operaciones a N clientes conectados al doc. Sin esto, el Doc Service tendría que conocer cada conexión WebSocket y mantener referencias.

vs Redis Pub/Sub: aceptable, sin durabilidad. vs WebSocket relay propio: reinventar broker. vs SQS: queue, no broadcast multicast.

WebSocket Gateway stickycanal de edición

Por qué: sticky session por documentId garantiza que todas las ops de un doc lleguen al mismo pod (state local del transformador OT). Sin sticky, los logs divergen.

vs HTTP polling: latencia inaceptable para colaboración en vivo. vs SSE: unidireccional, no permite enviar ops del cliente. vs gRPC streaming: aceptable pero sin browser nativo.

Snapshot Store (S3 / GCS)snapshots periódicos

Por qué: blobs grandes (snapshot de doc) en object storage barato y durable. Lifecycle a cold tier para snapshots viejos.

vs guardar en la DB: infla la DB con blobs grandes que rara vez se leen. vs filesystem: sin redundancia ni escala.

Permissions (Zanzibar / SpiceDB / OpenFGA)ACL granular

Por qué: modelo basado en relaciones (object-relation-subject) escala a billones de tuples. Soporta herencia (folder → docs), grupos, sharing por link, todo en un mismo modelo.

vs ACL en columnas de Postgres: queries recursivas matan performance. vs IAM cloud-native: diseñado para recursos de infra, no para 1B+ docs por usuarios externos. vs RBAC simple: no captura "compartido conmigo" granular.

Diseño de base de datos

Op log como fuente de verdad, snapshots como aceleración, ACL granular.

documents
document_idUUID PK
owner_idUUID FK
titleTEXT
current_versionBIGINT
updated_atTIMESTAMPTZ
idx (owner_id, updated_at DESC)
operations (op log)
document_idUUID PK
versionBIGINT PK
author_idUUID FK
op_typeENUM
payloadJSONB
tsTIMESTAMPTZ
PK (document_id, version) append-only
partition por document_id
snapshots
document_idUUID PK
versionBIGINT PK
blob_urlTEXT
created_atTIMESTAMPTZ
Latest snapshot acelera "abrir doc"
permissions (Zanzibar-style)
objectTEXT (doc:123)
relationENUM viewer/editor/owner
subjectTEXT (user:42 / group:eng)
idx (object, relation)
cache por user para evitar lookup recursivo

Cómo defiendo este diseño de base de datos

Op log es la fuente de verdad, snapshots son optimización. Si pierdo todos los snapshots, puedo reconstruir cualquier estado replayeando el op log. Si pierdo el op log, perdí audit y revert. Por eso el op log se replica con consensus, los snapshots solo con WAL shipping.

Composite PK (document_id, version). No solo da unicidad — permite particionar la tabla por document_id directamente. Cada doc es un range continuo de versions; queries de "ops desde v100" son range scan eficiente.

Permissions estilo Zanzibar. Tuplas (object, relation, subject) permiten modelar herencia (folder → docs), grupos, y compartir por link en un mismo modelo. Un check es: encuentra cualquier path que conecte user → relation → object. Cache agresiva con TTL e invalidation por path al cambiar herencia.

Snapshots cada 5 min. Trade-off: snapshots más frecuentes = load inicial más rápido pero más overhead de escritura; menos frecuentes = replay más largo. 5 min balancea bien para docs con < 1K ops/min.

Frase clave: "El op log resuelve consistency con OT/CRDT; los snapshots resuelven performance de load; las permissions resuelven el access control granular. Cada estructura existe por una razón distinta."

Decisiones técnicas

TemaDecisiónPor qué
ConcurrenciaOperational Transform o CRDT.Dos algoritmos clásicos; OT es de Google Docs, CRDT es más distribuido.
RoutingSticky session por documentId.Las operaciones se transforman secuencialmente en una sola instancia.
StorageOp log + snapshots periódicos.Reconstruir cualquier versión sin replay completo.
PresenceWebSocket con heartbeat.Cursores y avatares en vivo.
PermisosACL granular + cache de checks.Compartir por usuario, grupo o link.
OfflineCliente guarda ops y reenvía.Resincroniza al volver, OT/CRDT lo absorbe.

Cómo defiendo estas decisiones técnicas

Constraint clave: edición concurrente sin lost updates. Esta sola línea define el sistema. OT (Operational Transform) o CRDT son las dos respuestas válidas. OT es lo que Google Docs usa originalmente; CRDT es más moderno y distribuido.

Sticky session por docId. Sin esto, dos pods aplicarían ops del mismo doc en paralelo y los logs divergirían. La complejidad de coordinar entre pods (consensus) es peor que la complejidad de manejar sticky con failover.

Métricas que validan. Latencia de propagación de op (p99 < 100 ms entre clientes). Conflict rate (% de ops que requieren transformación). Reconnect storm rate tras deploy. Permission lookup latency (target < 5 ms con cache).

Cuándo cambio de opinión. Multi-region active-active → CRDT en lugar de OT (no necesita coordinador único). Edición offline-first crítica → CRDT con sync diferido. Docs > 1M ops → compactación más agresiva con snapshots cada minuto.

Frase clave: "El sistema balancea consistencia (OT garantiza convergencia), latencia (sticky + WS), durabilidad (op log replicado) y escalabilidad (sharding por docId). Cada decisión es defendible mostrando qué propiedad protege."

Pitfalls

Lost updates

Sin OT/CRDT, dos escrituras se pisan. Versionar y transformar siempre.

Routing inconsistente

Si el mismo doc aterriza en dos pods, los logs divergen. Sticky por docId.

Auditoría floja

Sin op log inmutable, no puedes hacer revert ni investigar abuso.

Script de respuesta: “Modelo el documento como un append-only op log con OT o CRDT. El editor envía operaciones por WebSocket contra una sticky session por docId; el servidor las transforma contra ops concurrentes y replica el resultado a todos los clientes. Snapshots periódicos aceleran la carga inicial; permisos por ACL controlan acceso. Presence va por el mismo canal. Mido latencia de propagación, conflictos y tasa de revert.”

Tiers de almacenamiento

Docs activos en RAM/SSD; el long-tail (docs nunca tocados) puede archivarse barato.

Hot · in-memory
Estado en memoria del Doc Service para docs activos, op log buffer, presence map, permission cache.
Process memory + Redis
~$36/GB RAM
Warm · SSD operacional
Op logs, snapshots recientes (último mes), metadata DB, ACL store.
Spanner / Postgres + S3 Standard para snapshots
~$0.10–0.30/GB/mes
Cold · object storage IA
Docs sin edición > 30 días, op logs compactados, snapshots viejos.
S3 Standard-IA / GCS Nearline
~$0.0125/GB/mes
Frozen · archive
Docs > 1 año sin acceso, audit logs históricos, copias para compliance.
S3 Glacier / Coldline
~$0.004/GB/mes

Cómo defenderlo en la entrevista

Re-promotion al abrir. Cuando user abre un doc que está en cold/frozen, se promueve automáticamente a warm (lo cargamos en SSD). Primera apertura tiene latencia extra (~1–5 s), las siguientes son normales. Aceptable porque docs viejos rara vez se abren.

Op log compactado vs raw. El op log raw puede tener millones de pequeñas ops. Periódicamente compacto en snapshots y archivo el log raw a IA — si necesito reconstruir un estado intermedio, retrieval lento es OK.

Frase clave: "Docs activos viven en memoria del pod (sticky); recientes en Spanner/SSD; viejos en IA con re-promotion lazy; archives sin acceso en Glacier. La distribución de acceso es Pareto extremo, los tiers monetizan eso."

Versión alternativa: con Load Balancer (no API Gateway)

Para Docs el WS sticky por documentId manda. LB cumple, Gateway agrega lo que ya tienes.

flowchart LR Editor(["Editor Web/Móvil"]) --> LB LB{{"⚖️ L7 LOAD BALANCER
sticky by documentId
(consistent hashing)"}} --> Doc["Doc Service Pod
+ Auth (Google SSO / JWT)
+ OT/CRDT engine"] Doc --> Log Doc --> Snap Doc --> Bcast["Pub/Sub broadcast"] Bcast --> Editor Doc --> Auth["Permissions Service
Zanzibar"] Auth --> AuthDB Editor -. presence .-> Pres["Presence Service"] subgraph Log["💾 Op Log · Spanner"] direction TB l1["operations"] l2["documents"] end subgraph Snap["🗄️ Snapshot Store · S3"] direction TB sn1["snapshots"] end subgraph AuthDB["💾 ACL Store · Zanzibar"] direction TB a1["permissions"] a2["groups"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#0c4a6e,stroke:#38bdf8,stroke-width:2px,color:#e0f2fe classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class Doc,Auth,Bcast,Pres service class Editor client

API Gateway vs Load Balancer: cómo defender cada uno

El sticky es el punto crítico. Cualquiera de los dos puede hacer sticky por header (cookie de sesión que codifica documentId). LB con consistent hashing por documentId es más simple operativamente. El Gateway aporta poco aquí — auth ya viene de Google SSO, rate limit por user es trivial, transformations no hacen falta.

Frase clave: "Docs es un caso donde LB con sticky-by-documentId es más limpio. Los pods son stateful (transformer OT vive ahí), el SSO es Google, rate limits son por user no por endpoint. Gateway sería overkill."

Cuándo elegir API Gateway

  • Drive API pública para apps third-party con OAuth scopes.
  • Apps Script integrations con rate limits.
  • Multi-tenant SaaS con quotas por organización.
  • Versioning API para clientes externos legacy.

Cuándo elegir Load Balancer

  • WebSocket sticky por documentId — consistent hashing simple.
  • Auth simple (Google SSO / JWT verificado en pod).
  • Latency-sensitive: ops a 100ms p99 no toleran +20ms de gateway.
  • Service mesh ya cubre observability.
  • Apps oficiales: no hay third-party que justifique gateway features.