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
}
50 36. Diseñar antes de construir: la arquitectura de Nexus
Te contratan para el backend de un Figma-lite. ¿Por dónde empiezas?

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:
- El modelo (diagrama ER): qué cosas existen y cómo se relacionan — esto responde “¿qué es Nexus?”.
- El contrato (rutas + schemas + códigos de error): cómo se habla con él — esto responde “¿cómo se usa?”.
- 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_idvs UUID interno y otras decisiones que parecen menores y no lo son
50.4 El modelo de Nexus
Lee las decisiones del dibujo:
- Los roles viven en la membresía, no en el usuario — eres
admindel equipo A yviewerdel B (cap. 17). documents.current_revisiones un puntero — comoHEADen git: el documento sabe “estoy en la rev 12” y la historia vive aparte enrevisions.- 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_idpor recurso:documents/7f3a…en URLs — el id secuencial expone volumen y permite enumerar (cap. 8).reventero por documento (no UUID global): la historia es ordenada por documento —rev=14se compara, un UUID no.team_members.rolecon 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
- El brief dice “los documentos pueden estar en carpetas”. Agrega la entidad al ER: ¿carpeta como tabla, como columna, o como tag? Defiende.
- 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.
- ¿Por qué
REVISIONSno tienepublic_iden 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.
- Tabla
folderssolo si las carpetas tienen comportamiento propio (permisos, anidamiento). Si son solo agrupación visual: columnafolder/pathendocumentso tablafolders(id, project_id, name)liviana. La respuesta seria pregunta primero: “¿la carpeta hace algo o solo agrupa?” — el modelo sigue al comportamiento. DELETE /teams/{id}/members/{user_id}→ admin only. Miembro inexistente → 404 (MEMBER_NOT_FOUND). Caller no-admin → 403. Último admin → 409LAST_ADMIN— regla de negocio que se descubre al diseñar el contrato, no al crashearla.- Las revisiones no son recursos de primera clase expuestos por URL propia — se acceden a través del documento (
/documents/{id}/ revisions/14). Elpublic_idexiste 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.