ruta backend / módulo 2

M2 — Backend profundo

Outcome del módulo: diseñar un sistema y defender cada trade-off — por qué esa DB, ese índice, ese cache, esa cola, y qué se rompe primero bajo carga o fallo.

El sub-roadmap de System Design tiene 4 fases: Networking → Building Blocks → Patterns → Real-World.


Framework de resolución — cómo atacar un problema de System Design (6 pasos)

Dos cosas distintas: el sub-roadmap 4 fases = qué tecnologías/conceptos aprender. Este framework 6 pasos = cómo atacar un problema concreto (entrevista o diseño real). Fuente: Hemant Pandey.

Tesis central: System Design NO es un examen de arquitectura, es un examen de pensamiento. El error #1 (aun de ingenieros de 7+ años): empiezan a dibujar cajas (Redis, Kafka, Cassandra, K8s) antes de preguntar qué problema resuelven. Pedile a 5 staff engineers que diseñen YouTube y tendrás 5 respuestas correctas distintas — el interviewer evalúa cómo pensás, no la respuesta: asunciones, tradeoffs, simplificación, comunicación.

flowchart TD
    S1[1· Clarificar requisitos<br/>preguntar ANTES de dibujar] --> S2[2· Definir las APIs<br/>cómo interactúa el usuario]
    S2 --> S3[3· Arquitectura high-level<br/>conectar piezas, big picture]
    S3 --> S4[4· Profundizar en lo que importa<br/>gastá tiempo donde está la complejidad]
    S4 --> S5[5· Discutir tradeoffs<br/>donde el senior se separa]
    S5 --> S6[6· Identificar cuellos<br/>qué se rompe primero y cómo lo arreglo]

Paso 1 — Clarificar requisitos (acá se gana la entrevista). Preguntar antes de dibujar: ¿cuántos usuarios? ¿read-heavy o write-heavy? ¿latencia esperada? ¿availability vs consistency? ¿presupuesto? Cada respuesta cambia la arquitectura. 5 min acá ahorran 20 después. (Es el mismo instinto que M0: necesidad primero.)

Paso 2 — Definir las APIs. Antes de elegir DB/Redis/Kafka, pensá cómo el usuario interactúa (Get Feed, Create Post, Like…). Con las APIs claras, lo demás se ordena.

Paso 3 — Arquitectura high-level. Recién ahora conectás clientes, load balancers, app servers, caches, DBs, colas. Mostrás que entendés el panorama, sin detalle todavía.

Paso 4 — Profundizar donde importa. No expliques todo. WhatsApp → entrega de mensajes. YouTube → storage/streaming de video. Uber → updates de ubicación. Gastá el tiempo donde está la complejidad real.

Paso 5 — Discutir tradeoffs (el separador senior). Toda decisión tiene costo: más cache = menos latencia pero datos stale · replicación = más availability pero desafíos de consistencia · sharding = más escala pero joins difíciles. No buscan la respuesta perfecta, buscan decisiones pensadas.

Paso 6 — Identificar cuellos. ¿Qué se rompe primero: DB, cache, red, cola? ¿Cómo lo arreglás? Pensar hacia adelante muestra madurez.

Anti-patrones (los errores que se repiten):

Nota atómica: Front: ¿por qué fallan System Design ingenieros técnicamente buenos? Back: porque empiezan a diseñar antes de pensar — dibujan arquitectura sin clarificar requisitos. Es examen de pensamiento (asunciones, tradeoffs, comunicación), no de memorizar arquitecturas.

Feynman: “Es como un arquitecto al que le pedís ‘una casa’. El malo empieza a dibujar mansiones. El bueno pregunta: ¿para cuántas personas? ¿presupuesto? ¿clima? La casa sale de las respuestas, no del catálogo.”

Aplicar: agarrá 20 problemas clásicos (Instagram, Dropbox, WhatsApp, Uber, TinyURL, Google Maps…) y resolvé cada uno con estos 6 pasos. A los ~5 vas a ver que comparten building blocks — dejás de memorizar y empezás a reconocer patrones.

