Carpetas por feature: la organización que RTK recomienda
La guía de estilo oficial de Redux Toolkit recomienda organizar el código en carpetas por funcionalidad con la lógica de cada dominio en un único fichero de slice, y esa recomendación no es una preferencia estética sino una consecuencia de dos propiedades medibles: localidad del cambio y borrabilidad. Esta lección describe la disposición canónica de app y features, explica por qué escala mejor que el corte por tipo cuando el número de dominios crece, cuantifica el punto en que el corte técnico deja de compensar, y aterriza la frontera de cada carpeta en una superficie pública explícita que se puede vigilar con reglas de lint y con propiedad de equipo.
El patrón de la lección anterior resolvió qué es la unidad de estructura: el módulo de dominio. Queda la pregunta de dónde se coloca esa unidad y qué la acompaña, y ahí la guía de estilo de Redux Toolkit es inusualmente explícita para un documento oficial: carpetas por funcionalidad, con la lógica de cada dominio concentrada en su slice. Lo interesante no es la recomendación, que cualquiera puede copiar, sino su justificación, porque no apela al gusto ni a la tradición sino a dos propiedades que se pueden observar en un repositorio real: cuánto se dispersa un cambio típico y cuánto cuesta borrar una funcionalidad entera. Ambas empeoran de forma superlineal con el corte por tipo y se mantienen casi constantes con el corte por dominio, y esa diferencia de curvas es todo el argumento.
- Reproducir la disposición canónica de
appyfeaturesque recomienda la guía de estilo de Redux Toolkit. - Explicar la escalabilidad del corte por dominio en términos de localidad del cambio y de borrabilidad.
- Situar el punto en que el corte técnico deja de compensar y qué señales lo anuncian.
- Definir la superficie pública de una carpeta y los mecanismos que la mantienen honesta.
La disposición canónica
La estructura que la guía oficial describe tiene solo dos zonas de primer nivel. Una carpeta app con lo que pertenece a la aplicación en su conjunto —la configuración del store, los hooks tipados, el enrutado raíz— y una carpeta features con una subcarpeta por dominio, cada una autosuficiente: su slice, sus componentes, sus hooks, su acceso a datos y sus pruebas. Es la disposición que usan las plantillas oficiales y la que asumen implícitamente los ejemplos de la documentación.
// src/app/store.ts, hooks.ts <- lo que pertenece a toda la app
// src/features/carrito/carritoSlice.ts
// src/features/carrito/CarritoPanel.tsx
// src/features/carrito/useCarrito.ts
// src/features/carrito/index.ts <- la superficie publica
// src/features/usuarios/usuariosSlice.ts
// src/components/ <- UI generica sin dominio
Conviene notar lo que la disposición implica y no dice en voz alta: los componentes de React viven junto al slice que consumen, no en un árbol paralelo de vistas. Esa colocación es la parte que más resistencia genera en equipos acostumbrados a separar presentación de estado, y es también la que más rinde, porque el par componente-slice es precisamente lo que cambia junto. La carpeta components sobrevive, pero reservada a lo que no pertenece a ningún dominio: los botones, los campos, los diálogos que cualquiera reutiliza y que no saben nada del negocio.
Las pruebas siguen la misma lógica y por la misma razón: viven junto a lo que prueban, porque cambian cuando cambia el comportamiento que verifican. Un árbol paralelo de pruebas reproduce exactamente el problema del corte por tipo, con el agravante de que la distancia entre el código y su prueba es la variable que mejor predice si la prueba se actualiza o se abandona. Colocar la prueba al lado del slice no la hace mejor, pero hace mucho más probable que alguien la mantenga.
La carpeta app tiene una definición estrecha que conviene defender: contiene lo que compone la aplicación a partir de sus funcionalidades —la creación del store, los hooks tipados, el enrutado, los proveedores de contexto— y nada que pertenezca a un dominio concreto. En cuanto empieza a acumular utilidades sueltas, tipos de negocio o lógica que alguien no supo dónde colocar, se convierte en el equivalente moderno de la carpeta de miscelánea, con el agravante de que todo el mundo depende de ella por construcción. La prueba es direccional: app puede importar de features, pero ninguna funcionalidad debería necesitar importar de app salvo los hooks tipados del store.
Abre el primer nivel de src y lee los nombres. Si dicen actions, reducers, containers y components, tu árbol te informa de qué librería usas, un dato que ya conocías. Si dicen carrito, usuarios, facturacion y envios, te informa de qué hace el producto, que es la única pregunta cuya respuesta necesitas cuando aterrizas en un repositorio ajeno. Es el criterio que Robert Martin llamó arquitectura que grita, y es un test de diez segundos que ninguna documentación sustituye.
Por qué escala mejor que el corte por tipo
El corte por tipo tiene un mérito real que conviene reconocer antes de descartarlo: con pocos dominios, el número de ficheros por carpeta es pequeño y encontrar cualquier cosa es trivial. El problema aparece cuando el producto crece, porque las dos organizaciones escalan de forma distinta. Con corte por tipo, cada carpeta técnica acumula un fichero por dominio, de modo que todas crecen a la vez y ninguna se puede leer entera; con corte por dominio, cada carpeta nueva es independiente de las existentes y el tamaño de las demás no cambia.
// Corte por tipo: cada carpeta crece con cada dominio nuevo.
// src/actions/carrito.ts, usuarios.ts, envios.ts, facturacion.ts
// src/reducers/carrito.ts, usuarios.ts, envios.ts, facturacion.ts
// src/selectors/carrito.ts, usuarios.ts, envios.ts, facturacion.ts
// anadir un dominio toca cuatro carpetas y no cierra ninguna
// Corte por dominio: cada carpeta nueva es independiente.
// src/features/carrito/, usuarios/, envios/, facturacion/
// anadir un dominio crea un directorio y no altera los demas
La consecuencia práctica se mide en dos ejes. El primero es la localidad del cambio: la proporción de modificaciones que caben dentro de una sola carpeta. Con corte por dominio, la inmensa mayoría de las tareas de producto tocan un directorio y solo uno, lo que reduce el riesgo de conflicto entre ramas y hace que una revisión de código quepa en una pantalla. El segundo es la borrabilidad: retirar una funcionalidad muerta consiste en borrar una carpeta y una línea del store, mientras que con el corte técnico exige rastrear cuatro ficheros y confiar en no haber dejado un tipo huérfano ni un selector fantasma. Como el código que no se puede borrar nunca se borra, la borrabilidad es una propiedad de higiene, no de comodidad.
flowchart TD T[cambio en una funcionalidad] --> T1[corte por tipo] T --> T2[corte por dominio] T1 --> T3[cuatro carpetas tocadas] T3 --> T4[revision dispersa y conflictos entre ramas] T2 --> T5[una carpeta tocada] T5 --> T6[revision local y borrado limpio] style T4 fill:#f38ba8,color:#11111b style T6 fill:#a6e3a1,color:#11111b
Hay un matiz que suele pasarse por alto: el corte por dominio no reduce el número total de dependencias del sistema, solo cambia dónde caen. Lo que antes era una relación difusa entre carpetas técnicas se convierte en una relación explícita entre funcionalidades, y esa relación se puede contar, dibujar y limitar. La organización no elimina el acoplamiento —ninguna lo hace—, lo hace observable, que es la condición previa para gestionarlo.
Hay un tercer eje que no es técnico y decide tanto como los otros dos: la propiedad. Una carpeta por dominio se puede asignar a un equipo en un fichero de propietarios de código, y a partir de ahí las revisiones llegan a quien conoce las reglas de ese negocio. Un corte por tipo no admite esa asignación, porque la carpeta reducers pertenece a todo el mundo, que en la práctica equivale a nadie. La estructura del repositorio y la estructura del equipo tienden a converger —el fenómeno que Conway describió en 1968— y el corte por dominio es simplemente el que hace esa convergencia deseable en vez de accidental.
Localidad del cambio
La mayoría de tareas cabe en una carpeta. Menos conflictos entre ramas y revisiones que se leen de un tirón.
Borrabilidad
Retirar una funcionalidad es borrar un directorio. Lo que no se puede borrar limpio nunca se borra.
Propiedad clara
Una carpeta, un equipo responsable. El corte técnico pertenece a todos y por tanto a nadie.
Legibilidad del dominio
El primer nivel del árbol describe el producto, no la librería con la que está escrito.
Dónde está el punto de equilibrio
Sería deshonesto presentar el corte por dominio como universalmente superior sin nombrar su umbral. En una aplicación con dos o tres dominios y una decena de ficheros, ambas organizaciones son equivalentes y la discusión es puro ritual; el coste del corte por tipo es lineal en el número de dominios y solo se vuelve doloroso cuando ese número pasa de unos pocos. La señal fiable no es el recuento de ficheros sino la experiencia del cambio: cuando notas que una tarea de producto normal te obliga a abrir sistemáticamente cuatro carpetas, ya cruzaste el umbral y llevas tiempo pagándolo.
Conviene además no confundir esta discusión con la de las arquitecturas por capas que circulan bajo nombres propios, con niveles fijos y reglas de dependencia estrictas entre ellos. Esas metodologías resuelven un problema de escala mayor y traen un coste de ceremonia que la mayoría de los productos no necesita; la recomendación de Redux Toolkit es deliberadamente más modesta y se queda en el corte principal. Adoptar el corte por dominio no obliga a adoptar ninguna de ellas, y quien las adopte descubrirá que su primer nivel es, precisamente, este mismo corte.
El error simétrico existe y también conviene nombrarlo: fragmentar en veinte funcionalidades una aplicación que tiene tres, con carpetas de un fichero cada una y un laberinto de importaciones cruzadas entre ellas. Una carpeta por dominio solo rinde si el dominio es real, es decir, si tiene una razón de cambio propia y alguien podría explicarlo sin mencionar los demás. La granularidad correcta es la que hace que la mayoría de las importaciones sean internas a la carpeta; si la mayoría cruza fronteras, tus fronteras están mal trazadas, y el remedio es fusionar, no añadir más niveles.
Una vez el corte principal es por dominio, si dentro colocas los componentes en una subcarpeta, si separas hooks de utilidades o si dejas todo plano tiene poca consecuencia sistémica, porque esas decisiones no cruzan la frontera y se pueden revisar sin coordinar con nadie. Es una propiedad valiosa por sí misma: el corte por dominio convierte un montón de decisiones globales en decisiones locales y reversibles. Discutir durante horas la organización interna de una carpeta mientras el corte principal sigue siendo por tipo es optimizar el detalle equivocado.
La frontera de la carpeta
Conviene distinguir el índice que declara una superficie del barril que reexporta todo lo que hay dentro por comodidad. El segundo tiene los inconvenientes del primero sin ninguna de sus ventajas: no restringe nada, dificulta el análisis de alcanzabilidad del empaquetador y puede arrastrar al paquete inicial módulos que nadie usa. Un índice bien escrito es corto, se lee como una declaración de intenciones y cambia poco; si el tuyo tiene treinta líneas y crece cada semana, has escrito un barril y la frontera sigue sin existir.
Una carpeta no es una frontera por existir; lo es cuando hay una diferencia observable entre lo que expone y lo que guarda. El mecanismo habitual es un fichero índice que reexporta la superficie pública —los componentes de entrada, los selectores para otros dominios, las acciones que otros pueden despachar— mientras el resto permanece accesible solo desde dentro. Sin ese índice, cualquier fichero puede importar cualquier ruta profunda de otra funcionalidad, y la carpeta se degrada a una etiqueta decorativa que no restringe nada.
// features/carrito/index.ts: la unica puerta legitima
export { default as CarritoPanel } from './CarritoPanel'
export { agregar, vaciar } from './carritoSlice'
export { selectTotal } from './carritoSlice'
// el reducer lo importa solo app/store.ts
export { default as carritoReducer } from './carritoSlice'
// Regla de importacion que convierte la convencion en un error de lint.
export const zonasRestringidas = [
{
target: './src/features/carrito',
from: './src/features/usuarios',
except: ['./index.ts'],
message: 'importa solo desde el indice publico de la funcionalidad',
},
]
Una convención que nadie verifica se erosiona en semanas, así que la frontera necesita un guardián automático. Las reglas de importación de los linters permiten prohibir que una funcionalidad importe rutas internas de otra, o incluso que se importen entre sí en absoluto, obligando a que toda comunicación pase por la capa app o por módulos compartidos explícitos. Merece la pena empezar con la restricción más fuerte que el equipo tolere: aflojar una regla es una línea de configuración, y reparar un año de importaciones profundas es un proyecto. Qué puede legítimamente cruzar esa puerta —y qué acoplamientos se cuelan aunque la puerta exista— es exactamente la materia de la lección siguiente.
Es tentador tratar el árbol de directorios como una cuestión menor, resuelta al empezar el proyecto y no vuelta a mirar, frente a las decisiones que parecen serias: qué librería de estado, qué patrón de efectos, qué estrategia de caché. Esa jerarquía está invertida. Un equipo consulta la estructura de carpetas decenas de veces al día, cada vez que crea un fichero, busca uno, revisa un cambio o decide dónde poner algo nuevo, y en cada una de esas consultas la estructura emite una recomendación implícita que casi nadie discute porque llega en forma de camino de menor resistencia. Si el árbol dice que los reducers viven juntos, el siguiente reducer irá ahí aunque su dominio esté en otra parte; si dice que cada dominio es autosuficiente, el impulso será mantenerlo así. Ninguna guía de estilo escrita influye tanto como esa presión silenciosa, porque las guías se leen una vez y el árbol se ve a todas horas. De ahí que el corte principal sea la decisión arquitectónica de mayor retorno por unidad de esfuerzo en una aplicación de producto: no cambia lo que el código puede hacer, cambia la probabilidad de que la próxima decisión de cualquiera sea la correcta. La arquitectura no vive en el documento que describe el sistema, vive en la fricción que el sistema opone a hacer las cosas de otra manera, y en una base de código de frontend esa fricción es, casi siempre, la forma del árbol de directorios.
- Toma los últimos veinte cambios fusionados en tu repositorio y anota, para cada uno, cuántas carpetas de primer nivel tocó. Calcula la media.
- Clasifica esos cambios por funcionalidad y comprueba cuántos habrían cabido en una sola carpeta con el corte por dominio.
- Elige la funcionalidad más aislada de tu aplicación y reorganízala como carpeta autosuficiente con su slice, sus componentes y su índice público.
- Aplica la prueba de borrado sobre ella en una rama descartable y anota cuántas importaciones externas se rompen y desde dónde vienen.
- Añade una regla de lint que prohíba importar rutas internas de esa carpeta y observa cuántas violaciones existen hoy.
- Escribe el criterio con el que decidirías si un dominio nuevo merece carpeta propia o pertenece a una existente, y contrasta la regla contra tres casos reales de tu producto.