<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Josue Correa</title>
    <description>The latest articles on DEV Community by Josue Correa (@josuecorreaq).</description>
    <link>https://dev.to/josuecorreaq</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4201065%2F2038a0b0-d1ff-4872-ae21-3191f02c006a.png</url>
      <title>DEV Community: Josue Correa</title>
      <link>https://dev.to/josuecorreaq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/josuecorreaq"/>
    <language>en</language>
    <item>
      <title>Monolito modular en Laravel: nueve dominios, 110 tests</title>
      <dc:creator>Josue Correa</dc:creator>
      <pubDate>Thu, 22 Oct 2026 18:33:00 +0000</pubDate>
      <link>https://dev.to/josuecorreaq/monolito-modular-en-laravel-nueve-dominios-110-tests-1001</link>
      <guid>https://dev.to/josuecorreaq/monolito-modular-en-laravel-nueve-dominios-110-tests-1001</guid>
      <description>&lt;p&gt;La &lt;a href="https://josuecorreaq.com/proyectos/plataforma-gestion-crediticia/" rel="noopener noreferrer"&gt;Plataforma Integral de Gestión Crediticia&lt;/a&gt; empezó como un monolito Laravel organizado por tipo técnico. Tenía 45 modelos en &lt;code&gt;app/Models&lt;/code&gt;, 83 archivos en &lt;code&gt;Services&lt;/code&gt;, 22 en &lt;code&gt;Utilities&lt;/code&gt; y 14 en &lt;code&gt;Filters&lt;/code&gt;. Las reglas de cada flujo quedaban repartidas entre esas carpetas, los Form Requests y los controllers, y cualquier cambio tocaba código que no pertenecía a ningún dominio.&lt;/p&gt;

&lt;p&gt;En poco más de cinco semanas, ese código pasó a nueve módulos, uno por dominio de negocio. Hoy esas cuatro carpetas no existen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Antes: por tipo técnico              Después: por dominio
app/                                 app/Modules/
  Models/      45                      01 … 09   Http · Application · Domain · Infrastructure
  Services/    83                    app/Shared/
  Utilities/   22                      19 clases comunes
  Filters/     14
  + controllers y form requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Las cuatro carpetas por tipo técnico ya no existen. Su código pasó a nueve módulos de dominio que repiten las mismas cuatro capas, y lo que comparten dos o más módulos quedó en Shared.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué un monolito modular
&lt;/h2&gt;

&lt;p&gt;El problema era de organización, no de escala. Por eso descarté las dos salidas más comunes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microservicios.&lt;/strong&gt; Separan físicamente y escalan cada pieza por su cuenta, pero piden despliegues independientes, red, contratos remotos y manejo de fallos distribuidos. El sistema no necesitaba nada de eso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hexagonal estricta.&lt;/strong&gt; Marca límites muy explícitos, pero suma interfaces y repositorios que solo tienen una implementación posible: Eloquent.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El monolito modular se queda con lo útil de cada una: fronteras de dominio claras, un solo despliegue y Eloquent donde corresponde.&lt;/p&gt;

&lt;h2&gt;
  
  
  Módulos por dominio, no por capa
&lt;/h2&gt;

&lt;p&gt;Cada módulo es un dominio de negocio y repite las mismas cuatro capas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/Modules/&amp;lt;Dominio&amp;gt;/
  Domain/          reglas de negocio puras
  Application/     casos de uso: Actions y Queries
  Infrastructure/  Eloquent, consultas, adapters
  Http/            controllers y form requests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hoy el código cumple la dirección de dependencias:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Domain&lt;/code&gt; no importa ninguna otra capa, ni HTTP, la fachada &lt;code&gt;DB&lt;/code&gt;, &lt;code&gt;Auth&lt;/code&gt; o Eloquent. Del framework solo toma utilidades como &lt;code&gt;Collection&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Application&lt;/code&gt; nunca importa &lt;code&gt;Http&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Http&lt;/code&gt; no persiste datos ni importa &lt;code&gt;Infrastructure&lt;/code&gt;, salvo excepciones inventariadas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No todo está cerrado: &lt;code&gt;Infrastructure&lt;/code&gt; todavía importa &lt;code&gt;Application&lt;/code&gt; en nueve lugares, que quedan por revisar.&lt;/p&gt;