Recurso: Grokking the Modern System Design Interview (Educative) — enseña el proceso (requisitos → arquitectura → tradeoffs), no arquitecturas sueltas. [mención del autor original; verificar si vale la pena pagarlo vs el material gratis].


Concepto 1 — Índices, EXPLAIN y el problema N+1

Idea núcleo: un índice es una estructura (B-tree) que evita escanear toda la tabla. Sin él, cada query es un full scan; con él mal usado, seguís escaneando.

Entender (capa 1)

Texto: una tabla sin índice se recorre fila por fila (O(n)). Un índice B-tree la busca en O(log n). Pero el índice solo sirve si la query filtra/ordena por su columna. EXPLAIN te dice si la DB usa el índice (Index Scan) o lo ignora (Seq Scan).

Visual:

flowchart TD
    Q[SELECT ... WHERE email = ?] --> D{¿hay índice en email?}
    D -->|Sí| I[Index Scan · O log n · lee pocas páginas]
    D -->|No| S[Seq Scan · O n · lee TODA la tabla]

Video: Database Indexing Explained (with PostgreSQL) — Hussein Nasser

Ejemplo (diagnóstico):

EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'x@y.com';
-- "Seq Scan on users"  → falta índice
CREATE INDEX idx_users_email ON users(email);
-- ahora: "Index Scan using idx_users_email"

El N+1 (el bug de performance más común con ORM):

# ❌ N+1: 1 query para los posts + 1 query por cada autor
for post in Post.objects.all():        # 1
    print(post.author.name)            # +N (una por post)

# ✅ 1-2 queries con eager loading (join/prefetch)
for post in Post.objects.select_related("author"):
    print(post.author.name)

Fijar (capa 2)

Nota atómica:

Feynman: “El índice es el índice alfabético de un libro: en vez de leer las 800 páginas para encontrar ‘Python’, vas directo. El N+1 es ir a la biblioteca 100 veces por 100 libros en vez de pedirlos todos en un solo viaje.”

Aplicar (capa 3)

Tomá una query lenta, corré EXPLAIN ANALYZE, agregá el índice, verificá que cambió a Index Scan y bajó el tiempo. Reproducí un N+1 y arreglalo con eager loading.

Límites


Concepto 2 — Transacciones y niveles de aislamiento (ACID)

Idea núcleo: una transacción agrupa operaciones en una unidad atómica (todo o nada). El nivel de aislamiento define qué anomalías podés ver cuando hay concurrencia.

Entender (capa 1)

Texto: ACID = Atomicity (todo o nada), Consistency (reglas válidas), Isolation (transacciones no se pisan), Durability (una vez commit, persiste). El aislamiento tiene niveles; más aislamiento = menos anomalías pero menos concurrencia.

Anomalías por nivel:

flowchart LR
    RU[Read Uncommitted] -->|permite| DR[Dirty Read]
    RC[Read Committed] -->|permite| NR[Non-repeatable Read]
    RR[Repeatable Read] -->|permite| PH[Phantom Read]
    SER[Serializable] -->|no permite| NONE[ninguna]

Nota: el diagrama es el modelo ANSI. En Postgres, Repeatable Read (snapshot isolation) ya evita phantom reads — es más estricto que el estándar en ese punto.

Video: Serializable vs Repeatable Read Isolation Level — Hussein Nasser

Ejemplo:

# atómico: o se descuenta Y se acredita, o nada
with db.transaction():
    cuenta_a.balance -= 100
    cuenta_b.balance += 100
    # si algo falla acá, rollback total — nunca queda plata a medias

Fijar (capa 2)

Nota atómica:

Feynman: “Una transacción es una apuesta con retiro: o pasan todas las jugadas o se cancela la mesa entera. El nivel de aislamiento es cuánto dejás que los otros jugadores vean tus cartas antes de bajarlas.”

Aplicar (capa 3)

Simulá dos transacciones concurrentes sobre la misma fila. Observá el comportamiento en Read Committed vs Serializable. Provocá un deadlock y vé cómo la DB aborta una.

Límites


Concepto 3 — Paginación: cursor vs offset

