Sistema de archivos en la nube
Crear carpetas, subir archivos en chunks, sincronizar entre dispositivos, versionar y compartir con permisos finos.
Recursos
File, Folder, Version, Chunk, Permission, SyncEvent.
Operaciones
Upload chunked, mover, renombrar, compartir, dedup.
Escala
Petabytes, miles de millones de archivos, sync continuo.
Riesgo
Renames no atómicos, permisos rotos, costos de egress.
Requisitos
Lo crítico: jerarquía consistente, blobs duraderos, sync confiable.
Funcionales
- Upload chunked y resumable.
- Carpetas, mover, renombrar atómicamente.
- Compartir por usuario, grupo o link público.
- Versionado y restore.
- Sync delta entre dispositivos.
No funcionales
- Durabilidad 11×9 en blobs.
- Listar carpeta < 300 ms p95.
- Sync delta < 5 s tras cambio.
- Disponibilidad 99.95%.
- Privacidad: cifrado en reposo y en tránsito.
Cómo aclarar requisitos en la entrevista
Casos de uso típicos. Backup automático de fotos del móvil, sync de carpeta de trabajo entre laptop y desktop, compartir folder con team via link, version history para restaurar archivo borrado, trash con retention de 30 días, edición colaborativa de Office docs (Word/Excel), shared drives empresariales con quotas, descarga selectiva (online-only files).
Preguntas que debes hacer:
• ¿Compartir solo a usuarios o también por link público con / sin auth?
• ¿Versioning ilimitado o limit (last N o por tiempo)?
• ¿Trash retention: hard delete después de X días?
• ¿Collab en línea (como Drive Docs) en alcance o aparte?
• ¿Quotas por user / team / org?
• ¿Encryption client-side (zero-knowledge) o server-side?
• ¿Tipo de archivos: cualquier blob o solo office docs?
• ¿Selective sync en cliente (online-only files)?
• ¿Compliance (HIPAA, GDPR, SOC2) en alcance?
Frase clave: "Asumo cualquier blob, sync delta, versioning con retention 30 días, share por link y por user, encryption server-side, sin collab en línea. Eso me deja un sistema parecido a Dropbox core."
Estimaciones de capacidad
Resoluciones año/día/seg. Lo que justifica chunks dedup + metadata DB independiente.
~50 GB / user promedio · ~30M new users/año.
+~5 EB nuevos/año (10% growth).
Mayor en docs corporativos repetidos.
~50K/s pico tarde.
10× picos durante work hours US/EU.
~1 PB metadata DB total.
~5% del storage en orfandad transitoria.
~43 s/día permitidos.
Endpoints
| Método | Path | Descripción | Notas |
|---|---|---|---|
| POST | /api/files/upload-url | Pre-signed URL multipart por chunks. | Resumable. |
| POST | /api/files/{id}/complete | Finaliza upload, crea versión. | |
| POST | /api/folders | Crea folder. | |
| GET | /api/files/{id}/metadata | Metadata + version actual. | |
| PATCH | /api/files/{id}/move | Move/rename atómico. | Tx en metadata DB. |
| POST | /api/files/{id}/share | Comparte con user/grupo/link. | ACL. |
| GET | /api/files/{id}/versions | Historial de versiones. | |
| GET | /api/sync/changes?since=... | Delta sync incremental. | Cliente de escritorio. |
Cómo defiendo estas APIs en la entrevista
Upload chunked es la decisión clave. POST /files/upload-url devuelve N URLs (una por chunk de 8 MB) más un upload session ID. El cliente sube en paralelo, retoma si se cae la red, dedup por hash antes de subir. Sin esto, un GB tomaría minutos sin posibilidad de resume.
Move/rename atómico vía PATCH. PATCH /files/{id}/move con body { "newParentId": "..." }. Atómico en la metadata DB (transacción). Renombrar = PATCH también.
Sync con cursor. GET /sync/changes?since={cursor} es el endpoint que cada device chequea cada minuto. Devuelve deltas más nuevo cursor; el cliente actualiza local. Idempotent — si el cliente pierde la respuesta, repite con el cursor anterior.
Frase clave: "Las APIs separan path control (metadata: HTTP rápido) de path data (chunks: pre-signed URLs al storage). Sync es polling con cursor — simple, sobrevive a desconexiones largas, scale horizontal trivial."
Ejemplo: upload chunked
POST /api/files/upload-url
{ "name": "video.mp4", "sizeBytes": 1073741824, "folderId": "f_4" }
201 Created
{
"fileId": "fi_72",
"uploadId": "up_1a",
"chunkSize": 8388608,
"uploadUrls": ["https://storage/.../1?sig=...", "..."]
}
POST /api/files/fi_72/complete
{ "uploadId": "up_1a", "etags": ["...","..."] }
Arquitectura
chunk hash check"] Dedup --> Store API -. eventos .-> Sync Sync --> Notify["Sync Notification Svc"] Notify --> Client Meta --> ACL Store --> GC["GC
chunks huérfanos"] subgraph Store["🗄️ Object Storage · S3 / GCS"] direction TB s1["chunks/sha256"] s2["thumbnails"] end subgraph Meta["💾 Metadata DB · Spanner / Postgres"] direction TB m1["files"] m2["file_versions"] m3["chunks"] m4["folders"] end subgraph Sync["📨 Sync Stream · Pub/Sub"] direction TB k1["sync_events"] end subgraph ACL["💾 Permissions · Zanzibar"] direction TB a1["permissions"] a2["sync_cursor"] end classDef gateway fill:#fbbf24,stroke:#f59e0b,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fef3c7 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class API gateway class Dedup,Notify,GC service class Client client
Cómo lo explico en la entrevista
Upload chunked. User sube un archivo de 1 GB. Cliente hace POST /api/files/upload-url. El API responde con un upload_session_id, chunk size (8 MB), y N pre-signed URLs (una por chunk). El cliente sube cada chunk directo a object storage en paralelo. Si se cae la red, retoma desde el último chunk completo.
Dedup transparente. Antes de subir cada chunk, el cliente lo hashea (SHA256) y pregunta al API: "¿ya existe este hash?". Si sí, no sube — solo registra la referencia (chunk_id). Si no, sube. Esto da ~30% de ahorro de storage en archivos comunes (PDFs corporativos, fotos repetidas).
Complete + atomic metadata. Cuando todos los chunks están en storage, cliente hace POST /files/{id}/complete con la lista ordenada de chunk IDs. El API ejecuta una transacción en la Metadata DB que: crea el archivo en el árbol, registra la versión nueva, incrementa ref_count de cada chunk, emite un sync_event. Esto debe ser atómico — si falla a mitad, no queda metadata huérfana.
Sync delta. Los demás dispositivos del user (laptop, móvil) tienen un sync_cursor. Periódicamente piden GET /sync/changes?since={cursor}. El servicio devuelve el delta (archivos cambiados/borrados/movidos) y el nuevo cursor. El cliente baja los chunks faltantes desde object storage.
Permisos. Compartir = insertar en permissions table (Zanzibar-style: doc:42 viewer user:alice). Acceso luego hace lookup que expande herencia (folder padre → root) hasta el primer match. Cache para evitar el árbol completo.
Frase clave: separo metadata transaccional (small, ACID) de blobs (large, eventually consistent). El cliente sube directo a storage; mi backend solo gestiona el árbol, los permisos y los eventos de sync.
Por qué cada componente
Cuellos de botella
- Move de carpeta gigante → tx grande en metadata DB.
- Hot folder con muchos sync subscribers (compartido empresa-wide).
- Permissions resolution recursivo en árboles profundos.
- GC de chunks huérfanos — storage waste si no se ejecuta.
- Pequeños archivos — mucho overhead de metadata vs bytes.
Mejoras / cómo escala
- Lazy materialization en moves: cambiar parent pointer, recompute en read.
- Sub-folders shard por hash si crecen mucho (avoid hot single row).
- Cache de decisiones de permisos con invalidation por path al cambiar herencia.
- Reference counting + GC nocturno para liberar chunks sin bloquear el path.
- Pack files para muchos archivos pequeños (write amplification reduction).
- Hot tier vs cold tier automático según último access (Glacier para old).
Por qué estos servicios cloud (vs alternativas)
Por qué: 11×9 durabilidad, multipart upload con resume, lifecycle a Glacier para archives, server-side encryption nativo, pre-signed URLs para upload directo del cliente.
vs HDFS / MinIO self-hosted: ops del cluster, replication manual, scaling complejo. vs filesystem en VM: imposible a EB. vs Cassandra: no es para blobs grandes.
Por qué: ACID estricto en move/rename atómico (cambia parent_id de N hijos en una tx). Spanner si multi-region; CockroachDB SQL-compatible self-managed; Postgres si single region es suficiente.
vs DynamoDB: sin transacciones multi-row complejas; rename de carpeta con 1000 hijos es problemático. vs Cassandra: last-write-wins rompe atomicidad. vs MongoDB: aceptable pero menos garantías.
Por qué: stream de cambios para alimentar el sync delta de cada device. Particionado por user_id mantiene orden de cambios por usuario. Cliente con cursor que avanza monotónicamente.
vs WebSocket directo: imposible mantener N conexiones por user activo (laptop, móvil, web). vs polling: ~10B sync events/día → polling sería >100K req/s desperdiciados.
Por qué: modelo basado en relaciones para sharing granular (viewer/editor/owner) con herencia (folder → archivos). Escala a billones de tuples; lookups con cache.
vs ACL en columnas SQL: queries recursivas son inviables. vs IAM cloud: diseñado para recursos de infra, no para 1B+ archivos por usuarios externos. vs RBAC simple: no captura "compartido conmigo" granular.
Por qué: downloads grandes (videos, PDFs) servidos por POP regional. Reduce egress cost del object storage en ~70%.
vs servir directo desde S3: egress fees prohibitivos a EB scale. vs P2P: falla en archivos cold (sin peers).
Por qué: nightly batch que escanea chunks con ref_count = 0 y los borra (después de TTL de 5 días por seguridad). Sin esto, storage waste crece sin límite.
vs reference counting síncrono: contención en hot chunks comunes. vs sin GC: EB de chunks huérfanos en años.
Diseño de base de datos
Metadata transaccional separada de blobs, dedup por hash, sync incremental.
(parent_id, name) UNIQUEidx
(owner_id, updated_at DESC)(file_id, version)idx
(created_at) para retention(sha256 UNIQUE) para dedupGC cuando ref_count = 0
Cursor de sync incremental por device
Cómo defiendo este diseño de base de datos
Metadata vs blobs vs permissions. Tres dominios independientes: metadata transaccional (jerarquía + versions), blobs (chunks en object storage), permissions (Zanzibar-style separado). Esta separación permite escalar cada uno independientemente.
Tree con parent_id, no path. Storing the path string (/users/alice/docs/file.pdf) sería simple pero rename/move se vuelve N×update. Con parent_id, mover una carpeta es un solo update; los hijos heredan automáticamente.
Versioning como append-only. file_versions nunca se modifica — cada save crea una nueva fila. files.current_version apunta a la actual. Restore es trivial: cambiar el pointer.
Chunks con ref counting. El mismo chunk puede ser referenciado por miles de archivos (dedup). ref_count se incrementa al asignar y decrementa al borrar versión. GC nocturno borra chunks con ref_count=0 > 5 días (margen de seguridad).
Frase clave: "El schema reconoce que metadata y blobs viven en mundos distintos: metadata es small + transaccional (Spanner/Postgres), blobs son massive + duraderos (object storage), permissions son granulares + read-heavy (Zanzibar). Tres tools, una semántica unificada."
Decisiones técnicas
| Tema | Decisión | Por qué |
|---|---|---|
| Metadata | DB con árbol jerárquico (parentId). | Renames atómicos a nivel registro. |
| Blobs | Object storage con chunks. | Resumable, paralelizable, deduplicable. |
| Dedup | Hash por chunk. | Ahorro de espacio enorme con archivos comunes. |
| Versioning | Cada complete crea nueva versión. | Restore, audit y collab. |
| Sync | Cliente con changeId; servidor responde delta. | Incremental, sobrevive a desconexiones. |
| Permisos | ACL por nodo + herencia. | Granular pero performante con cache. |
Cómo defiendo estas decisiones técnicas
Constraint: 1 EB de storage, 12K uploads/s, sync < 5 s. Define todo: blobs en object storage (no caben en DB), chunks (resume + dedup), metadata transaccional (atomic moves), sync con cursor (incremental).
Métricas que validan. Upload completion rate (target > 99%; failures = retry), dedup ratio (target ~30%), sync delta latency (target p95 < 5 s), permission resolution latency (target < 50 ms con cache), GC reclaim rate (chunks recovered/día).
Cuándo cambio de opinión. Edición colaborativa en línea (como Drive Docs) → agrego op log + WebSocket layer separado. Compliance HIPAA → encryption client-side, audit log granular. Multi-region → Spanner para metadata, replicated buckets para blobs.
Frase clave: "Es un trade entre simplicidad y features. El sistema simple es solo storage durable. Cada feature (sharing, sync, versioning) agrega complejidad — la mantengo justificada por uso real."
Pitfalls
Si renombras una carpeta con miles de hijos sin tx, dejas estado inconsistente.
Sin GC, los blobs sin referencia quedan ocupando espacio.
Calcular ACL recursivo en lectura es lento. Cachea decisiones.
Script de respuesta: “Separo metadata (árbol jerárquico, ACL, versiones) del almacenamiento de blobs (object storage en chunks). El upload es chunked y resumable con pre-signed URLs; deduplico por hash. El move/rename es una operación transaccional sólo en metadata. El sync usa un cursor que devuelve deltas. Cachea decisiones de permisos heredados y agrega GC para chunks huérfanos.”
Tiers de almacenamiento
Caso clásico de tiering — lifecycle policies por último access. Definen el modelo de costo del producto.
Cómo defenderlo en la entrevista
Lifecycle por último access, no por edad. Un archivo de hace 5 años que aún se abre semanalmente debe quedarse en Standard. Lifecycle policy con last_access_time es lo correcto, no creation_time. S3 Intelligent-Tiering lo automatiza.
Glacier Instant Retrieval es el sweet spot. $0.004/GB/mes (~85% off vs Standard) con retrieval en milisegundos. Para chunks que casi nunca tocas pero "no quiero esperar 12 h cuando los necesite", es el tier ideal.
Re-promotion al primer access. Cuando user abre un archivo en cold/frozen, se promueve a hot. Primera apertura tiene latencia extra; siguientes son normales. Esto evita que tiers cold maten UX.
Frase clave: "Esto es Dropbox 101: tiers automáticos por access pattern. La mayoría de archivos en cuentas viejas nunca se abren — Glacier baja el costo total ~10×. Hot tier sirve el 95% del tráfico desde el 5% del storage."
Versión alternativa: con Load Balancer (no API Gateway)
Para file system el upload va directo a object storage; el LB solo gestiona metadata.
NGINX / ALB"}} --> Pod["API Pods
+ JWT auth middleware
+ Rate limit (Redis)"] Pod --> Meta Pod --> Dedup["Dedup
chunk hash check"] Client -->|chunks pre-signed URL| Store Pod -. eventos .-> Sync Sync --> Notify["Sync Notification Svc"] Notify --> Client Pod --> ACL["Permissions Service
Zanzibar"] Store --> GC["GC chunks huérfanos"] subgraph Store["🗄️ Object Storage · S3 / GCS"] direction TB s1["chunks/sha256"] s2["thumbnails"] end subgraph Meta["💾 Metadata DB · Spanner / Postgres"] direction TB m1["files"] m2["file_versions"] m3["chunks"] m4["folders"] end subgraph Sync["📨 Sync Stream · Pub/Sub"] direction TB k1["sync_events"] end classDef lb fill:#34d399,stroke:#10b981,stroke-width:3px,color:#0f172a,font-weight:bold classDef service fill:#78350f,stroke:#fbbf24,stroke-width:2px,color:#fef3c7 classDef client fill:#1f2937,stroke:#94a3b8,stroke-width:2px,color:#e5e7eb class LB lb class Pod,Dedup,Notify,ACL,GC service class Client client
API Gateway vs Load Balancer: cómo defender cada uno
Para apps oficiales: LB. Tu sync daemon en laptop/móvil es código tuyo — auth con JWT propio, sin necesidad de OAuth flows complejos. Con 1B uploads/día, gateway-per-request explotaría el costo. LB simple + auth en pod es claramente mejor.
Para Public API: Gateway. Si ofreces Dropbox API a third-parties (apps que se integran con Dropbox), Gateway aporta: OAuth, quotas, plans, observability per-developer-key. Path separado api.dropbox.com/v2/ con Gateway, sync interno con LB.
Frase clave: "Sync daemon usa LB porque es propio y high-throughput. Public API usa Gateway porque third-party necesita features de developer experience. En la práctica coexisten."
Cuándo elegir API Gateway
- Public Dropbox/Drive API con OAuth y quotas por developer.
- Business API con plans por organización.
- Third-party integrations (Slack, Zapier, Office) con scopes granulares.
- Versioning en path (
/v1/,/v2/) con sunset headers.
Cuándo elegir Load Balancer
- Sync daemon oficial (high throughput, auth propio).
- App móvil oficial: ~10 requests/sesión, latency critical.
- Pre-signed URL flow: bytes nunca tocan tu API, gateway no aporta.
- Sync events ingest: 120K/s, fire-and-forget.
- Service mesh con sidecar para mTLS.
- Internal microservices (chunk service, dedup, GC) entre sí.