&lt;p&gt;Lo que comparten dos o más módulos vive en &lt;code&gt;Shared&lt;/code&gt;. Tiene dos reglas, y ninguna se cumple al cien por ciento:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Shared&lt;/code&gt; no depende de ningún módulo.&lt;/strong&gt; La excepción son los helpers de autorización, que necesitan al usuario autenticado; esos imports figuran en una lista permitida de un test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nada entra a &lt;code&gt;Shared&lt;/code&gt; si lo usa un solo módulo.&lt;/strong&gt; De sus 19 clases, 16 la cumplen. Las otras tres las usa un único módulo y son candidatas a volver a él.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cada dominio es dueño de sus datos. Un módulo puede ser dueño de un maestro de datos, que otro módulo usa para evaluar. El segundo no toca ese maestro: guarda su propia copia de los valores que usó, y cuando necesita dejar constancia en el maestro lo pide a través de una Action pública del dueño.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo hablan los módulos entre sí
&lt;/h2&gt;

&lt;p&gt;Un módulo no debería entrar a las clases internas ni a las tablas de otro, sino consumir lo que el otro publica en su capa Application. En el código actual, casi cuatro de cada cinco imports entre módulos apuntan a esa capa: sobre todo Queries, seguidas de DTOs, Actions y contratos.&lt;/p&gt;

&lt;p&gt;El resto son excepciones conocidas. La principal es el usuario autenticado, un modelo que viaja por todos los módulos sin que ninguno lo consulte directamente. Las demás son algunos bindings de ruta y relaciones Eloquent de la transición. Ninguna aparece sola: las acotan los mismos tests que vigilan las fronteras.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fronteras que no dependen de la disciplina
&lt;/h2&gt;

&lt;p&gt;Las reglas de arriba se pueden romper sin querer en cualquier PR. Por eso las vigilan &lt;strong&gt;110 tests de arquitectura&lt;/strong&gt;, que leen el código fuente y fallan cuando una frontera se cruza:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependencias entre módulos.&lt;/strong&gt; Los tres módulos del flujo crediticio no pueden importar la infraestructura de otro dueño ni formar ciclos entre ellos. Para el resto, un test fija el inventario exacto de dependencias: 28 aristas. Cualquier dependencia nueva lo hace fallar y obliga a decidirla a propósito.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tablas.&lt;/strong&gt; Una consulta cruda como &lt;code&gt;DB::table('pagos')&lt;/code&gt; no importa ninguna clase, así que pasa por delante de cualquier guarda que lea imports. Otro test arma el mapa de tabla a dueño con el &lt;code&gt;getTable()&lt;/code&gt; de cada modelo, y falla si un módulo consulta en crudo una tabla ajena. Su lista de excepciones está vacía, y un test exige que siga así.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capa Http.&lt;/strong&gt; Un controller coordina la petición, el caso de uso y la respuesta. No persiste datos y no usa &lt;code&gt;app()&lt;/code&gt; como service locator. La excepción principal es el route model binding, porque Laravel necesita el tipo del modelo en la firma. Pero el tipo da identidad, no acceso: el controller puede leer &lt;code&gt;$modelo-&amp;gt;id&lt;/code&gt;, no llamar a &lt;code&gt;::query()&lt;/code&gt; ni a &lt;code&gt;-&amp;gt;save()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Capa Application.&lt;/strong&gt; Los colaboradores llegan por el constructor y no se crean con &lt;code&gt;new&lt;/code&gt; dentro de un método, para que se puedan sustituir en las pruebas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El inventario de dependencias es la guarda más simple de entender. Esta es una versión simplificada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_el_inventario_de_dependencias_no_cambia&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$aristas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;archivosPorModulo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$modulo&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$archivos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$archivos&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$archivo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;modulosImportados&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$archivo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$importado&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$importado&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nv"&gt;$modulo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nv"&gt;$aristas&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$modulo&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$importado&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nv"&gt;$aristas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$aristas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$aristas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertSame&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="s1"&gt;'ModuloA -&amp;gt; ModuloB'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'ModuloA -&amp;gt; ModuloC'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c1"&gt;// ... una arista por línea, 28 en total&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$aristas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cada guarda llegó con su movimiento
&lt;/h2&gt;