Idea núcleo: OFFSET se vuelve lento en páginas profundas; el cursor (keyset) es estable y O(log n).

Entender:

-- ❌ offset: la DB igual lee y descarta las 100000 filas anteriores
SELECT * FROM posts ORDER BY id LIMIT 20 OFFSET 100000;
-- ✅ keyset/cursor: salta directo con el índice
SELECT * FROM posts WHERE id > :last_id ORDER BY id LIMIT 20;

Nota atómica: Front: ¿por qué offset alto es lento? Back: la DB lee y descarta todas las filas del offset antes de devolver; O(offset). El cursor usa el índice para saltar: O(log n) y estable ante inserciones. Feynman: “Offset es contar desde la página 1 cada vez que abrís el libro. Cursor es poner un señalador donde quedaste.” Aplicar: implementá paginación por cursor en una API y compará el tiempo contra offset en la página 10.000. Límites: cursor no permite “saltar a página N” arbitraria; sirve para scroll/feed, no para paginadores numerados.


Concepto 4 — Caching (cache-aside + invalidación)

Idea núcleo: guardar resultados caros en memoria rápida (Redis) para no recalcular. Lo difícil no es cachear: es invalidar.

Entender (capa 1)

Texto: patrón cache-aside: la app consulta el cache; si falla (miss), va a la DB y guarda el resultado en el cache. En writes, hay que invalidar o actualizar la entrada, o servís datos viejos.

Visual:

flowchart TD
    R[read] --> C{¿en cache?}
    C -->|hit| RET[devuelve del cache]
    C -->|miss| DB[lee de DB] --> SET[guarda en cache con TTL] --> RET
    W[write] --> UPD[actualiza DB] --> INV[invalida/actualiza cache]

Video: Caching Techniques Explained: Write-Through, Write-Back, Cache-Aside — Hussein Nasser

Ejemplo:

def get_user(uid):
    if (u := cache.get(f"user:{uid}")) is not None:
        return u                       # hit
    u = db.get_user(uid)               # miss
    cache.set(f"user:{uid}", u, ttl=300)
    return u

def update_user(uid, data):
    db.update_user(uid, data)
    cache.delete(f"user:{uid}")        # invalidar, si no servís datos viejos

Fijar (capa 2)

Nota atómica:

Feynman: “El cache es una nota pegada en la heladera con el precio de algo. Rápido de leer. El problema es acordarte de cambiar la nota cuando el precio cambia — si no, cobrás mal.”

Aplicar (capa 3)

Meté cache-aside a un endpoint caro con Redis. Verificá hit/miss. Después provocá el bug: cambiá el dato en DB sin invalidar y observá que el endpoint sirve el valor viejo. Arreglalo.

Límites


Concepto 5 — Colas y comunicación async entre servicios

Idea núcleo: en vez de que A llame a B y espere, A pone un mensaje en una cola y B lo procesa cuando puede. Desacopla y absorbe picos.

Entender:

flowchart LR
    A[servicio A] -->|publica evento| Q[(cola / Kafka)]
    Q -->|consume| B[servicio B]
    Q -->|consume| C[servicio C]

Concepto 6 — Idempotencia

Idea núcleo: una operación idempotente da el mismo resultado si se ejecuta 1 o N veces. Crítica porque las redes reintentan y las colas entregan “al menos una vez”.

Entender (capa 1)

Texto: si un cliente reintenta un pago porque no recibió respuesta, no querés cobrar dos veces. Solución: idempotency key — el cliente manda una clave única; el server registra la clave y, si llega repetida, devuelve el resultado anterior en vez de re-ejecutar.

Visual:

flowchart TD
    REQ[request + Idempotency-Key: abc] --> CHK{¿key abc ya procesada?}
    CHK -->|sí| PREV[devuelve resultado guardado · no re-ejecuta]
    CHK -->|no| EXEC[ejecuta · guarda resultado bajo abc] --> RET[devuelve]

Video: Idempotency in APIs Explained | Why It Matters + Code Example — Arkynate

Ejemplo:

