Arquitecturas
Dos preguntas, mezcladas a propósito
Las cinco apps responden dos preguntas distintas, así que no son cinco puntos de una misma escala. Los nombres de las carpetas responden la primera; los import, la segunda. Organizar por área de negocio puede convivir con cualquier dirección de dependencias.
| App | Qué corta la parte superior de src/ | Hacia dónde apuntan las dependencias |
|---|---|---|
| Layered | Rol técnico: http/, services/, repositories/ | Hacia el almacenamiento: los servicios importan repositorios concretos. |
| Feature modules | Área de negocio: modules/projects/, modules/tasks/ | Hacia el almacenamiento dentro de cada grupo de código alrededor de un área de negocio, con sus rutas, reglas y SQL; el resto del código usa solo su punto de entrada público.; entre módulos, tasks → projects. |
| Hexagonal | Adentro y afuera: core/, adapters/ | Hacia el en Hexagonal, el código de la aplicación que no conoce ninguna tecnología: reglas, modelo y puertos, en core/.: los código que traduce entre un puerto y una tecnología concreta, como Express o node:sqlite. implementan interfaz que la aplicación declara en sus propios términos, para lo que ofrece (las operaciones) o para lo que necesita de afuera (almacenamiento, atomicidad). que declara el núcleo. |
| Clean | en Clean, cada uno de los cuatro niveles de src/; un anillo importa solo de sí mismo y de los anillos interiores.: entities/, use-cases/, interface-adapters/, frameworks/ | Hacia adentro, anillo por anillo, hasta las objeto cuyos métodos protegen sus propias invariantes, como Project.archive en Clean.. |
| Use-case-first | en Use-case-first, una carpeta por operación en la parte superior de src/, con su ruta, sus reglas y su SQL, sin imports entre acciones.: create-project/, archive-project/, … | Hacia el almacenamiento: cada acción ejecuta su propio SQL. |
Las tres reglas
- Un proyecto se archiva solo si ninguna de sus tareas está sin terminar (
todooin-progress). - Un proyecto archivado no acepta tareas nuevas.
- Una tarea avanza de a un paso (
todo → in-progress → done); una tarea endoneno avanza.
Términos
- Capa (layer)
- grupo de código con un solo rol técnico, como recibir HTTP, aplicar reglas o guardar datos, que solo depende de las capas de abajo.
- Módulo (module)
- grupo de código alrededor de un área de negocio, con sus rutas, reglas y SQL; el resto del código usa solo su punto de entrada público.
- Puerto (port)
- interfaz que la aplicación declara en sus propios términos, para lo que ofrece (las operaciones) o para lo que necesita de afuera (almacenamiento, atomicidad).
- Adaptador (adapter)
- código que traduce entre un puerto y una tecnología concreta, como Express o
node:sqlite. - Caso de uso (use case)
- una cosa que un usuario puede hacer con el sistema; acá, cada una de las seis operaciones del contrato.
- Entidad (entity)
- objeto cuyos métodos protegen sus propias invariantes, como
Project.archiveen Clean. - Regla de dependencia (dependency rule)
- en Clean, las dependencias del código fuente apuntan hacia adentro, hacia las reglas de negocio, nunca hacia frameworks o drivers.
- Acción (action)
- en Use-case-first, una carpeta por operación en la parte superior de
src/, con su ruta, sus reglas y su SQL, sin imports entre acciones. - Núcleo (core)
- en Hexagonal, el código de la aplicación que no conoce ninguna tecnología: reglas, modelo y puertos, en
core/. - Anillo (ring)
- en Clean, cada uno de los cuatro niveles de
src/; un anillo importa solo de sí mismo y de los anillos interiores.
Layered
grupo de código con un solo rol técnico, como recibir HTTP, aplicar reglas o guardar datos, que solo depende de las capas de abajo. técnicas en una sola dirección. Cada request baja del controlador hasta SQLite.
- Cómo está organizada
src/se divide por rol técnico. Una capa solo depende de las de abajo, ymain.tsconecta las piezas. Los servicios usan repositorios concretos, sin interfaces: hay una sola implementación de almacenamiento.- Dónde viven las reglas
- Regla 1 (archivar):
ProjectService.archive. Regla 2 (crear tarea):TaskService.create. Regla 3 (avanzar):TaskService.advance. - Recorrido de archivar un proyecto
http/app.tsenruta la solicitud al controlador.http/project-controller.tsvalida el id conrequireId.services/project-service.tsabre la transacción derepositories/database.ts.repositories/project-repository.tsbusca el proyecto; el servicio decide404o409.repositories/task-repository.tscuenta las tareas sin terminar; si hay alguna,409.updateStatusguarda el cambio;http/errors.tstraduce cualquier error.
- Qué facilita
- Encontrar un tipo de código: todo el SQL está en
repositories/. Leer una regla sin HTTP ni SQL:ProjectService.archivees un método corto. - Qué cuesta
- En esta app, un cambio de funcionalidad se reparte entre las tres capas. El modelo compartido las acopla: un campo nuevo toca
models.ts, el esquema, el SQL y HTTP. - Por dónde empezar a leer
src/http/app.ts(el único lugar que lista las 6 operaciones)src/services/project-service.ts(regla 1 y transacción en un solo método)src/repositories/database.ts(conexión, esquema y helper de transacción)
- Se diferencia de Feature modules en
- que corta
src/por rol técnico, no por área de negocio: la regla de una tarea y su SQL quedan en carpetas distintas. - Código
apps/layered
Feature modules
Código agrupado por área de negocio: projects y tasks, cada una con sus rutas, reglas y SQL.
- Cómo está organizada
- Cada grupo de código alrededor de un área de negocio, con sus rutas, reglas y SQL; el resto del código usa solo su punto de entrada público. está dividido adentro por rol:
*-routes.ts, lógica y*-store.ts. Desde afuera solo se importa suindex.ts.taskspuede importarprojects; al revés, no. - Dónde viven las reglas
- Regla 1 (archivar):
archiveenmodules/projects/projects.ts. Regla 2 (crear tarea) y regla 3 (avanzar):createyadvanceenmodules/tasks/tasks.ts. - Recorrido de archivar un proyecto
modules/projects/project-routes.ts:requireIdrechaza los ids que no son UUID (400).modules/projects/projects.ts:archiveabre la transacción (shared/database.ts).modules/projects/project-store.ts:findProjectByIdlee;archivedecide404o409.main.ts: la función inyectadahasUnfinishedTasksconsulta al módulotasks.modules/tasks/task-store.ts: cuenta las tareas pendientes dentro de la misma transacción.modules/projects/projects.ts:archivedecide la regla 1 (409) o guarda.
- Qué facilita
- Cada tabla vive en un solo módulo, con sus reglas y sus rutas. Los tests de reglas están junto al módulo que protegen.
- Qué cuesta
- Por la regla 1,
projectsrecibe una función detasks, conectada enmain.ts. Los dos módulos dependen deshared/, que según su README podría volverse una capa oculta. La conexiónDatabaseforma parte de la API pública de cada módulo. - Por dónde empezar a leer
src/main.ts(crea los dos módulos y los conecta)src/modules/projects/projects.ts(la regla 1 y por qué no importatasks)src/modules/tasks/tasks.ts(el único import entre módulos; reglas 2 y 3)
- Se diferencia de Layered en
- que
src/se corta primero por área de negocio, no por rol técnico. - Código
apps/feature-modules
Hexagonal
Un núcleo sin tecnología, y alrededor todo lo que habla con HTTP o SQLite.
- Cómo está organizada
core/guarda las reglas y los interfaz que la aplicación declara en sus propios términos, para lo que ofrece (las operaciones) o para lo que necesita de afuera (almacenamiento, atomicidad). que declara.adapters/implementa esos puertos con Express,node:sqlitey una versión en memoria.- Dónde viven las reglas
- Reglas 1, 2 y 3:
ProjectBoard.archiveProject,createTaskyadvanceTask, encore/project-board.ts. HTTP solo valida la forma del pedido. - Recorrido de archivar un proyecto
adapters/http/app.ts: valida el id y llama aarchiveProject.core/project-board.ts: pone los chequeos y la escritura dentro del puertoAtomic.adapters/sqlite/atomic.ts: abreBEGIN IMMEDIATEantes de leer.core/project-board.ts: rechaza si el proyecto no existe o ya está archivado.adapters/sqlite/task-store.ts: cuenta las tareas sin terminar; el núcleo decide la regla 1.adapters/sqlite/project-store.ts: guarda;SqliteAtomicconfirma conCOMMIT.
- Qué facilita
- Las reglas se prueban sin base de datos, sobre el adaptador en memoria. Una sola llamada en
main.tselige el almacenamiento. - Qué cuesta
- Tres puertos y dos adaptadores de almacenamiento para seis operaciones. El adaptador en memoria existe para los tests y tiene que imitar a SQLite.
- Por dónde empezar a leer
src/core/project-board.ts(las operaciones y las reglas)src/core/ports.ts(lo que el núcleo le pide al almacenamiento)src/main.ts(dónde se conectan el núcleo y los adaptadores)
- Se diferencia de Clean en
- el foco: la frontera entre adentro y afuera, y el adaptador reemplazable. Clean pone las reglas en entidades.
- Referencia externa
- Hexagonal Architecture — Alistair Cockburn
- Código
apps/hexagonal
Clean
Cuatro anillos: las reglas en el centro y todas las dependencias apuntando hacia adentro.
- Cómo está organizada
- Las entidades protegen sus invariantes. Cada operación es una clase de una cosa que un usuario puede hacer con el sistema; acá, cada una de las seis operaciones del contrato.. La regla de dependencia prohíbe que un anillo importe de uno exterior, y el lint la hace cumplir.
- Dónde viven las reglas
- Regla 1 (archivar) y regla 2 (crear tarea):
Project.archiveyProject.acceptTaskenentities/project.ts. Regla 3 (avanzar):Task.advanceenentities/task.ts. - Recorrido de archivar un proyecto
frameworks/http/app.ts:route()arma un pedido plano, sin Express, para el controlador.interface-adapters/project-controller.ts: valida el id y llama al caso de uso.use-cases/archive-project.ts:ArchiveProject.executeabre la transacción conTransactionRunner.frameworks/sqlite/: los repositorios devuelven el proyecto y todas sus tareas como entidades.entities/project.ts:Project.archiverechaza si ya está archivado o si quedan tareas sin terminar.- El caso de uso guarda con
update;interface-adapters/error-translation.tstraduce los rechazos a404o409.
- Qué facilita
- Las reglas 1 a 3 se prueban con entidades solas, sin base de datos. Los controladores traducen HTTP sin Express: reciben y devuelven datos planos.
- Qué cuesta
- Más archivos para flujos chicos:
main.tsarma 12 objetos antes de arrancar. La regla 1 carga todas las tareas como entidades, donde alcanzaría unCOUNT. - Por dónde empezar a leer
src/entities/project.ts(reglas 1 y 2 como métodos de la entidad)src/use-cases/archive-project.ts(una operación completa dentro de una transacción)src/use-cases/persistence.ts(los contratos que implementaframeworks/sqlite/)
- Se diferencia de Hexagonal en
- el énfasis: acá las entidades guardan las invariantes y cada operación es una clase. Hexagonal junta las reglas en
ProjectBoardy muestra adaptadores intercambiables. - Referencia externa
- The Clean Architecture — Robert C. Martin
- Código
apps/clean
Use-case-first
Una carpeta por cada en Use-case-first, una carpeta por operación en la parte superior de src/, con su ruta, sus reglas y su SQL, sin imports entre acciones. del usuario; cada archivo tiene su ruta, sus reglas y su SQL.
- Cómo está organizada
- Ninguna acción importa a otra; solo comparten
shared/(conexión, errores y HTTP). Es una interpretación de Screaming Architecture, en la que el árbol muestra qué hace el sistema; no es una arquitectura formal. - Dónde viven las reglas
- Regla 1 (archivar):
archiveProject. Regla 2 (crear tarea):createTask. Regla 3 (avanzar):advanceTask. Cada una está en el archivo de su acción. - Recorrido de archivar un proyecto
- La ruta de
archive-project/archive-project.tsvalida el id conrequireId(shared/http.ts). archiveProjectabre la transacción deshared/database.ts.findProject: si no existe el proyecto,404; si ya está archivado,409.countUnfinishedTasksconsultatasksdirectamente; si hay pendientes,409.updateProjectStatusguardaarchived, y la transacción haceCOMMIT.
- La ruta de
- Qué facilita
- Leer una acción completa, de la ruta al SQL, en un solo archivo. Sumar una acción: una carpeta nueva, más su import y su registro en
main.ts. - Qué cuesta
- Duplicación deliberada: la regla de entrada y "cargar el proyecto o
404" se repiten. Un cambio de columna se busca por texto en cada acción. Que un proyecto archivado tenga solo tareasdonedepende de tres acciones a la vez. - Por dónde empezar a leer
src/main.ts(registra las seis acciones)src/archive-project/archive-project.ts(una acción entera, de arriba abajo)src/shared/database.ts(el esquema y la transacción compartidos)
- Se diferencia de Feature modules en
- que agrupa por verbo, no por sustantivo:
archive-projectconsultataskssin pasar por otro módulo. - Referencia externa
- Screaming Architecture — Robert C. Martin
- Código
apps/use-case-first
Las cinco, lado a lado
Las dos primeras filas miden el mismo request: archivar un proyecto (probalo en el playground). La regla 1 impide archivar un proyecto con tareas sin terminar.
| Medida | Layered | Feature modules | Hexagonal | Clean | Use-case-first |
|---|---|---|---|---|---|
| Archivos que se ejecutan al archivar (éxito / con error) | 7 / 9 | 9 / 10 | 6 / 8 | 11 / 14 | 3 / 4 |
| Cómo cuenta la regla 1 las tareas sin terminar | COUNT … status <> 'done' | COUNT … status <> 'done' | COUNT … status IN (?, ?) | Carga todas las tareas; decide la objeto cuyos métodos protegen sus propias invariantes, como Project.archive en Clean. | COUNT … IN ('todo', 'in-progress') |
Archivos que importan node:sqlite | 3, en repositories/ | 3: shared/database.ts y 2 stores | 5, en adapters/sqlite/ | 4, en frameworks/sqlite/ | 1: shared/database.ts; las 6 acciones escriben su SQL |
Archivos que importan express | 4, en http/ | 4: app.ts, 2 archivos de rutas, shared/http.ts | 2, en adapters/http/ | 2, en frameworks/http/ | 7: las 6 acciones y shared/http.ts |
| Qué controla el lint | Dirección de las grupo de código con un solo rol técnico, como recibir HTTP, aplicar reglas o guardar datos, que solo depende de las capas de abajo.; node:sqlite solo en repositories/ | Puntos de entrada públicos; projects nunca importa tasks | El núcleo importa solo archivos propios y no usa globales de Node; adaptadores independientes | Cada anillo importa solo de sí mismo y de anillos interiores; sin globales de Node adentro | Las acciones no se importan entre sí; node:sqlite solo en shared/database.ts |
| Dónde corren los tests de reglas (archivos / tests) | SQLite real, sin sustituto (2 / 5) | SQLite; projects solo, con un hasUnfinishedTasks falso (3 / 7) | código que traduce entre un puerto y una tecnología concreta, como Express o node:sqlite. en memoria; regla 1 en ambos adaptadores (3 / 19) | Solo entidades, sin almacenamiento (3 / 9) | SQLite, datos de prueba en SQL plano (5 / 7) |
| Archivos / líneas, sin tests ni helpers de test | 14 / 609 | 14 / 688 | 15 / 787 | 25 / 1001 | 11 / 636 |
Cómo leer estos números
- Muestran dónde está el código y cuánto recorre un request. No miden calidad.
- La primera fila cuenta los archivos cuyo código se ejecuta mientras se atiende el request. No incluye el registro de rutas al arrancar ni los imports solo de tipos.
- El tamaño favorece a lo chico. Con 6 operaciones y 3 reglas, las apps con menos estructura parecen más baratas; muchas reglas que crucen operaciones cambiarían el balance.
- Los tests no se comparan por cantidad. Los 19 de Hexagonal incluyen tests que corren una vez por adaptador. Compará cómo se preparan, no los totales.
- El lint revisa imports, y en Hexagonal y Clean también globales. Nunca revisa dónde está escrita la lógica.
- Los conteos salen del código en el commit
93f7d9ey cambian cuando cambian las apps.