50  36. Diseñar antes de construir: la arquitectura de Nexus

Te contratan para el backend de un Figma-lite. ¿Por dónde empiezas?

Parte 9 — Proyecto final: Nexus
NotaEn una frase

Nexus: un workspace colaborativo donde equipos editan documentos en tiempo real, con historial completo — un Figma-lite. Antes de la primera línea de código: el dominio dibujado, el contrato escrito, y las decisiones documentadas. Este capítulo es diseño en papel — el código llega en el 37.

50.1 El problema

Te llega el brief: “workspace por equipos; proyectos con documentos que varias personas editan a la vez; historial para volver atrás; miembros con roles”. Es exactamente el tipo de requerimiento difuso que recibe un backend real — nadie te da el esquema, te dan una intención. El error de junior: empezar por POST /login. El error caro: descubrir en la semana 3 que “editar a la vez” necesitaba un modelo distinto desde el día 1.

El esquema es el plano de la base de datos: qué tablas hay, qué columnas tiene cada una y de qué tipo. Es contrato: una fila que no cumple el esquema no entra. Se cambia con migraciones, no a mano.

La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.

50.2 Cómo lo resuelve un equipo

Un equipo serio responde al brief con tres artefactos antes de codificar:

  1. El modelo (diagrama ER): qué cosas existen y cómo se relacionan — esto responde “¿qué es Nexus?”.
  2. El contrato (rutas + schemas + códigos de error): cómo se habla con él — esto responde “¿cómo se usa?”.
  3. Los ADRs (media página por decisión): por qué se eligió así — esto responde “¿por qué así y no de otra forma?” para el humano del futuro que pregunte.

50.3 Conceptos nuevos

  • U Del requerimiento al modelo: entidades, relaciones, ownership — el ER completo
  • U ADR mínimo: documentar por qué capas, por qué esas fronteras
  • U Diseñar el contrato primero: rutas, schemas, códigos de error — OpenAPI como fuente de verdad
  • D public_id vs UUID interno y otras decisiones que parecen menores y no lo son

50.4 El modelo de Nexus