def charge(idempotency_key, amount):
    if (prev := store.get(idempotency_key)) is not None:
        return prev                     # ya se hizo, devolver lo mismo
    result = payment_provider.charge(amount)
    store.set(idempotency_key, result)
    return result

Fijar (capa 2)

Nota atómica:

Feynman: “Es el sello de mano en la entrada de un boliche: podés pasar el brazo por el lector mil veces, pero solo te cobran la entrada una vez porque ya tenés el sello.”

Aplicar (capa 3)

Agregá una idempotency key a un endpoint de creación. Mandá el mismo request 3 veces y verificá que solo se crea un recurso.

Límites


Concepto 7 — Patrones de sistemas distribuidos

Idea núcleo: bajo fallo parcial (un servicio cae, la red corta), estos patrones evitan corrupción y cascadas.

Entender (compacto):

flowchart LR
    subgraph Outbox
      TX[(tx: escribe datos + fila outbox)] --> REL[relay lee outbox] --> PUB[publica evento]
    end

Nota atómica: Front: ¿qué resuelve el outbox pattern? Back: la atomicidad entre “guardar en DB” y “publicar evento”: ambos no pueden ir en la misma transacción (DB + broker son sistemas distintos), así que se escribe el evento en una tabla outbox dentro de la misma tx y un relay lo publica después. Feynman: “El circuit breaker es el térmico de tu casa: si hay corto, corta la luz de una en vez de dejar que se queme todo el cableado.” Aplicar: implementá retries con backoff exponencial + jitter en un cliente HTTP; simulá un servicio caído y observá el patrón. Límites: cada patrón agrega complejidad; no metas SAGA/CQRS si un monolito con transacciones te resuelve.


Concepto 8 — Diseño de software (SOLID, capas, hexagonal)

Idea núcleo: separar responsabilidades para que el cambio sea barato y testeable.

Entender (compacto):

flowchart LR
    HTTP[adapter HTTP] --> PORT[[port de entrada]] --> CORE[núcleo de negocio] --> PORT2[[port de salida]] --> DBA[adapter DB]

Nota atómica: Front: ¿qué desacopla la arquitectura hexagonal? Back: el núcleo de negocio de los detalles de infraestructura (DB, framework, API externa) vía puertos/interfaces; los adapters implementan esos puertos. Permite testear el negocio sin DB y cambiar infra sin tocar la lógica. Feynman: “El negocio es el motor; los adapters son los enchufes. Cambiás de toma de corriente (DB) sin rediseñar el motor.” Aplicar: refactorizá un endpoint que mezcla lógica + SQL en capas (controller / service / repository); testeá el service mockeando el repository. Límites: en un CRUD chico, tanta capa es over-engineering; escalá la estructura al tamaño real. Recurso (catálogo profundo de patrones): refactoring.guru/es/design-patterns — los 22 patrones GoF (creacionales/estructurales/de comportamiento) con problema, solución, analogía real, UML y código en 10 lenguajes; además su sección de code smells → refactoring. Úsalo como diccionario; acá el foco es cuándo/por qué aplicarlos en un backend, no memorizarlos.


Concepto 9 — APIs modernas (más allá del REST-sync)

Idea núcleo: el REST síncrono básico es el piso; el mercado espera async, real-time y contract-first.

Entender (compacto):


Concepto 10 — Lógica en la base de datos (functions, views, triggers, RLS)

Idea núcleo: mover lógica de negocio cerca del dato (functions, views, materialized views, triggers, y sobre todo Row-Level Security) gana performance y consistencia a prueba de bugs de app — al costo de testabilidad y portabilidad. Es el trade-off que las ofertas senior premium piden explícitamente (“move business logic closer to the data layer”).

Entender (capa 1)

Texto: hay dos filosofías. App-centric (la default de la mayoría de equipos): la DB es storage tonto, toda la lógica vive en el código — fácil de testear, fácil de portar, pero cada invariante depende de que todos los servicios/devs se acuerden de aplicarla. DB-centric: parte de la lógica vive en la DB — se ejecuta siempre, sin importar qué servicio, script o dev olvidado la salte. No es “una mejor que otra”: es dónde conviene poner qué lógica.