&lt;p&gt;Las guardas no se escribieron todas al principio. Cada una llegó justo después del movimiento que protege:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Al vaciar &lt;code&gt;Services&lt;/code&gt;, &lt;code&gt;Utilities&lt;/code&gt; y &lt;code&gt;Filters&lt;/code&gt; en los módulos, entró la guarda de la capa Http, para que la lógica no volviera a los controllers.&lt;/li&gt;
&lt;li&gt;Cuando los modelos Eloquent dejaron &lt;code&gt;app/Models&lt;/code&gt; y pasaron a su módulo dueño, entraron la guarda de propiedad de modelos y el inventario de dependencias.&lt;/li&gt;
&lt;li&gt;Al día siguiente entró la guarda de tablas. Las otras guardas leen imports, y una consulta cruda no importa ninguna clase, así que se saltaba las tres.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;El frontend no notó ninguno de esos movimientos. Eso lo sostuvo el &lt;a href="https://josuecorreaq.com/notas/contrato-de-rutas-laravel/" rel="noopener noreferrer"&gt;test de contrato de rutas&lt;/a&gt;, que fija método, URI, controlador y middleware de cada ruta.&lt;/p&gt;

&lt;h2&gt;
  
  
  El grafo no es perfecto, y el test lo dice
&lt;/h2&gt;

&lt;p&gt;Sería cómodo decir que el grafo de dependencias entre módulos no tiene ciclos. No es cierto, y el test no lo afirma. Solo el flujo crediticio es acíclico. Entre los demás quedan acoplamientos de la transición: dos módulos transversales se apuntan mutuamente por las relaciones del usuario.&lt;/p&gt;

&lt;p&gt;Un ciclo se aceptó a propósito. Una operación que pertenece a un módulo necesita leer el resultado de otros dos, y esos dos ya dependen del primero. Invertir la dependencia con contratos obligaba a duplicar en el primer módulo los DTO de los otros dos. Como el consumo es solo de la capa Application, el ciclo se quedó, con la explicación escrita en el propio test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Una guarda rota también pasa
&lt;/h2&gt;

&lt;p&gt;Un detector que deja de detectar no falla: queda verde y vacío, exactamente igual que un código sin violaciones. Por eso las guardas importantes tienen un segundo test que les pasa ejemplos conocidos y comprueba que los encuentra:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;test_el_detector_reconoce_las_consultas_crudas&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$fuente&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;&amp;lt;&amp;lt;&amp;lt;'PHP'
    DB::table('pagos')-&amp;gt;get();
    DB::connection('mysql')-&amp;gt;table('facturas')-&amp;gt;first();
    PHP;&lt;/span&gt;

    &lt;span class="nv"&gt;$tablas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;tablasReferidas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$fuente&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertContains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'pagos'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tablas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;assertContains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'facturas'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$tablas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La misma idea se aplica a las listas de excepciones. Una entrada que sobrevive al cambio que la justificaba deja de proteger y empieza a esconder. Por eso un test exige que cada excepción siga conteniendo el patrón que excusa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que cuesta
&lt;/h2&gt;

&lt;p&gt;Las guardas tienen un precio. Cada dependencia nueva entre módulos obliga a editar el inventario, y esa fricción es intencional: convierte un import casual en una decisión visible en el diff. Los 110 tests también hay que mantenerlos cuando el código cambia de forma legítima.&lt;/p&gt;

&lt;p&gt;Y el trabajo no está terminado. Quedan los acoplamientos de la transición, los nueve imports de &lt;code&gt;Infrastructure&lt;/code&gt; hacia &lt;code&gt;Application&lt;/code&gt; y las tres clases de &lt;code&gt;Shared&lt;/code&gt; que usa un solo módulo. Ninguno está escondido: cada uno figura en un test o en esta nota.&lt;/p&gt;

&lt;p&gt;El resto de la plataforma, con sus tres niveles de acceso y su arquitectura por capas, está en el &lt;a href="https://josuecorreaq.com/proyectos/plataforma-gestion-crediticia/" rel="noopener noreferrer"&gt;caso de estudio&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Soy Josué Correa, desarrollador backend en Piura, Perú. Escribo sobre Laravel, APIs y arquitectura en &lt;a href="https://josuecorreaq.com/" rel="noopener noreferrer"&gt;josuecorreaq.com&lt;/a&gt;, donde la figura de esta nota es interactiva.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>laravel</category>
      <category>php</category>
      <category>architecture</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
