Son dos conceptos que operan en dimensiones distintas:
- Fetaure-based: se encarga de agrupar el código, organizarlo, estructurarlo.
- Arquitectura hexagonal: aisla la lógica de negocio de los detalles técnicos (frameworks, APIs, almacenamiento).
A continuación voy a intentar explicarlo, tomaré como ejemplo un proyecto implementado en vue.
Arquitectura Feature-Based (organización por características)
Problema que resuelve: el caos de organizar por tipo de archivo (components/, composables/, views/ como carpetas globales), donde trabajar en una sola funcionalidad obliga a saltar constantemente entre carpetas dispersas por todo el proyecto.
Organiza el código según funcionalidades del producto, no según tipo de archivo.
src/
features/
pedidos/
components/
ListaPedidos.vue
DetallePedido.vue
composables/
usePedidos.ts
pedidos.types.ts
usuarios/
components/
PerfilUsuario.vue
composables/
useUsuarios.ts
pagos/
...
Características:
- Todo lo relacionado con una funcionalidad vive junto (componentes, composables, tipos, tests).
- Facilita encontrar y modificar código relacionado con una feature concreta.
- Reduce el acoplamiento entre features distintas.
- Es más una convención de organización de carpetas que una filosofía arquitectónica profunda.
Cuándo usarla: proyectos pequeños o medianos, con lógica de negocio relativamente simple, o equipos que priorizan velocidad de desarrollo y facilidad de navegación sobre el aislamiento estricto de capas.
Arquitectura Hexagonal (Ports & Adapters)
Problema que resuelve: la lógica de negocio mezclada dentro de componentes .vue, que hace difícil testear reglas de negocio sin levantar todo el árbol de componentes, y que acopla tu producto a una API o proveedor concretos.
Su objetivo es aislar la lógica de negocio (dominio) de los detalles técnicos externos: de la API concreta, del almacenamiento local, o incluso del framework de UI que uses (en este caso, Vue). No hace falta crear subcarpetas para conseguirlo — basta con separar la responsabilidad por el nombre del fichero:
src/
features/
pedidos/
pedidos.manager.ts ← reglas de negocio puras, sin Vue ni fetch
pedidos.repository.ts ← el contrato: qué necesita el manager, sin decir cómo
pedidos.adapter.ts ← implementación real (llama a la API, a localStorage...)
composables/
usePedidos.ts ← conecta el manager con Vue
components/
PedidosView.vue
¿Qué hace cada fichero? (con el ejemplo de pedidos)
pedidos.manager.ts— Las reglas del negocio, en concreto: el total se calcula sumandoprecio × cantidadde cada línea; un pedido solo se puede cancelar si su estado es “pendiente”; un pedido no puede tener líneas con cantidad negativa. Son verdades del negocio que se cumplen siempre, sin importar de dónde vengan los datos ni cómo se pinten en pantalla.pedidos.repository.ts— El contrato: dice qué necesita el manager para funcionar, por ejemplo “poder buscar un pedido por id” y “poder guardarlo”, pero no dice cómo se hace ni de dónde vienen los datos.pedidos.adapter.ts— Aquí es donde realmente se llama a la API (la petición HTTP) o donde se lee y escribe enlocalStorage. Si mañana necesitas un segundo adaptador (por ejemplo, modo offline), lo añades junto al resto comopedidos.local.adapter.ts.composables/— Decide qué adaptador usar hoy, mantiene el estado reactivo (si está cargando, cuál es el pedido actual) y conecta las acciones del componente con las reglas del manager y el adaptador elegido.components/— Pinta lo que el composable le da y dispara sus funciones cuando hay una interacción. No decide nada por su cuenta.
Cómo dependen los ficheros entre sí
components/PedidosView.vue
│ usa
▼
composables/usePedidos.ts
│ │
│ usa reglas │ instancia adaptador
▼ ▼
pedidos.manager.ts pedidos.adapter.ts
▲ │
│ │ implementa
│ ▼
└──────────── pedidos.repository.ts
(el repository referencia tipos del manager)
Las flechas de dependencia siempre apuntan hacia pedidos.manager.ts, nunca al revés: el manager no sabe que existen pedidos.repository.ts, pedidos.adapter.ts ni los composables. El adapter depende de dos cosas — del repository, porque lo implementa, y del manager, porque construye o devuelve sus entidades. El composable conecta el manager y el adapter, pero esa conexión pasa siempre a través del repository.
Lo que importa realmente es la dirección de las dependencias (
adapter → repository → manager), no los ficheros concretos que uses para conseguirlo.
No hace falta implementar los tres ficheros desde el primer día. Si el proyecto es pequeño, puedes empezar solo con pedidos.manager.ts y composables/, e ir añadiendo pedidos.repository.ts y pedidos.adapter.ts más adelante, cuando de verdad necesites poder intercambiar la fuente de datos. Simplifica al principio, añade capas cuando la complejidad real lo pida.
Si mañana necesitas modo offline, creas un nuevo adaptador que implemente el mismo repository y lo inyectas en el composable en vez del adaptador HTTP. El componente .vue no cambia una sola línea, y tampoco cambia la lógica de negocio en pedidos.manager.ts.
Características:
- El manager no sabe nada de Vue, de fetch, ni de dónde vienen los datos.
- El composable actúa de puerto de entrada: conecta el manager con la reactividad de Vue.
- El componente
.vuees solo un adaptador de presentación, intercambiable. - Testear las reglas de negocio del manager no requiere montar ningún componente ni mockear el DOM.
Cuándo usarla: proyectos con reglas de negocio no triviales (cálculos, validaciones, estados complejos), equipos que necesitan alta testabilidad, o cuando se prevén cambios de fuente de datos (API, GraphQL, local-first, múltiples backends).
La diferencia clave
| Aspecto | Feature-Based | Hexagonal |
|---|---|---|
| Qué organiza | Cómo se agrupan los archivos/carpetas | Cómo se separan las responsabilidades (negocio vs Vue/API/storage) |
| Pregunta que responde | ”¿Dónde está el código de X funcionalidad?" | "¿Qué tan aislado está mi lógica de negocio de Vue y de la API?” |
| Nivel | Organizacional / estructura de proyecto | Arquitectónico / dependencias y flujo de datos |
| Foco principal | Cohesión por funcionalidad de producto | Independencia del dominio respecto al framework y la infraestructura |
| Origen | Convención práctica de organización | Acuñada por Alistair Cockburn (~2005) |
| En Vue, se traduce en | Carpetas features/pedidos/, features/usuarios/ | pedidos.manager.ts + pedidos.repository.ts + pedidos.adapter.ts + composables/ como puente hacia los componentes |
Combinándolas: lo mejor de ambos mundos
No son excluyentes — de hecho, es la combinación más natural en un proyecto Vue real: organizas por feature, y dentro de cada feature, aplicas la separación hexagonal.
src/
features/
pedidos/
pedidos.manager.ts
pedidos.repository.ts
pedidos.adapter.ts
composables/
components/
usuarios/
usuarios.manager.ts
usuarios.repository.ts
usuarios.adapter.ts
composables/
components/
pagos/
pagos.manager.ts
pagos.repository.ts
pagos.adapter.ts
composables/
components/
Con esta estructura obtienes:
- Navegabilidad: todo el código de “pedidos” está junto, fácil de localizar como design engineer que trabaja tanto en UI como en lógica.
- Aislamiento: dentro de “pedidos”,
pedidos.manager.tsno depende de fetch, de axios ni de Vue. - Composables reutilizables:
usePedidos()puede usarse en varios componentes distintos (lista, detalle, dashboard) sin duplicar lógica. - Testabilidad real: testeas
pedidos.manager.tscon Vitest puro, sin@vue/test-utils, y testeas los composables mockeando elrepository— sin necesidad de red ni backend real.
Esta combinación es especialmente valiosa en proyectos que empiezan simples pero se espera que crezcan: puedes arrancar con componentes y composables planos por feature, e ir separando manager, repository y adapter feature por feature, a medida que la lógica de negocio se vuelve más compleja — sin necesidad de una reescritura completa.
Conclusión
- Feature-based responde a “¿cómo organizo mis carpetas para encontrar las cosas rápido?”
- Hexagonal responde a “¿cómo protejo mi lógica de negocio de Vue, de la API y del almacenamiento?”
En un proyecto Vue, la combinación natural es: carpetas por feature por fuera, y manager → repository → adapter → composables → componentes por dentro de cada una.