Piezas del arsenal DB-centric en Postgres:

RLS funciona seteando un identificador de sesión (tenant/usuario) al abrir la conexión — típicamente desde un claim del JWT — y la policy compara ese valor contra la columna tenant_id de cada fila. Para que sea fail-closed usá current_setting('app.tenant_id', true) (el true = missing_ok): si nadie seteó el parámetro, devuelve NULL y la policy no matchea ninguna fila. ⚠️ Ojo: sin ese segundo argumento, Postgres lanza error si el parámetro no existe en la sesión (no devuelve cero filas silenciosamente).

Visual:

flowchart TD
    REQ[request con JWT · tenant_id=42] --> APP[app: valida JWT]
    APP --> SET["SET LOCAL app.tenant_id = 42<br/>(por conexión/transacción)"]
    SET --> Q["SELECT * FROM invoices<br/>(sin WHERE tenant_id!)"]
    Q --> RLS{RLS policy:<br/>tenant_id = current_setting}
    RLS -->|filtra automático| ROWS[solo filas del tenant 42]
    BUG[bug en la app: olvidó el WHERE] -.->|igual protegido| RLS

Video: Implement Authorization using Row Level Security (RLS) with Supabase — Supabase

Ejemplo:

-- ❌ Aislamiento SOLO en la app: un WHERE olvidado = fuga de datos entre tenants
SELECT * FROM invoices WHERE customer_id = 55;
-- si alguien olvida "AND tenant_id = :current_tenant" en ESTA query
-- o en cualquier query nueva del futuro, ve facturas de otro tenant.

-- ✅ RLS: el aislamiento vive en la DB, no depende de que la app se acuerde
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON invoices
    USING (tenant_id = current_setting('app.tenant_id', true)::int);  -- `true` = fail-closed si no está seteado

-- en cada transacción, la app setea el tenant desde el JWT (SET LOCAL vive dentro de la tx):
BEGIN;
SET LOCAL app.tenant_id = '42';

-- a partir de acá, CUALQUIER query a invoices —aunque no tenga WHERE—
-- solo puede ver/tocar filas de tenant_id = 42. Un bug en la app
-- ya no filtra datos entre clientes.
SELECT * FROM invoices;  -- Postgres agrega el filtro solo
-- Materialized view: agregación cara precalculada
CREATE MATERIALIZED VIEW monthly_revenue AS
SELECT tenant_id, date_trunc('month', created_at) AS month, sum(amount) AS total
FROM invoices GROUP BY tenant_id, month;

REFRESH MATERIALIZED VIEW monthly_revenue;  -- corre en cron, no en cada request

-- Trigger: invariante que corre pase lo que pase, sin depender del código que llame
CREATE FUNCTION set_updated_at() RETURNS trigger AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_invoices_updated_at
    BEFORE UPDATE ON invoices
    FOR EACH ROW EXECUTE FUNCTION set_updated_at();

Fijar (capa 2)

Nota atómica:

Feynman: “RLS es un portero parado en cada mesa del restaurante, no uno solo en la puerta. Aunque el mesero se equivoque y lleve la cuenta a la mesa equivocada, el portero de esa mesa no deja que nadie que no sea el dueño la vea. No depende de que el mesero se acuerde de nada.”

Aplicar (capa 3)

Creá una tabla invoices multi-tenant, habilitá RLS, definí la policy por tenant_id, seteá app.tenant_id por sesión. Después escribí a propósito una query “con bug” (sin WHERE tenant_id) y verificá que igual no ves filas de otro tenant. Agregá una materialized view para una agregación y medí el tiempo de leerla precalculada vs recalcularla al vuelo.

Límites


Checklist de dominio M2

Marcás cuando podés explicar (Feynman) + aplicar:

Salida verificable del módulo: una API con arquitectura por capas, 1 query optimizada con índice (EXPLAIN antes/después), 1 cache-aside con invalidación, idempotency key en el endpoint de creación, y un diagrama Mermaid del system design defendible pieza por pieza.