erDiagram
  USERS ||--o{ TEAM_MEMBERS : "belongs"
  TEAMS ||--o{ TEAM_MEMBERS : "has"
  TEAMS ||--o{ PROJECTS : "owns"
  PROJECTS ||--o{ DOCUMENTS : "contains"
  DOCUMENTS ||--o{ REVISIONS : "history"
  USERS ||--o{ INVITATIONS : "invited"
  TEAM_MEMBERS {
    bigint team_id FK
    bigint user_id FK
    text role "admin|editor|viewer"
  }
  DOCUMENTS {
    bigint id PK
    uuid public_id
    bigint project_id FK
    int current_revision "the pointer"
    jsonb content "latest snapshot cache"
  }
  REVISIONS {
    bigint document_id FK
    int rev
    jsonb snapshot "full state at rev"
    bigint author_id FK
    timestamptz created_at
  }

Lee las decisiones del dibujo:

  • Los roles viven en la membresía, no en el usuario — eres admin del equipo A y viewer del B (cap. 17).
  • documents.current_revision es un puntero — como HEAD en git: el documento sabe “estoy en la rev 12” y la historia vive aparte en revisions.
  • Cada revisión es un snapshot completo — restore = leer una fila, no re-ejecutar la historia (cap. 37).

50.5 El contrato primero

POST   /auth/register | /auth/login | /auth/refresh | /auth/logout
GET    /teams                     → mis teams
POST   /teams/{id}/invitations    → admin only
POST   /invitations/{token}/accept
GET    /projects/{id}/documents
POST   /documents                 → editor+
GET    /documents/{id}            → member: content + current_revision
POST   /documents/{id}/revisions  → save new revision (the write hot path)
GET    /documents/{id}/revisions  → history list
POST   /documents/{id}/restore/{rev} → move the pointer back
WS     /documents/{id}/live       → presence + edits broadcast

Más los errores estables (cap. 25): UNAUTHORIZED, FORBIDDEN, DOC_NOT_FOUND, NOT_A_MEMBER, ROLE_TOO_LOW, INVITATION_EXPIRED, VALIDATION, CONFLICT, INTERNAL. El frontend puede programarse completo contra esta tabla antes de que exista el backend.

50.6 Los ADRs — tres decisiones, media página cada una

# ADR-01: Monolito modular por capas
Contexto: equipo chico, dominio cohesionado, realtime+HTTP en un proceso.
Decisión: monolito con capas (rutas/servicios/storage) del cap. 7.
Alternativas: microservicios (overhead de red sin el problema de escala
de equipos). Consecuencias: si una pieza crece 10× distinta, se extrae —
las fronteras ya existen.

# ADR-02: Snapshot + puntero para versionado (no solo diffs)
Contexto: restore debe ser O(1); el historial es append-only.
Decisión: snapshot completo por revisión + current_revision como puntero.
Alternativas: solo diffs (restore = replay N eventos), CRDT (proyecto
aparte). Consecuencias: +espacio en disco; -complejidad de restore.

# ADR-03: Last-Writer-Wins en edición concurrente
Contexto: dos cursores en el mismo documento.
Decisión: LWW con rev + broadcast — el merge fino por carácter es CRDT,
otro nivel de complejidad. Alternativas: OT/CRDT. Consecuencias: ediciones
en el mismo párrafo se pisan — aceptado y documentado.
Actúa como tech lead: diseña la arquitectura de Nexus, un workspace
colaborativo (teams → projects → documents con versionado y edición
concurrente). Entregables: diagrama ER con justificación de cada
relación y ON DELETE, contrato REST+WS completo (rutas, schemas,
códigos de error estables), modelo de membresías con roles por equipo,
y 3 ADRs de media página (estructura del proyecto, modelo de
versionado, resolución de conflictos) con alternativas descartadas y
consecuencias aceptadas. No escribas código — el diseño primero.

50.7 ¿Por qué diseñar en papel primero?

Lo único que necesitas llevarte: cada línea del ER y cada ADR es un problema resuelto cuando costaba una oración — en código cuesta un refactor, en prod una migración. El diseño no es burocracia: es el lugar barato de equivocarse.

Producción (prod) es el entorno real: donde están los usuarios, los datos que importan y las consecuencias. Todo lo demás — local, staging — existe para que los errores ocurran antes de llegar ahí.

Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.

  • public_id por recurso: documents/7f3a… en URLs — el id secuencial expone volumen y permite enumerar (cap. 8).
  • rev entero por documento (no UUID global): la historia es ordenada por documento — rev=14 se compara, un UUID no.
  • team_members.role con CHECK (admin|editor|viewer): el rol inválido lo rechaza la BD, no solo la validación.
  • Invitaciones con token + expires_at + used_at: otro flujo con estado — el token opaco viaja por email, el servidor verifica uno-uso-expira (mismo patrón que refresh tokens del cap. 18).

50.8 Errores comunes

Error Por qué pasa Fix
Empezar por el código “El diseño sale solo” ER + contrato antes — el dibujo se cambia en segundos
Roles en el usuario “Es admin de la app” El rol es de la membresía — contexto por equipo
Diseñar para el futuro imaginario “Y si necesitamos microservicios” Diseña para el brief de hoy + ADRs que documenten los bordes
Contrato después del código “Ya veremos qué devuelve” El contrato es la fuente de verdad — frontend y backend se construyen en paralelo contra él
Decisiones sin documentar “Queda en la PR” ADR de media página — el por qué que el código no dice

50.9 Buenas prácticas

  • El brief se reescribe como modelo: cada sustantivo del requerimiento es un candidato a entidad; cada verbo, una relación o una ruta.
  • Decidir lo reversible rápido y lo irreversible lento (Ap. K, decisión framework): el modelo de versionado es puerta de una vía — merece la página de análisis; el nombre de la ruta no.

Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.

  • El contrato incluye los errores: una ruta sin sus 4xx definidos está a medio diseñar.
  • El diseño se presenta: un RFC de una página que el equipo puede comentar — las objeciones en papel son gratis.

50.10 Ejercicio

  1. El brief dice “los documentos pueden estar en carpetas”. Agrega la entidad al ER: ¿carpeta como tabla, como columna, o como tag? Defiende.
  2. Escribe la ruta y el contrato de “quitar a un miembro del equipo”: método, path, quién puede, qué devuelve si el miembro no existe, si el caller no es admin, si es el último admin.
  3. ¿Por qué REVISIONS no tiene public_id en la tabla de arriba?

return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.

Un método es una función que vive dentro de un objeto/clase: task.save() — save es un método de task. La diferencia con una función suelta: el método conoce al objeto que lo contiene (this/ self).

50.11 Mini reto

Presentar el diseño como si fuera un RFC de equipo — y defender cada decisión.

Ejercicio.

  1. Tabla folders solo si las carpetas tienen comportamiento propio (permisos, anidamiento). Si son solo agrupación visual: columna folder/path en documents o tabla folders(id, project_id, name) liviana. La respuesta seria pregunta primero: “¿la carpeta hace algo o solo agrupa?” — el modelo sigue al comportamiento.
  2. DELETE /teams/{id}/members/{user_id} → admin only. Miembro inexistente → 404 (MEMBER_NOT_FOUND). Caller no-admin → 403. Último admin → 409 LAST_ADMIN — regla de negocio que se descubre al diseñar el contrato, no al crashearla.
  3. Las revisiones no son recursos de primera clase expuestos por URL propia — se acceden a través del documento (/documents/{id}/ revisions/14). El public_id existe para lo que viaja solo por URL; la revisión viaja dentro del documento.

Mini reto. El RFC: contexto (el brief) → modelo (ER) → contrato → 3 ADRs → “lo que NO construimos” (CRDT, microservicios, comentarios en documento) → preguntas abiertas. Defender cada decisión = decir qué alternativa se descartó y qué precio se aceptó — eso es lo que un RFC real necesita, y lo que el Ap. K entrena.

Repaso de entrevista: las preguntas de este tema viven en el Apéndice H (diseño y arquitectura).

50.12 Vocabulario técnico del capítulo

Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.

Un caché es una copia rápida de algo costoso de obtener: el resultado de una query pesada, la sesión. Redis es el caché estándar. La regla de oro: cachear es fácil, invalidar (saber cuándo el caché ya no vale) es lo difícil.

Una imagen es la plantilla inmutable de la que nacen contenedores: la foto del disco + cómo arrancar. Se construye en capas (Dockerfile), se versiona con tags, se publica en un registry.

Concurrencia = manejar muchas cosas en progreso (atender 1000 requests intercalando). Paralelismo = ejecutar varias a la vez en varios núcleos. Un camarero con 10 mesas es concurrente; 10 camareros son paralelos.

Vertical: máquina más gorda (más CPU/RAM) — fácil, tiene techo. Horizontal: más máquinas — el camino de internet, pero requiere que la app no guarde estado local (por eso todo va a BD/Redis).

Un monolito es una app donde todo corre en un solo proceso: rutas, lógica, BD access. No es insulto — es el default correcto mientras el equipo sea chico. Se puede modular por dentro sin dividir el deploy.

Microservicios = dividir la app en servicios independientes que se comunican por red. Ganas despliegue separado; pagas con distribución (transacciones entre servicios, latencia, observabilidad). Equipo de 5 no necesita esto.

Un proceso es un programa en ejecución: el sistema operativo le da memoria propia y un lugar en la fila del CPU. Tu API es un proceso; Postgres es otro; el navegador es varios. Que “se caiga el proceso” = el programa murió.

Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.

Una foreign key es un puntero verificado: tasks.project_id apunta a projects.id y la BD garantiza que el proyecto existe. Es la diferencia entre “el dato dice 5” y “el dato apunta a algo real”.

La autorización responde “¿qué puedes hacer?”: eres usuario válido (autenticado), pero ¿puedes borrar esta tarea? Se decide por rol o por ownership — y se verifica en cada request, no se recuerda.

Un token es una credencial portable: una cadena que dice quién eres y hasta cuándo. El servidor la emite tras el login; el cliente la presenta en cada request en vez de la contraseña.

Un dominio es el nombre que compras (midominio.com) y apuntas a tu servidor por DNS. Sin él, tus usuarios tendrían que memorizar una IP. El HTTPS serio requiere dominio.

50.13 Lo que deberías saber hacer ahora