<?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: Alejandro Lafourcade Despaigne</title>
    <description>The latest articles on DEV Community by Alejandro Lafourcade Despaigne (@alafourcadev).</description>
    <link>https://dev.to/alafourcadev</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F368299%2F65296deb-2359-486a-b2a3-263ca7cccbfb.png</url>
      <title>DEV Community: Alejandro Lafourcade Despaigne</title>
      <link>https://dev.to/alafourcadev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alafourcadev"/>
    <language>en</language>
    <item>
      <title>Día 14: Cambiaste una clase y rompiste 10 más. Efecto dominó.</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:23:36 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-14-cambiaste-una-clase-y-rompiste-10-mas-efecto-domino-3oab</link>
      <guid>https://dev.to/alafourcadev/dia-14-cambiaste-una-clase-y-rompiste-10-mas-efecto-domino-3oab</guid>
      <description>&lt;p&gt;Cambiaste un campo en un DTO. Un rename inocente. Y de repente tienes 10 clases con errores de compilación, 3 tests rotos y un compañero preguntándote "¿qué tocaste?".&lt;/p&gt;

&lt;p&gt;No tocaste nada raro. Tocaste &lt;strong&gt;una clase&lt;/strong&gt;. El problema es que esa clase está conectada a todo como si fuera el centro del universo. Y en tu codebase, probablemente lo es.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto no es solo de Java
&lt;/h2&gt;

&lt;p&gt;El anti-pattern es el mismo en cualquier stack. Solo cambia la sintaxis:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java / Spring Boot&lt;/strong&gt;: un &lt;code&gt;UserDTO&lt;/code&gt; compartido entre controller, service y repository&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript / NestJS&lt;/strong&gt;: un &lt;code&gt;user.dto.ts&lt;/code&gt; que se usa en controllers, guards, pipes y servicios&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python / Django&lt;/strong&gt;: un serializer gigante que sirve para crear, leer, actualizar y exportar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C# / .NET&lt;/strong&gt;: un &lt;code&gt;UserModel&lt;/code&gt; compartido entre API, business layer y data access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby on Rails&lt;/strong&gt;: un modelo &lt;code&gt;User&lt;/code&gt; con &lt;code&gt;as_json&lt;/code&gt; sobreescrito 4 veces para diferentes contextos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP / Laravel&lt;/strong&gt;: un &lt;code&gt;UserResource&lt;/code&gt; que hace transform distinto según quién lo llame&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El patrón es siempre el mismo: un objeto compartido entre capas con necesidades distintas. Cuando lo tocas, se rompe todo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alto acoplamiento, baja cohesión
&lt;/h2&gt;

&lt;p&gt;Dos conceptos que suenan a examen de facultad pero que definen si tu código es mantenible o una bomba de tiempo:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acoplamiento&lt;/strong&gt; = cuánto depende una clase de otra. Si cambiar A rompe B, C y D, están acopladas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cohesión&lt;/strong&gt; = cuánto se relacionan las cosas dentro de una clase. Si una clase hace cosas que no tienen nada que ver entre sí, tiene baja cohesión.&lt;/p&gt;

&lt;p&gt;Lo ideal: bajo acoplamiento, alta cohesión. La realidad de la mayoría de los proyectos: exactamente lo opuesto.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ejemplo: un DTO que gobierna todo
&lt;/h2&gt;

&lt;p&gt;Un solo &lt;code&gt;UserDTO&lt;/code&gt; compartido entre controller, service y hasta la capa de persistencia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;AddressDTO&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;creditCardLast4&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y se usa en todos lados:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="n"&gt;dto&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;OrderDTO&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getOrders&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendWelcome&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReportService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Report&lt;/span&gt; &lt;span class="nf"&gt;generateUserReport&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserDTO&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora renombras &lt;code&gt;name&lt;/code&gt; a &lt;code&gt;fullName&lt;/code&gt;. O eliminas &lt;code&gt;creditCardLast4&lt;/code&gt; porque ya no lo necesitas en la respuesta. O agregas un campo.&lt;/p&gt;

&lt;p&gt;Todos esos servicios se rompen. El controller se rompe. Los tests se rompen. El frontend se rompe. Todo porque compartes &lt;strong&gt;un objeto entre 5 capas con necesidades distintas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El &lt;code&gt;OrderService&lt;/code&gt; no necesita el password. El &lt;code&gt;NotificationService&lt;/code&gt; no necesita las órdenes. El &lt;code&gt;ReportService&lt;/code&gt; no necesita la tarjeta de crédito. Pero todos dependen del mismo DTO gigante.&lt;/p&gt;

&lt;p&gt;En TypeScript sería el mismo problema con una interfaz &lt;code&gt;User&lt;/code&gt; compartida entre routes, middleware y services. En Python, un serializer con 15 campos que se usa para crear, listar y exportar. La tecnología cambia, el error no.&lt;/p&gt;

&lt;h2&gt;
  
  
  La solución: cada capa recibe lo que necesita
&lt;/h2&gt;

&lt;p&gt;import InArticleCTA from '../../components/InArticleCTA.astro';&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Lo que el frontend envía para crear un usuario&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;CreateUserRequest&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// Lo que el endpoint devuelve&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;UserResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// Lo que el servicio de notificaciones necesita&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;UserNotificationInfo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// Lo que el servicio de reportes necesita&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nf"&gt;UserReportData&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;createdAt&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;totalOrders&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada clase recibe exactamente lo que necesita. Ni más, ni menos.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserResponse&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestBody&lt;/span&gt; &lt;span class="nc"&gt;CreateUserRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/users/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;UserResponse&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendWelcome&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserNotificationInfo&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora renombras un campo en &lt;code&gt;CreateUserRequest&lt;/code&gt;. ¿Qué se rompe? Solo el controller y su test. Nada más. El servicio de notificaciones ni se entera. El de reportes sigue funcionando. Cero efecto dominó.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Antes&lt;/th&gt;
&lt;th&gt;Después&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Clases afectadas por un cambio de DTO&lt;/td&gt;
&lt;td&gt;10+&lt;/td&gt;
&lt;td&gt;1-2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Campos innecesarios viajando por la red&lt;/td&gt;
&lt;td&gt;5-6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Riesgo de exponer password en respuesta&lt;/td&gt;
&lt;td&gt;Alto&lt;/td&gt;
&lt;td&gt;Nulo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  La otra cara: cohesión
&lt;/h2&gt;

&lt;p&gt;Acoplamiento bajo sin cohesión alta no sirve de mucho. Mira esta clase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;createUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateUserRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Report&lt;/span&gt; &lt;span class="nf"&gt;generateMonthlyReport&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;syncWithExternalCRM&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;exportToCsv&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Crear usuarios, mandar mails, generar reportes, sincronizar con un CRM y exportar a CSV. ¿Qué tienen en común? Que involucran usuarios. Y eso no es suficiente para estar en la misma clase.&lt;/p&gt;

&lt;p&gt;Alta cohesión significa que todo lo que hace una clase está estrechamente relacionado. Los métodos trabajan sobre los mismos datos para el mismo propósito.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateUserRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpdateUserRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserNotificationService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendDeactivationEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserExportService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;exportToCsv&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserExportCriteria&lt;/span&gt; &lt;span class="n"&gt;criteria&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Report&lt;/span&gt; &lt;span class="nf"&gt;generateMonthlyReport&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada clase tiene una razón para existir. Cada método trabaja con los mismos conceptos. Eso es cohesión alta.&lt;/p&gt;

&lt;p&gt;Si ayer leíste el artículo del God Object, esto te va a sonar familiar. SRP, acoplamiento y cohesión son tres formas de mirar el mismo problema: clases que hacen demasiado y están conectadas a todo.&lt;/p&gt;

&lt;h2&gt;
  
  
  La regla práctica
&lt;/h2&gt;

&lt;p&gt;Cuando necesitas hacer un cambio, hazte estas tres preguntas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;¿Cuántas clases tengo que tocar?&lt;/strong&gt; Si son más de 2 o 3, tienes acoplamiento alto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;¿Los métodos de esta clase cambian juntos?&lt;/strong&gt; Si no, tienes cohesión baja.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;¿Estoy compartiendo un objeto entre capas que tienen necesidades diferentes?&lt;/strong&gt; Si sí, necesitas modelos separados.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  El error de fondo
&lt;/h2&gt;

&lt;p&gt;El efecto dominó no es un problema de Spring, ni de Java, ni de ningún framework. Es un problema de diseño. Compartir objetos entre capas porque es "más rápido" y "menos código" funciona al principio. Después pagas el precio cada vez que tocas algo.&lt;/p&gt;

&lt;p&gt;Un poco de duplicación explícita (modelos separados por capa) es infinitamente mejor que un acoplamiento implícito que te explota en la cara cuando menos lo esperas.&lt;/p&gt;

&lt;p&gt;La próxima vez que vayas a compartir un DTO entre capas, hazte una pregunta: ¿realmente necesitan exactamente los mismos campos? Casi seguro que no.&lt;/p&gt;

&lt;p&gt;Día 14 de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 13: Tu clase tiene 50 métodos. Hace de todo. No hace nada bien.</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:23:16 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-13-tu-clase-tiene-50-metodos-hace-de-todo-no-hace-nada-bien-4928</link>
      <guid>https://dev.to/alafourcadev/dia-13-tu-clase-tiene-50-metodos-hace-de-todo-no-hace-nada-bien-4928</guid>
      <description>&lt;p&gt;Abre tu proyecto. Busca la clase más grande. La que tiene más métodos. La que todos tocan pero nadie entiende del todo. La que cuando entra alguien nuevo al equipo, le dices "no te preocupes por esa, ya la vas a ir entendiendo".&lt;/p&gt;

&lt;p&gt;Esa clase tiene nombre técnico. Se llama &lt;strong&gt;God Object&lt;/strong&gt;. Y probablemente le está costando a tu empresa más plata que cualquier bug que hayas corregido en el último año.&lt;/p&gt;

&lt;p&gt;Lo mejor (y lo peor) es que no importa el lenguaje, el framework, ni el tamaño del equipo. Esta clase existe en tu proyecto. Solo cambia el nombre.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto no es solo de Spring
&lt;/h2&gt;

&lt;p&gt;El anti-pattern es universal. Solo cambia el nombre del archivo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java / Spring Boot&lt;/strong&gt;: &lt;code&gt;OrderService&lt;/code&gt;, &lt;code&gt;UserManager&lt;/code&gt;, &lt;code&gt;PedidoController&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET&lt;/strong&gt;: &lt;code&gt;OrderController&lt;/code&gt; con 40 endpoints y 12 servicios inyectados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node / NestJS&lt;/strong&gt;: &lt;code&gt;user.service.ts&lt;/code&gt; que hace auth, emails, pagos y logging&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python / Django&lt;/strong&gt;: &lt;code&gt;views.py&lt;/code&gt; con 2000 líneas y toda la lógica de negocio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby on Rails&lt;/strong&gt;: el modelo &lt;code&gt;User&lt;/code&gt; con 80 métodos y 15 callbacks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PHP / Laravel&lt;/strong&gt;: el &lt;code&gt;OrderService&lt;/code&gt; que también es &lt;code&gt;PaymentService&lt;/code&gt; y &lt;code&gt;MailService&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El problema es el mismo: una clase que empezó razonable y se fue inflando con cada feature nueva, porque "total, ya está inyectado acá, le agregamos este método y listo".&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo sabes que lo tienes
&lt;/h2&gt;

&lt;p&gt;Tu clase es un God Object si cumple al menos tres de estas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Tiene más de 5 dependencias en el constructor (o imports/inyecciones).&lt;/li&gt;
&lt;li&gt;Mezcla responsabilidades que no se hablan entre sí: validación, emails, PDFs, métricas, integraciones.&lt;/li&gt;
&lt;li&gt;Cada merge request en esa clase genera conflictos con otros devs.&lt;/li&gt;
&lt;li&gt;Para testear un método hay que mockear media aplicación.&lt;/li&gt;
&lt;li&gt;Nadie en el equipo la entiende completa. Todos conocen "su pedazo".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si las cumples todas, lo siento. Lo tuyo no es una clase, es una pequeña empresa.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ejemplo clásico
&lt;/h2&gt;

&lt;p&gt;Este es el tipo de clase del que hablamos. Java con Spring Boot, pero el patrón se ve igual en cualquier stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;ProductRepository&lt;/span&gt; &lt;span class="n"&gt;productRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt; &lt;span class="n"&gt;userRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;EmailClient&lt;/span&gt; &lt;span class="n"&gt;emailClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PdfGenerator&lt;/span&gt; &lt;span class="n"&gt;pdfGenerator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;MetricsService&lt;/span&gt; &lt;span class="n"&gt;metricsService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;WarehouseClient&lt;/span&gt; &lt;span class="n"&gt;warehouseClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DiscountEngine&lt;/span&gt; &lt;span class="n"&gt;discountEngine&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PaymentGateway&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;NotificationService&lt;/span&gt; &lt;span class="n"&gt;notificationService&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;AuditLogger&lt;/span&gt; &lt;span class="n"&gt;auditLogger&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Constructor con 11 dependencias...&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 80 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;updateOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;UpdateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 60 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;cancelOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 45 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 50 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;applyDiscount&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 30 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;generateInvoice&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 40 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;sendConfirmationEmail&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 25 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;notifyWarehouse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 35 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;updateMetrics&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;orderId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 20 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 15 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;OrderReport&lt;/span&gt; &lt;span class="nf"&gt;generateReport&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;DateRange&lt;/span&gt; &lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* 55 líneas */&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// ... 40 métodos más&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once dependencias, 50+ métodos, un constructor que no entra en la pantalla. Cada cambio es un campo minado.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué duele en el día a día
&lt;/h2&gt;

&lt;p&gt;El problema no es estético, es operacional. Lo sientes cada sprint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cada cambio es riesgoso.&lt;/strong&gt; Tocas el cálculo de descuentos y, no se sabe cómo, rompes el envío de emails. Cuando todo está acoplado, un cambio en una línea tiene efectos en lugares inesperados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los tests son un infierno.&lt;/strong&gt; Para probar un método simple tienes que mockear las 11 dependencias, aunque el método solo use 2.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Los merge conflicts son el pan de cada semana.&lt;/strong&gt; Si tres devs tocan esta clase en paralelo, los conflictos están garantizados.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nadie se anima a refactorizarla.&lt;/strong&gt; Es tan grande y tan usada que el primero que la toque en serio va a romper media aplicación.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Al final, cada feature nueva tarda el doble de lo que debería. Y eso sí se traduce en plata.&lt;/p&gt;

&lt;h2&gt;
  
  
  La solución: Single Responsibility Principle
&lt;/h2&gt;

&lt;p&gt;SRP no dice "una clase hace una cosa". Dice algo más preciso, y más útil: &lt;strong&gt;una clase debe tener una única razón para cambiar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Lee esa definición de nuevo. "Razón para cambiar" es el criterio. Si tu clase cambia cuando cambia el pricing, cuando cambia el formato del email, y cuando cambia la integración con el warehouse, esas son tres razones para cambiar. Tres clases distintas.&lt;/p&gt;

&lt;p&gt;Vamos al refactor. Primero, un &lt;code&gt;OrderService&lt;/code&gt; con una sola responsabilidad: gestionar el ciclo de vida de las órdenes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderValidator&lt;/span&gt; &lt;span class="n"&gt;orderValidator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;OrderEventPublisher&lt;/span&gt; &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;OrderService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderRepository&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="nc"&gt;OrderValidator&lt;/span&gt; &lt;span class="n"&gt;orderValidator&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                        &lt;span class="nc"&gt;OrderEventPublisher&lt;/span&gt; &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderRepository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;orderValidator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderValidator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;eventPublisher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="nf"&gt;createOrder&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;orderValidator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;orderRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;eventPublisher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;saved&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;De 11 dependencias a 3. Un método que se lee en 10 segundos. Un propósito claro.&lt;/p&gt;

&lt;p&gt;El resto de funcionalidades va a servicios especializados, cada uno con su razón propia para existir:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderPricingService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;DiscountEngine&lt;/span&gt; &lt;span class="n"&gt;discountEngine&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="nf"&gt;calculateTotal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;discountCode&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Money&lt;/span&gt; &lt;span class="n"&gt;subtotal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calculateSubtotal&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;Discount&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;discountEngine&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;resolve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discountCode&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;applyTo&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subtotal&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderNotificationService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;EmailClient&lt;/span&gt; &lt;span class="n"&gt;emailClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PdfGenerator&lt;/span&gt; &lt;span class="n"&gt;pdfGenerator&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@EventListener&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOrderCreated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pdfGenerator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generate&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrder&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="n"&gt;emailClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sendOrderConfirmation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrder&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrderFulfillmentService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;WarehouseClient&lt;/span&gt; &lt;span class="n"&gt;warehouseClient&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@EventListener&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;onOrderCreated&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;OrderCreatedEvent&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;warehouseClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;reserve&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getOrder&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;getItems&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada uno tiene una única razón para cambiar:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OrderPricingService&lt;/code&gt; cambia cuando cambian las reglas de pricing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OrderNotificationService&lt;/code&gt; cambia cuando cambia el template del email o el PDF.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;OrderFulfillmentService&lt;/code&gt; cambia cuando cambia la integración con el warehouse.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  El detalle que hace que funcione: eventos
&lt;/h2&gt;

&lt;p&gt;Fíjate que a los servicios secundarios no los llama &lt;code&gt;OrderService&lt;/code&gt;. Se suscriben a un evento (&lt;code&gt;OrderCreatedEvent&lt;/code&gt;) que &lt;code&gt;OrderService&lt;/code&gt; publica.&lt;/p&gt;

&lt;p&gt;Esto cambia tres cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;OrderService&lt;/code&gt; no conoce a &lt;code&gt;OrderNotificationService&lt;/code&gt; ni a &lt;code&gt;OrderFulfillmentService&lt;/code&gt;. No los importa, no los inyecta, no los llama.&lt;/li&gt;
&lt;li&gt;Si mañana agregas un &lt;code&gt;OrderAnalyticsService&lt;/code&gt; que escuche el mismo evento, no tocas &lt;code&gt;OrderService&lt;/code&gt;. Ni una línea.&lt;/li&gt;
&lt;li&gt;Los tests de &lt;code&gt;OrderService&lt;/code&gt; solo verifican que el evento se publica, no todo lo que pasa después.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si tu lenguaje/framework no tiene eventos nativos (Spring &lt;code&gt;@EventListener&lt;/code&gt;, NestJS &lt;code&gt;EventEmitter&lt;/code&gt;, Django signals, Rails callbacks), funciona igual con un bus de eventos simple. El patrón es el mismo.&lt;/p&gt;

&lt;h2&gt;
  
  
  La regla del constructor
&lt;/h2&gt;

&lt;p&gt;Si quieres una heurística rápida que puedas aplicar mañana en tu proyecto:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cuenta las dependencias del constructor de cada clase. Si tiene más de 4 o 5, es sospechoso.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No es un número mágico, pero es un olor fuerte. Piénsalo así: dos clases del mismo proyecto, una con 3 dependencias y otra con 11 — ¿cuál te da más miedo tocar?&lt;/p&gt;

&lt;p&gt;Otra señal útil: si puedes agrupar los métodos de una clase en "bloques" que no se comunican entre sí, cada bloque probablemente debería ser su propia clase.&lt;/p&gt;

&lt;h2&gt;
  
  
  El error al aplicar SRP mal
&lt;/h2&gt;

&lt;p&gt;El error más común es entender SRP como "un método por clase" y terminar con 200 archivos que no significan nada. Eso no es SRP, es fragmentación sin criterio.&lt;/p&gt;

&lt;p&gt;SRP agrupa por &lt;strong&gt;cohesión&lt;/strong&gt;: los métodos que trabajan con los mismos datos y cambian por los mismos motivos van juntos. Los que no, se separan.&lt;/p&gt;

&lt;p&gt;La regla no es "una clase por método". Es "una clase por responsabilidad".&lt;/p&gt;

&lt;h2&gt;
  
  
  El resultado
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ANTES:   OrderService (50 métodos, 11 dependencias)

DESPUÉS: OrderService             (ciclo de vida)
         OrderPricingService      (precios y descuentos)
         OrderNotificationService (emails y PDFs)
         OrderFulfillmentService  (warehouse)
         OrderReportingService    (reportes y métricas)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cinco clases pequeñas que puedes testear, entender y modificar de forma independiente. Ninguna con más de 3 dependencias. Ninguna con más de 5 métodos.&lt;/p&gt;

&lt;h2&gt;
  
  
  La pregunta que tienes que hacerte
&lt;/h2&gt;

&lt;p&gt;Tu God Object no se creó un día. Se fue armando commit a commit, con la lógica razonable de "ya está acá, le agrego este método y listo". Nadie diseña un monstruo de 50 métodos a propósito.&lt;/p&gt;

&lt;p&gt;La próxima vez que vayas a agregar un método a una clase existente, hazte esta pregunta:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;¿Esta clase cambiaría por el mismo motivo por el que estoy agregando este método?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Si la respuesta es sí, adelante. Si es no, ese método tiene otro hogar.&lt;/p&gt;

&lt;p&gt;Con eso, ya tienes la mitad del SRP aplicado en la práctica.&lt;/p&gt;

&lt;p&gt;Día 13 de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 12: 5.000 personas hicieron click al mismo tiempo. Tu servidor pidió perdón.</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:22:56 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-12-5000-personas-hicieron-click-al-mismo-tiempo-tu-servidor-pidio-perdon-26ci</link>
      <guid>https://dev.to/alafourcadev/dia-12-5000-personas-hicieron-click-al-mismo-tiempo-tu-servidor-pidio-perdon-26ci</guid>
      <description>&lt;p&gt;Tu tienda online sale a internet. Llevas meses preparándote. La campaña de marketing hizo su trabajo. 5.000 personas están esperando con el dedo en el botón de "Comprar". La cuenta regresiva llega a cero. Todos hacen click al mismo tiempo.&lt;/p&gt;

&lt;p&gt;Tu servidor tiene 20 threads. Se saturan en menos de un segundo. Los otros 4.980 requests se encolan. Empiezan los timeouts. El login deja de funcionar. La página de inicio no carga. El botón de "Comprar" gira y gira. La gente empieza a refrescar. Cada refresh son más requests. Más cola. Más timeouts. El ciclo se retroalimenta.&lt;/p&gt;

&lt;p&gt;En 3 minutos, lo que iba a ser tu mejor día se convirtió en tu peor pesadilla.&lt;/p&gt;

&lt;h2&gt;
  
  
  No es un problema de capacidad. Es un problema de control.
&lt;/h2&gt;

&lt;p&gt;La reacción instintiva es "necesitamos más servidores". Y sí, tal vez. Pero el problema real es otro: &lt;strong&gt;tu API no tiene límites&lt;/strong&gt;. Cualquier cliente puede hacer las requests que quiera, a la velocidad que quiera. No hay semáforo. No hay velocidad máxima. No hay nada que diga "para un poco".&lt;/p&gt;

&lt;p&gt;Esto no es exclusivo del lanzamiento de una tienda. Es el mismo problema cuando:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un bot descubre tu API y la empieza a scrapear&lt;/li&gt;
&lt;li&gt;Un frontend con un bug hace polling cada 100ms en vez de cada 10 segundos&lt;/li&gt;
&lt;li&gt;Un partner de integración reintenta sin backoff exponencial&lt;/li&gt;
&lt;li&gt;Un usuario frustrado martillea F5 porque la página "no carga"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sin rate limiting, tu API es un buffet libre donde un solo comensal se puede comer toda la comida y dejar a los demás sin nada.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting: el concepto
&lt;/h2&gt;

&lt;p&gt;La idea es simple: &lt;strong&gt;poner un límite a cuántas requests puede hacer cada cliente en un período de tiempo&lt;/strong&gt;. Si lo supera, la API le responde con un error claro (HTTP 429 — Too Many Requests) en vez de intentar procesar todo y colapsar.&lt;/p&gt;

&lt;p&gt;El algoritmo más usado se llama &lt;strong&gt;Token Bucket&lt;/strong&gt; y funciona así:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cada cliente tiene un "balde" con N tokens&lt;/li&gt;
&lt;li&gt;Cada request consume 1 token&lt;/li&gt;
&lt;li&gt;Los tokens se regeneran a velocidad constante (por ejemplo, 50 por minuto)&lt;/li&gt;
&lt;li&gt;Si el balde está vacío, la request se rechaza con 429&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Es elegante porque permite ráfagas cortas (si el balde está lleno, puedes hacer varias requests rápidas) pero limita el tráfico sostenido.&lt;/p&gt;

&lt;p&gt;Este algoritmo lo usan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS API Gateway&lt;/strong&gt; — Token Bucket con burst&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stripe API&lt;/strong&gt; — 100 requests por segundo por defecto&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub API&lt;/strong&gt; — 5.000 requests por hora para autenticados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Twitter/X API&lt;/strong&gt; — Rate limits por endpoint y por ventana de tiempo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare&lt;/strong&gt; — Token Bucket configurable por zona&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;nginx&lt;/strong&gt; — &lt;code&gt;limit_req&lt;/code&gt; con leaky bucket (variante similar)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No es un concepto oscuro ni experimental. Es &lt;strong&gt;infraestructura estándar&lt;/strong&gt; que cualquier API pública del mundo implementa. La pregunta no es "¿necesito rate limiting?" — es "¿por qué todavía no lo tengo?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementación: cómo se ve
&lt;/h2&gt;

&lt;p&gt;En cualquier lenguaje, rate limiting es un filtro que se ejecuta ANTES de tu lógica de negocio. Si el cliente ya superó su límite, ni siquiera llegas al controller.&lt;/p&gt;

&lt;p&gt;La implementación del Token Bucket en Java es sorprendentemente simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;refillRatePerMs&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;lastRefillTimestamp&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;TokenBucket&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;refillPerMinute&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;refillRatePerMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;refillPerMinute&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;60000.0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lastRefillTimestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;synchronized&lt;/span&gt; &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="nf"&gt;tryConsume&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;refill&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="o"&gt;--;&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;refill&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentTimeMillis&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="kt"&gt;double&lt;/span&gt; &lt;span class="n"&gt;newTokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lastRefillTimestamp&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;refillRatePerMs&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;newTokens&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;lastRefillTimestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En Python con Flask sería un decorator. En Node con Express un middleware. En Go un middleware con &lt;code&gt;golang.org/x/time/rate&lt;/code&gt;. El patrón es idéntico: interceptar, verificar, dejar pasar o rechazar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aplicándolo al escenario de la tienda
&lt;/h2&gt;

&lt;p&gt;Volvamos al lanzamiento. Lo que necesitas es:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Component&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RateLimitFilter&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;Filter&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConcurrentHashMap&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;doFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ServletRequest&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ServletResponse&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                         &lt;span class="nc"&gt;FilterChain&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;IOException&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ServletException&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

        &lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletRequest&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;HttpServletResponse&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;clientIp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRemoteAddr&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getRequestURI&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

        &lt;span class="nc"&gt;TokenBucket&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;computeIfAbsent&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;clientIp&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;":"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;crearBucket&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;tryConsume&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"X-Rate-Limit-Remaining"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;valueOf&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAvailableTokens&lt;/span&gt;&lt;span class="o"&gt;()));&lt;/span&gt;
            &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;doFilter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setStatus&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setHeader&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Retry-After"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getWriter&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Too Many Requests"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora cada IP tiene un límite. Los primeros 10 "Comprar" pasan. Del 11 en adelante reciben un 429 limpio en milisegundos, sin ocupar threads, sin tocar la base de datos, sin degradar el servicio para los demás.&lt;/p&gt;

&lt;h2&gt;
  
  
  Límites diferentes para rutas diferentes
&lt;/h2&gt;

&lt;p&gt;No todos los endpoints son iguales. El endpoint de compra necesita protección fuerte (stock limitado, procesamiento pesado). El listado de productos puede ser más generoso:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Límite&lt;/th&gt;
&lt;th&gt;Razón&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/comprar&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10/min&lt;/td&gt;
&lt;td&gt;Proteger stock, procesamiento de pago&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/productos&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;60/min&lt;/td&gt;
&lt;td&gt;Lectura, ligero&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;5/min&lt;/td&gt;
&lt;td&gt;Prevención de fuerza bruta&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/imágenes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;100/min&lt;/td&gt;
&lt;td&gt;Assets estáticos (mejor en CDN)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Los headers que el cliente necesita
&lt;/h2&gt;

&lt;p&gt;Cuando rechazas una request con 429, sé un buen ciudadano HTTP. Dile al cliente cuándo puede reintentar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;429&lt;/span&gt; &lt;span class="ne"&gt;Too Many Requests&lt;/span&gt;
&lt;span class="na"&gt;Retry-After&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10&lt;/span&gt;
&lt;span class="na"&gt;X-Rate-Limit-Limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;50&lt;/span&gt;
&lt;span class="na"&gt;X-Rate-Limit-Remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El frontend (o el bot, o el partner de integración) puede leer estos headers y adaptarse: mostrar un mensaje al usuario, implementar backoff, o simplemente dejar de disparar requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números del escenario de la tienda
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Sin rate limit&lt;/th&gt;
&lt;th&gt;Con rate limit&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Requests en ráfaga&lt;/td&gt;
&lt;td&gt;5.000 simultáneas&lt;/td&gt;
&lt;td&gt;10/min por IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latencia p95 (otros endpoints)&lt;/td&gt;
&lt;td&gt;2.800ms&lt;/td&gt;
&lt;td&gt;180ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU promedio en pico&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disponibilidad bajo carga&lt;/td&gt;
&lt;td&gt;78%&lt;/td&gt;
&lt;td&gt;99.5%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El rate limiting no hizo que "Comprar" fuera más rápido. Hizo que &lt;strong&gt;todo lo demás siguiera funcionando&lt;/strong&gt;. El login, la página de inicio, el carrito — todo siguió vivo porque un grupo de clientes impacientes no pudo agotar los recursos del servidor.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Rate limiting en el backend o en el API Gateway?
&lt;/h2&gt;

&lt;p&gt;En un sistema real, generalmente lo quieres en &lt;strong&gt;ambos&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;API Gateway / Load Balancer&lt;/strong&gt; (nginx, AWS API Gateway, Cloudflare): Primera línea de defensa. Bloquea tráfico malicioso antes de que llegue a tu app. Protege contra DDoS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aplicación&lt;/strong&gt; (tu código): Segunda línea. Límites específicos por lógica de negocio (por ejemplo, máximo 3 compras del mismo producto por usuario para evitar acaparadores).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El gateway protege tu infraestructura. Tu app protege tu negocio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rate limiting no es hostilidad
&lt;/h2&gt;

&lt;p&gt;Esto es importante: rate limiting no es ser malo con tus clientes. Es &lt;strong&gt;proteger la experiencia de todos&lt;/strong&gt; tus clientes. Incluyendo los que no están haciendo nada mal.&lt;/p&gt;

&lt;p&gt;Es como el límite de velocidad en una autopista. No existe para molestarte. Existe para que la autopista funcione para todos.&lt;/p&gt;

&lt;p&gt;Una API sin rate limiting es una autopista sin límite de velocidad. Funciona bien con poco tráfico. El día que aparece un camión, un bot, o 5.000 personas emocionadas — se pudre todo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 12
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. Si tu API no tiene rate limiting, el próximo bot, el próximo lanzamiento, o el próximo Black Friday te va a recordar por qué lo necesitas.&lt;/p&gt;

&lt;p&gt;Sigue la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — con una implementación manual del Token Bucket sin dependencias externas para que entiendas el algoritmo por dentro. Si te está sirviendo, déjame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 11: System.out.println en producción — la confesión que nadie hace</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:22:17 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-11-systemoutprintln-en-produccion-la-confesion-que-nadie-hace-33hk</link>
      <guid>https://dev.to/alafourcadev/dia-11-systemoutprintln-en-produccion-la-confesion-que-nadie-hace-33hk</guid>
      <description>&lt;p&gt;Son las 3 de la mañana. Producción está caído. Abres los logs. 50.000 líneas de &lt;code&gt;System.out.println&lt;/code&gt; que dicen cosas como "entró al método", "valor: " + algo, "pasó por acá". Ninguna tiene timestamp. Ninguna tiene nivel de severidad. Ninguna te dice qué request generó ese log.&lt;/p&gt;

&lt;p&gt;Bienvenido a tu propia pesadilla. La diseñaste tú mismo.&lt;/p&gt;

&lt;p&gt;Y antes de que digas "yo no hago eso" — haz un &lt;code&gt;grep&lt;/code&gt; rápido de &lt;code&gt;System.out.println&lt;/code&gt; en tu proyecto. O de &lt;code&gt;console.log&lt;/code&gt; si estás en Node. O de &lt;code&gt;print()&lt;/code&gt; sin &lt;code&gt;logging&lt;/code&gt; si estás en Python. El resultado te va a sorprender.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto no es solo de Java
&lt;/h2&gt;

&lt;p&gt;El anti-pattern es universal. Cada lenguaje tiene su versión:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java&lt;/strong&gt;: &lt;code&gt;System.out.println&lt;/code&gt; en vez de SLF4J/Logback&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript/Node&lt;/strong&gt;: &lt;code&gt;console.log&lt;/code&gt; en vez de Winston/Pino&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: &lt;code&gt;print()&lt;/code&gt; en vez del módulo &lt;code&gt;logging&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: &lt;code&gt;fmt.Println&lt;/code&gt; en vez de &lt;code&gt;log/slog&lt;/code&gt; o Zap&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt;: &lt;code&gt;puts&lt;/code&gt; en vez de &lt;code&gt;Logger&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET&lt;/strong&gt;: &lt;code&gt;Console.WriteLine&lt;/code&gt; en vez de &lt;code&gt;ILogger&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El problema es el mismo en todos los casos: estás imprimiendo texto plano a stdout sin estructura, sin niveles, sin metadata. Parece que estás logueando. Pero cuando lo necesitas de verdad, descubres que lo que tienes es ruido.&lt;/p&gt;

&lt;h2&gt;
  
  
  Por qué System.out.println destruye tu producción
&lt;/h2&gt;

&lt;p&gt;No es solo que no tenga metadata. Es que &lt;strong&gt;afecta el rendimiento&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Es sincrónico&lt;/strong&gt; — cada println bloquea el thread hasta que el output se flushea. Con 200 threads concurrentes, todos escribiendo a stdout, creas contención en un recurso compartido.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tiene niveles&lt;/strong&gt; — no puedes filtrar. ¿Quieres ver solo errores? Imposible. Es todo o nada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No tiene contexto&lt;/strong&gt; — cuando 100 requests concurrentes imprimen "Procesando pago...", no sabes cuál es cuál.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No rota&lt;/strong&gt; — stdout no tiene rotación de archivos. Si redireccionas a un archivo, crece hasta llenar el disco.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  El ANTES: el caos
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PagoResponse&lt;/span&gt; &lt;span class="nf"&gt;procesarPago&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PagoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Procesando pago..."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Monto: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMonto&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Cliente: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClienteId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cobrar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pago OK: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Error en pago: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMessage&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En producción esto se ve así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Procesando pago...
Monto: 15000
Procesando pago...
Cliente: 4821
Monto: 8700
Error en pago: timeout
Cliente: 9102
Pago OK: TXN-44291
Procesando pago...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Qué pago falló? ¿De qué cliente? ¿A qué hora? No tienes idea. Los logs de distintos requests se mezclan. Es ruido puro.&lt;/p&gt;

&lt;h2&gt;
  
  
  El DESPUÉS: logging con sentido
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;Logger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LoggerFactory&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLogger&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PagoService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PagoResponse&lt;/span&gt; &lt;span class="nf"&gt;procesarPago&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PagoRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Procesando pago. clienteId={}, monto={}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                 &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClienteId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMonto&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;resultado&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gateway&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cobrar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Pago exitoso. txnId={}, clienteId={}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClienteId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resultado&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Fallo al procesar pago. clienteId={}, monto={}"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClienteId&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getMonto&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ahora los logs se ven así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-04-13 03:14:22.841 INFO  [http-nio-8080-exec-7] c.a.s.PagoService : Procesando pago. clienteId=4821, monto=15000
2026-04-13 03:14:23.102 ERROR [http-nio-8080-exec-3] c.a.s.PagoService : Fallo al procesar pago. clienteId=9102, monto=8700
java.net.SocketTimeoutException: timeout
    at com.app.gateway.GatewayClient.cobrar(GatewayClient.java:45)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cada línea tiene &lt;strong&gt;timestamp, nivel, thread, clase y datos estructurados&lt;/strong&gt;. En 2 segundos sabes qué falló, cuándo y para quién.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los niveles no son decoración
&lt;/h2&gt;

&lt;p&gt;Cada framework de logging tiene los mismos niveles. La tabla aplica a cualquier lenguaje:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Nivel&lt;/th&gt;
&lt;th&gt;Cuándo usarlo&lt;/th&gt;
&lt;th&gt;Ejemplo&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ERROR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Algo falló y necesita atención&lt;/td&gt;
&lt;td&gt;Error al procesar pago, conexión perdida&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WARN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Algo raro pero la app sigue&lt;/td&gt;
&lt;td&gt;Retry #3, cache miss inesperado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INFO&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Eventos de negocio relevantes&lt;/td&gt;
&lt;td&gt;Pago procesado, usuario creado&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DEBUG&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Detalle técnico para troubleshooting&lt;/td&gt;
&lt;td&gt;Query ejecutada, payload recibido&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TRACE&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Todo lo demás (casi nunca en prod)&lt;/td&gt;
&lt;td&gt;Entrada/salida de cada método&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La regla: &lt;strong&gt;en producción corres en INFO&lt;/strong&gt;. Si hay un problema, subes un paquete específico a DEBUG &lt;strong&gt;sin reiniciar&lt;/strong&gt;. En Spring Boot, Actuator te permite cambiar niveles de log en caliente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST localhost:8080/actuator/loggers/com.tuapp.gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"configuredLevel": "DEBUG"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin redeploy. Sin restart. Cambias el nivel, miras los logs, resuelves el problema, vuelves a INFO.&lt;/p&gt;

&lt;p&gt;En Node con Winston es similar: cambias el &lt;code&gt;level&lt;/code&gt; del transport en runtime. En Python con &lt;code&gt;logging.getLogger().setLevel()&lt;/code&gt;. El concepto es el mismo en todos lados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured logging: el salto de calidad
&lt;/h2&gt;

&lt;p&gt;Si tus logs van a un ELK Stack, Datadog, CloudWatch o cualquier herramienta de observabilidad, el formato texto plano es un problema. Lo que necesitas es JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-13T03:14:23.102Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"level"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"logger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"com.tuapp.service.PagoService"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fallo al procesar pago"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"clienteId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9102&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"monto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8700&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stack_trace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"java.net.SocketTimeoutException..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Buscable. Filtrable. Alertable. Puedes hacer queries como "dame todos los ERROR del PagoService en la última hora donde monto &amp;gt; 10000". Intenta hacer eso con &lt;code&gt;System.out.println&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;En Spring Boot se configura con Logstash encoder. En Node con Pino (que ya sale en JSON por default). En Python con &lt;code&gt;python-json-logger&lt;/code&gt;. En Go con &lt;code&gt;slog&lt;/code&gt; (que viene integrado desde Go 1.21).&lt;/p&gt;

&lt;h2&gt;
  
  
  Lo que NO deberías loguear
&lt;/h2&gt;

&lt;p&gt;Antes de loguear todo, un segundo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Passwords, tokens, API keys.&lt;/strong&gt; Jamás. Ni en DEBUG. &lt;code&gt;log.debug("Token: {}", token)&lt;/code&gt; es un incidente de seguridad esperando a pasar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datos personales sensibles.&lt;/strong&gt; DNI, tarjetas de crédito, datos médicos. Si tu log tiene PII, tienes un problema de compliance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El request body completo.&lt;/strong&gt; Un JSON de 2MB en cada log llena tu disco en horas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datos que no ayudan a diagnosticar nada.&lt;/strong&gt; "Entró al método" no es información. "Procesando pedido 4821 para cliente 102" sí lo es.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  El error de fondo
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;System.out.println&lt;/code&gt; te da una falsa sensación de visibilidad. Parece que estás logueando. Parece que tienes información. Pero cuando la necesitas de verdad, a las 3AM con producción caído, descubres que lo que tienes es ruido.&lt;/p&gt;

&lt;p&gt;Logging profesional no es difícil. Todos los lenguajes y frameworks modernos ya tienen las herramientas listas. Solo tienes que dejar de usar &lt;code&gt;print&lt;/code&gt; y empezar a pensar qué información necesitarías para diagnosticar un problema que todavía no pasó.&lt;/p&gt;

&lt;p&gt;Haz un &lt;code&gt;grep&lt;/code&gt; de &lt;code&gt;System.out.println&lt;/code&gt; en tu proyecto. Si aparece más de cero veces en código de producción, tienes tarea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 11
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales.&lt;/p&gt;

&lt;p&gt;Sigue la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — con el BEFORE/AFTER para que veas la diferencia en la consola en tiempo real. Si te está sirviendo, déjame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 10: Le pusiste índices a todo y ahora los INSERT tardan 800ms</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:22:03 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-10-le-pusiste-indices-a-todo-y-ahora-los-insert-tardan-800ms-kd2</link>
      <guid>https://dev.to/alafourcadev/dia-10-le-pusiste-indices-a-todo-y-ahora-los-insert-tardan-800ms-kd2</guid>
      <description>&lt;p&gt;Antes de arrancar, una confesión: a mí no me gustan las bases de datos. Me aburren. Prefiero escribir código de negocio, diseñar arquitecturas, resolver problemas de integración. Pero justamente por eso aprendí a respetarlas. Porque cada vez que las ignoré, me mordieron. Fuerte.&lt;/p&gt;

&lt;p&gt;Hoy toca hablar de índices. Un tema que muchos consideran aburrido y que termina siendo un infierno en producción.&lt;/p&gt;

&lt;p&gt;La historia típica es esta: alguien en el equipo corre un &lt;code&gt;EXPLAIN&lt;/code&gt;, ve un full table scan, y la solución parece obvia: &lt;em&gt;"ponle un índice"&lt;/em&gt;. Funciona. La query pasa de 3 segundos a 40 milisegundos. Eureka.&lt;/p&gt;

&lt;p&gt;Entonces le ponen índice a la siguiente columna. Y a la siguiente. Y a la siguiente. Seis meses después, los INSERT tardan 800ms y nadie entiende por qué.&lt;/p&gt;

&lt;p&gt;Bienvenido al anti-pattern más común en bases de datos: &lt;strong&gt;indexar todo por si acaso&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué es un índice (y por qué cuesta)
&lt;/h2&gt;

&lt;p&gt;Antes de hablar de código, el concepto. Porque esto no es exclusivo de MySQL ni de PostgreSQL — es de &lt;strong&gt;cualquier base de datos que hayas usado o vayas a usar&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Un índice es una &lt;strong&gt;estructura de datos adicional&lt;/strong&gt; que la base mantiene en paralelo a tu tabla. Generalmente es un B-tree (o un B+tree) que ordena los valores de una columna para que buscar uno específico sea logarítmico en vez de lineal.&lt;/p&gt;

&lt;p&gt;Acelera las lecturas. Eso está claro.&lt;/p&gt;

&lt;p&gt;Lo que nadie te cuenta es el otro lado: &lt;strong&gt;cada índice hay que mantenerlo actualizado en cada escritura&lt;/strong&gt;. Cuando haces un &lt;code&gt;INSERT&lt;/code&gt;, la base no solo escribe la fila — actualiza &lt;strong&gt;todos&lt;/strong&gt; los índices de esa tabla. Cada uno. Uno por uno.&lt;/p&gt;

&lt;p&gt;Si tienes 12 índices, cada &lt;code&gt;INSERT&lt;/code&gt; tiene que actualizar 12 estructuras de datos. Cada &lt;code&gt;UPDATE&lt;/code&gt; que toque una columna indexada recalcula ese índice. Cada &lt;code&gt;DELETE&lt;/code&gt; limpia la fila y limpia los índices asociados.&lt;/p&gt;

&lt;p&gt;No es gratis. Nunca fue gratis.&lt;/p&gt;

&lt;p&gt;Y esto aplica en:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;PostgreSQL&lt;/strong&gt; (B-tree, GIN, GiST, BRIN)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MySQL / MariaDB&lt;/strong&gt; (B-tree, Hash, Full-text)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Oracle&lt;/strong&gt; (B-tree, Bitmap)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Server&lt;/strong&gt; (Clustered, Non-clustered)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MongoDB&lt;/strong&gt; (B-tree, compound, text, geospatial)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB&lt;/strong&gt; (local + global secondary indexes)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cambia la sintaxis. Cambia el engine. El tradeoff es idéntico: &lt;strong&gt;escrituras más caras a cambio de lecturas más baratas&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ANTES: la tabla navideña
&lt;/h2&gt;

&lt;p&gt;Mira esta entidad. Dime si no te suena de algún proyecto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pedidos"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_cliente"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;     &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"clienteId"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_fecha"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"fecha"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_estado"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"estado"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_total"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_moneda"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;      &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"moneda"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_sucursal"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"sucursal"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_vendedor"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;    &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"vendedorId"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_canal"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;       &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"canal"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_prioridad"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;   &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"prioridad"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_tipo"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;        &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"tipo"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pedido&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&lt;/span&gt; &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;clienteId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;LocalDate&lt;/span&gt; &lt;span class="n"&gt;fecha&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;estado&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;moneda&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;sucursal&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;vendedorId&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;canal&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;prioridad&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;tipo&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Diez índices.&lt;/strong&gt; Para una tabla transaccional que recibe miles de INSERTs por hora. Cada escritura paga el costo de mantener diez B-trees actualizados. Todos. Para cada fila.&lt;/p&gt;

&lt;p&gt;Alguien los puso en algún momento pensando "por si acaso alguien quiere buscar por canal". Nadie nunca buscó por canal. Pero el índice sigue ahí, encareciendo cada escritura, consumiendo disco, ralentizando los backups.&lt;/p&gt;

&lt;h2&gt;
  
  
  El DESPUÉS: solo lo que importa
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="nd"&gt;@Table&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"pedidos"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;indexes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_cliente_fecha"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"clienteId, fecha"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
    &lt;span class="nd"&gt;@Index&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"idx_estado"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;        &lt;span class="n"&gt;columnList&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"estado"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Pedido&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// mismos campos&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Dos índices.&lt;/strong&gt; Uno compuesto que cubre el 80% de las queries reales (buscar pedidos de un cliente en un rango de fechas) y otro para el filtro por estado que el dashboard usa todo el tiempo.&lt;/p&gt;

&lt;p&gt;Nada más. El resto de las columnas no necesitan índice porque &lt;strong&gt;nadie las filtra en producción&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números del benchmark
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ANTES (10 índices)&lt;/th&gt;
&lt;th&gt;DESPUÉS (2 índices)&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INSERT 5K registros&lt;/td&gt;
&lt;td&gt;~1240ms&lt;/td&gt;
&lt;td&gt;~380ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SELECT por cliente&lt;/td&gt;
&lt;td&gt;~5ms&lt;/td&gt;
&lt;td&gt;~5ms&lt;/td&gt;
&lt;td&gt;Sin cambio&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Espacio en disco&lt;/td&gt;
&lt;td&gt;2.1 GB&lt;/td&gt;
&lt;td&gt;380 MB&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;82%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Los SELECTs &lt;strong&gt;apenas cambiaron&lt;/strong&gt; porque los 8 índices que sacamos casi nunca se usaban. Estaban ahí consumiendo disco y frenando escrituras sin que nadie los necesitara.&lt;/p&gt;

&lt;p&gt;El código de este benchmark está en el repo — puedes correrlo y ver los números reales en tu máquina. Cambia la cantidad de registros para ver cómo escala la diferencia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las 4 reglas para decidir qué indexar
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Regla 1: Mira las queries reales, no las que imaginas
&lt;/h3&gt;

&lt;p&gt;El error más común es indexar &lt;em&gt;en teoría&lt;/em&gt;. "Por si acaso alguien consulta por sucursal". Nadie va a consultar por sucursal. Pero el índice está ahí, encareciendo cada escritura para una query que no existe.&lt;/p&gt;

&lt;p&gt;Mira lo que tu app hace &lt;strong&gt;de verdad&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- PostgreSQL: las queries más lentas&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean_exec_time&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_stat_statements&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;mean_exec_time&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- MySQL: queries que hacen full scan&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;statements_with_full_table_scans&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;no_index_used_count&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- MongoDB: queries lentas&lt;/span&gt;
&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;system&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;millis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;gt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}}).&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si no hay una query real que use esa columna en un &lt;code&gt;WHERE&lt;/code&gt;, &lt;code&gt;JOIN&lt;/code&gt; o &lt;code&gt;ORDER BY&lt;/code&gt;, &lt;strong&gt;no necesitas el índice&lt;/strong&gt;. Punto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regla 2: Los índices compuestos son tu mejor amigo
&lt;/h3&gt;

&lt;p&gt;Un índice compuesto en &lt;code&gt;(clienteId, fecha)&lt;/code&gt; cubre automáticamente:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;WHERE clienteId = ?&lt;/code&gt; → usa la primera columna&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE clienteId = ? AND fecha &amp;gt; ?&lt;/code&gt; → usa ambas&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ORDER BY clienteId, fecha&lt;/code&gt; → usa ambas&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WHERE clienteId = ? ORDER BY fecha&lt;/code&gt; → usa ambas&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Un índice por separado en &lt;code&gt;clienteId&lt;/code&gt; y otro en &lt;code&gt;fecha&lt;/code&gt; &lt;strong&gt;no&lt;/strong&gt; dan el mismo resultado. El optimizador elige uno de los dos, no los combina mágicamente. Un índice compuesto bien pensado reemplaza 2 o 3 índices individuales.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regla 3: Las columnas de baja cardinalidad casi nunca justifican un índice solo
&lt;/h3&gt;

&lt;p&gt;Un campo &lt;code&gt;estado&lt;/code&gt; con 4 valores posibles (&lt;code&gt;PENDIENTE&lt;/code&gt;, &lt;code&gt;PROCESADO&lt;/code&gt;, &lt;code&gt;ENVIADO&lt;/code&gt;, &lt;code&gt;CANCELADO&lt;/code&gt;) filtra el 25% de la tabla por valor. En tablas grandes, eso sigue siendo un montón de filas. El optimizador a veces decide ignorar el índice y hacer un full scan porque es más rápido.&lt;/p&gt;

&lt;p&gt;Lo mismo con booleanos. Un índice en una columna &lt;code&gt;activo&lt;/code&gt; donde el 90% es &lt;code&gt;true&lt;/code&gt; es inútil — la query va a tocar el 90% de las filas de todas formas.&lt;/p&gt;

&lt;p&gt;Si insistes en indexar una columna de baja cardinalidad, combínala con otra columna más selectiva en un índice compuesto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Regla 4: La regla del 5%
&lt;/h3&gt;

&lt;p&gt;Si una query devuelve más del &lt;strong&gt;5% de las filas&lt;/strong&gt; de la tabla, el optimizador probablemente ignore el índice y haga un full scan. Porque leer un 5% de la tabla ordenada secuencialmente es más rápido que saltar de un lado a otro siguiendo un B-tree.&lt;/p&gt;

&lt;p&gt;Los índices son para queries &lt;strong&gt;selectivas&lt;/strong&gt;. Si tu filtro es "tráeme el 80% de las filas", indexar no ayuda.&lt;/p&gt;

&lt;h2&gt;
  
  
  La pregunta clave antes de crear un índice
&lt;/h2&gt;

&lt;p&gt;Antes de crear un índice, pregúntate: &lt;strong&gt;¿esta tabla recibe más lecturas o más escrituras?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tabla de productos / catálogo&lt;/strong&gt;: miles de lecturas por segundo, pocas escrituras por día. Ponle los índices que quieras. Los reads valen mucho y los writes casi no duelen.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tabla de eventos / logs / auditoría&lt;/strong&gt;: miles de escrituras por segundo, lecturas ocasionales para reportes. Cada índice duele mucho. Indexa lo mínimo indispensable.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tabla de pedidos / transacciones&lt;/strong&gt;: escrituras Y lecturas frecuentes. Aquí es donde tienes que ser quirúrgico. Indexa lo que las queries reales necesitan, nada más.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No hay una respuesta universal. Hay &lt;strong&gt;tradeoffs&lt;/strong&gt;. Tu trabajo es entenderlos, no ignorarlos.&lt;/p&gt;

&lt;h2&gt;
  
  
  El error de fondo
&lt;/h2&gt;

&lt;p&gt;El problema nunca fue "faltan índices". El problema fue no preguntarse &lt;strong&gt;cuáles&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Poner un índice en cada columna es como poner un semáforo en cada esquina — en algún momento, el remedio es peor que la enfermedad.&lt;/p&gt;

&lt;p&gt;Un índice es un &lt;strong&gt;contrato&lt;/strong&gt;: "acepto pagar más en cada escritura para ganar velocidad en esta lectura específica". Si no sabes cuál es la lectura que estás optimizando, no firmes el contrato.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 10
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. La próxima vez que alguien diga "ponle un índice", pregúntale dos cosas: &lt;strong&gt;¿a cuál columna?&lt;/strong&gt; y &lt;strong&gt;¿por qué?&lt;/strong&gt;. Si no puede responder las dos, no hay que indexar nada todavía.&lt;/p&gt;

&lt;p&gt;Sigue la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — con un benchmark que puedes correr para ver en vivo cómo los índices de más frenan los INSERTs. Si te está sirviendo, déjame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 9: Tu app funcionó todo el QA. El lunes a las 9am explotó con 100 usuarios.</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:21:48 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-9-tu-app-funciono-todo-el-qa-el-lunes-a-las-9am-exploto-con-100-usuarios-40il</link>
      <guid>https://dev.to/alafourcadev/dia-9-tu-app-funciono-todo-el-qa-el-lunes-a-las-9am-exploto-con-100-usuarios-40il</guid>
      <description>&lt;p&gt;Viernes, 18:00. Deploy a producción. Todo anda perfecto con el equipo de testing. El lunes a las 9:00 entran 100 usuarios reales y la app escupe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQLTransientConnectionException: Connection is not available,
request timed out after 30000ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El pool de conexiones se quedó sin stock. Y nadie configuró nada porque &lt;em&gt;"Spring Boot se encarga de eso"&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Si esto te suena familiar, seguí leyendo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Qué es un connection pool (y por qué existe)
&lt;/h2&gt;

&lt;p&gt;Antes de hablar de código, necesitamos entender de qué estamos hablando. Porque este problema no es de Spring ni de Java — es de cualquier aplicación que hable con una base de datos.&lt;/p&gt;

&lt;p&gt;Abrir una conexión a una base de datos es &lt;strong&gt;caro&lt;/strong&gt;. Cada conexión implica:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Un &lt;strong&gt;TCP handshake&lt;/strong&gt; (3 roundtrips de red)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autenticación&lt;/strong&gt; (usuario, password, negociación SSL si aplica)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Negociación de protocolo&lt;/strong&gt; con el servidor&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alocación de recursos&lt;/strong&gt; en el lado del servidor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo junto puede tardar 50-100ms. Si tu endpoint abre y cierra una conexión por cada request, con 100 requests por segundo estás creando 100 conexiones nuevas por segundo. Eso mata a la base de datos.&lt;/p&gt;

&lt;p&gt;La solución existe desde los 90s y es universal: un &lt;strong&gt;connection pool&lt;/strong&gt;. Creás N conexiones al inicio, las dejás abiertas, y las reutilizás. Cuando tu código necesita una conexión, la pide prestada del pool. Cuando termina, la devuelve. Nadie abre ni cierra nada.&lt;/p&gt;

&lt;p&gt;Cada lenguaje tiene su implementación:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java&lt;/strong&gt;: HikariCP (el que usa Spring Boot por defecto), C3P0, Apache DBCP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: SQLAlchemy Pool, psycopg2 pool, Django lo maneja internamente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: pg-pool, mysql2 pool, Prisma tiene pool integrado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: el &lt;code&gt;database/sql&lt;/code&gt; del stdlib maneja el pool automáticamente&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt;: ActiveRecord tiene pool integrado&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET&lt;/strong&gt;: el ADO.NET tiene pooling automático&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La herramienta cambia. El concepto es idéntico: &lt;strong&gt;no crear conexiones nuevas, reusar las que ya tenés&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El primer error: no configurarlo
&lt;/h2&gt;

&lt;p&gt;La configuración por defecto de HikariCP en Spring Boot es:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;maximum-pool-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;     &lt;span class="c1"&gt;# Máximo 10 conexiones&lt;/span&gt;
&lt;span class="na"&gt;connection-timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30000&lt;/span&gt; &lt;span class="c1"&gt;# 30 segundos esperando una conexión&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;10 conexiones. Para una app con 100 usuarios concurrentes donde cada request tarda 200ms en la query, eso significa que podés procesar 50 requests por segundo (10 conexiones / 0.2 seg). Suena bien.&lt;/p&gt;

&lt;p&gt;Pero si un endpoint tiene una query que tarda 2 segundos — un reporte, una búsqueda compleja, una transacción larga — esas 10 conexiones sirven 5 requests por segundo. El resto espera. Y si más de 10 requests llegan al mismo tiempo, alguno va a esperar 30 segundos completos y explotar con timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  El segundo error (el grave): connection leaks
&lt;/h2&gt;

&lt;p&gt;Peor que tener pocas conexiones es &lt;strong&gt;perder&lt;/strong&gt; conexiones. Un connection leak pasa cuando tu código pide una conexión del pool y nunca la devuelve.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NUNCA hagas esto&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Reporte&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;generar&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataSource&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnection&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;PreparedStatement&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prepareStatement&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT ..."&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="nc"&gt;ResultSet&lt;/span&gt; &lt;span class="n"&gt;rs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeQuery&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;mapear&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// Si hubo una excepción arriba, conn.close() nunca se llama&lt;/span&gt;
    &lt;span class="c1"&gt;// La conexión queda colgada. Para siempre.&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Este patrón — en cualquier lenguaje — es una &lt;strong&gt;bomba de tiempo&lt;/strong&gt;. Cada error deja una conexión huérfana. Después de 10 errores, tu pool está vacío. La app entera se cae, no solo este endpoint.&lt;/p&gt;

&lt;p&gt;El síntoma clásico:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Funciona un rato y después se cuelga. Si reiniciamos el pod, vuelve a andar."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Reiniciar el pod limpia el pool. Pero el leak sigue ahí, esperando.&lt;/p&gt;

&lt;h2&gt;
  
  
  La versión correcta
&lt;/h2&gt;

&lt;p&gt;En Spring Boot, &lt;strong&gt;nunca llames a &lt;code&gt;dataSource.getConnection()&lt;/code&gt; directamente&lt;/strong&gt;. Usá &lt;code&gt;JdbcTemplate&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReporteService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ReporteService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;JdbcTemplate&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;jdbcTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Reporte&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;generar&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jdbcTemplate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
            &lt;span class="s"&gt;"SELECT id, nombre, total FROM reportes WHERE fecha &amp;gt; ?"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rowNum&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Reporte&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nombre"&lt;/span&gt;&lt;span class="o"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getBigDecimal&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"total"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
            &lt;span class="o"&gt;),&lt;/span&gt;
            &lt;span class="nc"&gt;LocalDate&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;now&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;minusDays&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;JdbcTemplate&lt;/code&gt; maneja la conexión por vos. La pide, la usa, y la devuelve. &lt;strong&gt;Siempre.&lt;/strong&gt; Incluso si hay una excepción. Si usás JPA/Hibernate, &lt;code&gt;@Transactional&lt;/code&gt; hace lo mismo.&lt;/p&gt;

&lt;p&gt;Este patrón se repite en todos los lenguajes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt;: usá &lt;code&gt;with connection.cursor()&lt;/code&gt; (context manager)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: usá &lt;code&gt;pool.query()&lt;/code&gt;, nunca &lt;code&gt;pool.connect()&lt;/code&gt; sin &lt;code&gt;.release()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: usá &lt;code&gt;db.Query()&lt;/code&gt;, no &lt;code&gt;db.Conn()&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby&lt;/strong&gt;: ActiveRecord maneja todo, no pelees contra él&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La regla universal: &lt;strong&gt;dejá que el framework maneje el ciclo de vida de la conexión&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak detection: tu seguro de vida
&lt;/h2&gt;

&lt;p&gt;HikariCP tiene una funcionalidad que te salva de los leaks silenciosos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;datasource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;hikari&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;leak-detection-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;  &lt;span class="c1"&gt;# 60 segundos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si una conexión lleva más de 60 segundos sin ser devuelta, HikariCP te lo dice en los logs con un stack trace completo de quién la pidió:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="no"&gt;WARN&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="nc"&gt;HikariPool&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nc"&gt;Connection&lt;/span&gt; &lt;span class="n"&gt;leak&lt;/span&gt; &lt;span class="n"&gt;detection&lt;/span&gt; &lt;span class="n"&gt;triggered&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt;
  &lt;span class="n"&gt;on&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;nio&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stack&lt;/span&gt; &lt;span class="n"&gt;trace&lt;/span&gt; &lt;span class="nl"&gt;follows:&lt;/span&gt;
    &lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;Exception&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Apparent&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="n"&gt;leak&lt;/span&gt; &lt;span class="n"&gt;detected&lt;/span&gt;
      &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;example&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ReporteService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;generar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ReporteService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;java&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Archivo, línea, thread. No más adivinar. &lt;strong&gt;Activalo siempre.&lt;/strong&gt; Es gratis y te va a salvar una noche de guardia.&lt;/p&gt;

&lt;h2&gt;
  
  
  La matemática del pool size (el punto que nadie entiende)
&lt;/h2&gt;

&lt;p&gt;Todos quieren saber: &lt;em&gt;"¿cuántas conexiones necesito?"&lt;/em&gt; La intuición dice: &lt;strong&gt;más es mejor&lt;/strong&gt;. La intuición está equivocada.&lt;/p&gt;

&lt;p&gt;La fórmula del autor de HikariCP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;connections = ((core_count * 2) + effective_spindle_count)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para un servidor de base de datos con 4 cores y un SSD:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;conexiones = (4 * 2) + 1 = 9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sí. &lt;strong&gt;9 conexiones.&lt;/strong&gt; En un pool bien configurado, 9 conexiones pueden manejar &lt;strong&gt;miles de requests por segundo&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;¿Por qué? Porque la base de datos solo puede hacer trabajo real en paralelo hasta cierto límite. Si tu servidor tiene 4 cores, más de 8-10 queries concurrentes empiezan a &lt;strong&gt;pelearse por CPU&lt;/strong&gt;. Agregás más context switches, más locks, más contención. Más conexiones no te da más throughput — te da &lt;strong&gt;menos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;La clave no es tener muchas conexiones. Es &lt;strong&gt;devolverlas rápido&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  La configuración que recomiendo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;datasource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;hikari&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;maximum-pool-size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;
      &lt;span class="na"&gt;minimum-idle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;connection-timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10000&lt;/span&gt;       &lt;span class="c1"&gt;# 10 seg, no 30&lt;/span&gt;
      &lt;span class="na"&gt;idle-timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;300000&lt;/span&gt;            &lt;span class="c1"&gt;# 5 min&lt;/span&gt;
      &lt;span class="na"&gt;max-lifetime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1200000&lt;/span&gt;           &lt;span class="c1"&gt;# 20 min&lt;/span&gt;
      &lt;span class="na"&gt;leak-detection-threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60000&lt;/span&gt;
      &lt;span class="na"&gt;pool-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mi-app-pool&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Por qué estos valores?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;20 conexiones&lt;/strong&gt;: suficiente para la mayoría de apps CRUD. Ajustá según tu caso.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;minimum-idle 5&lt;/strong&gt;: no mantengas 20 conexiones abiertas a las 3am cuando no hay nadie.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;connection-timeout 10s&lt;/strong&gt;: si en 10 segundos no hay conexión, algo está mal. Fallá rápido.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;max-lifetime 20min&lt;/strong&gt;: las conexiones se renuevan para evitar firewalls que matan conexiones idle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pool-name&lt;/strong&gt;: cuando tengas múltiples datasources, saber cuál es cuál vale oro.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Métricas que tenés que mirar
&lt;/h2&gt;

&lt;p&gt;Con Actuator + Micrometer tenés estas métricas en tiempo real:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="err"&gt;hikaricp.connections.active&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;Conexiones&lt;/span&gt; &lt;span class="err"&gt;en&lt;/span&gt; &lt;span class="err"&gt;uso&lt;/span&gt; &lt;span class="err"&gt;ahora&lt;/span&gt;
&lt;span class="err"&gt;hikaricp.connections.idle&lt;/span&gt;      &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;Conexiones&lt;/span&gt; &lt;span class="err"&gt;disponibles&lt;/span&gt;
&lt;span class="err"&gt;hikaricp.connections.pending&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;Threads&lt;/span&gt; &lt;span class="err"&gt;esperando&lt;/span&gt; &lt;span class="err"&gt;conexión&lt;/span&gt;
&lt;span class="err"&gt;hikaricp.connections.timeout&lt;/span&gt;   &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="err"&gt;Timeouts&lt;/span&gt; &lt;span class="err"&gt;acumulados&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reglas simples para leer estas métricas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Si &lt;code&gt;pending &amp;gt; 0&lt;/code&gt; de forma sostenida → &lt;strong&gt;tu pool es chico&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Si &lt;code&gt;timeout&lt;/code&gt; crece → &lt;strong&gt;tenés un problema serio&lt;/strong&gt; (leak o pool agotado)&lt;/li&gt;
&lt;li&gt;Si &lt;code&gt;active&lt;/code&gt; está siempre al máximo → &lt;strong&gt;estás al límite&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Si &lt;code&gt;idle&lt;/code&gt; nunca baja de &lt;code&gt;minimum-idle&lt;/code&gt; → &lt;strong&gt;todo bien&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cuándo NO tocar el pool size
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Para "mejorar performance" sin datos&lt;/strong&gt; — Subir de 10 a 100 conexiones no hace tu app más rápida. Probablemente la haga más lenta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin medir primero&lt;/strong&gt; — Si no sabés cuántas conexiones activas tenés en promedio, no sabés si necesitás más. Medí antes de tocar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuando el problema es un leak&lt;/strong&gt; — Más conexiones solo te dan más tiempo antes de que explote. &lt;strong&gt;Arreglá el leak.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin coordinar con el DBA&lt;/strong&gt; — Si tu base soporta 100 conexiones y vos configurás 80 en cada instancia con 3 instancias, le estás pidiendo 240 conexiones a una base que soporta 100. Boom.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Esto es el Día 9
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. Si tu app dice "too many connections", el problema no es el límite. Es cuánto tardás en devolver cada una.&lt;/p&gt;

&lt;p&gt;La próxima vez que alguien te diga "hay que subir el pool size", preguntale dos cosas: ¿cuántas conexiones activas hay en promedio? ¿cuánto tardan las queries? Si no puede responder las dos, no hay que subir nada. Hay que medir.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; — con un demo del leak funcionando para que lo veas romper en vivo. Si te está sirviendo, dejame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 8: El usuario subió un Excel y tu servidor pidió perdón</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:21:30 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-8-el-usuario-subio-un-excel-y-tu-servidor-pidio-perdon-c0a</link>
      <guid>https://dev.to/alafourcadev/dia-8-el-usuario-subio-un-excel-y-tu-servidor-pidio-perdon-c0a</guid>
      <description>&lt;p&gt;El usuario sube un Excel con 500.000 filas. Tu endpoint lo lee, lo procesa, y le devuelve el resultado. Sincrónicamente. En el mismo thread del request HTTP.&lt;/p&gt;

&lt;p&gt;El browser muestra "esperando respuesta del servidor" durante 4 minutos. El load balancer corta por timeout a los 60 segundos. La app se come 2GB de memoria. El pod se reinicia. El usuario vuelve a intentar. Y así tres veces.&lt;/p&gt;

&lt;p&gt;Si esto te suena familiar, seguí leyendo.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP no fue diseñado para esto
&lt;/h2&gt;

&lt;p&gt;Antes de hablar de código, necesitamos hablar de algo más fundamental: &lt;strong&gt;HTTP es un protocolo de request-response diseñado para respuestas rápidas&lt;/strong&gt;. Millisegundos, tal vez segundos. No minutos.&lt;/p&gt;

&lt;p&gt;Cuando metés una operación de 4 minutos dentro de un request HTTP, estás peleando contra todo el stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;El &lt;strong&gt;load balancer&lt;/strong&gt; tiene un timeout (generalmente 60 segundos)&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;proxy&lt;/strong&gt; tiene un timeout&lt;/li&gt;
&lt;li&gt;El &lt;strong&gt;browser&lt;/strong&gt; tiene un timeout&lt;/li&gt;
&lt;li&gt;La &lt;strong&gt;conexión TCP&lt;/strong&gt; tiene un timeout&lt;/li&gt;
&lt;li&gt;Tu &lt;strong&gt;thread pool&lt;/strong&gt; tiene un límite de threads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Todo conspira contra vos. Y no es un bug — es que estás usando la herramienta equivocada para el trabajo.&lt;/p&gt;

&lt;p&gt;Es como mandar un paquete de 200 kilos por correo postal. Técnicamente podrías. Pero no fue diseñado para eso. Necesitás un flete.&lt;/p&gt;

&lt;h2&gt;
  
  
  El patrón universal: recibí, encolá, procesá, notificá
&lt;/h2&gt;

&lt;p&gt;La solución no es "hacer que tarde menos". La solución es &lt;strong&gt;cambiar la arquitectura&lt;/strong&gt;. Y el patrón es el mismo en cualquier lenguaje:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. RECIBIR  → Aceptar el request, validar lo básico
2. ENCOLAR  → Generar un ID de job, responder inmediatamente
3. PROCESAR → En background, en batches, con progreso
4. NOTIFICAR → El usuario consulta el estado o recibe una notificación
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esto existe en todos lados:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python/Django&lt;/strong&gt;: Celery + Redis como broker&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt;: Bull/BullMQ con Redis, o AWS SQS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go&lt;/strong&gt;: Goroutines + channels, o temporal.io para workflows complejos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby/Rails&lt;/strong&gt;: Sidekiq&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Java/Spring&lt;/strong&gt;: &lt;code&gt;@Async&lt;/code&gt; + ThreadPoolExecutor, o Spring Batch para escenarios pesados&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET&lt;/strong&gt;: Hangfire, o Azure Functions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La herramienta cambia. El patrón es idéntico: &lt;strong&gt;no proceses en el thread del request HTTP&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El crimen contra la arquitectura
&lt;/h2&gt;

&lt;p&gt;Mirá este endpoint. Parece inocente. Es un desastre:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/importar"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;importar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;archivo&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Registro&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;registros&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;parsear&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archivo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 500K filas en memoria&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Registro&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;registros&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;validar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;transformar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;guardar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;               &lt;span class="c1"&gt;// 500K INSERTs, uno por uno&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Listo"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contemos los problemas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Todo en memoria&lt;/strong&gt;: 500K objetos Java. Si cada uno pesa 1KB, son 500MB solo en datos. Más los objetos temporales, fácilmente llegás a 2GB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todo secuencial&lt;/strong&gt;: un registro a la vez. Si cada INSERT tarda 2ms, son 1000 segundos. Más de 16 minutos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todo síncrono&lt;/strong&gt;: el usuario espera con el browser abierto. El thread HTTP queda bloqueado.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cero feedback&lt;/strong&gt;: el usuario no sabe si va por el registro 100 o el 499.999.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Todo o nada&lt;/strong&gt;: si falla en la fila 499.999, ¿perdiste todo? ¿Hacés rollback de 499.998 registros?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  La versión que respeta al usuario (y al servidor)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Paso 1: Recibí y respondé inmediatamente
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@PostMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/importar"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;ImportacionResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;importar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="nd"&gt;@RequestParam&lt;/span&gt; &lt;span class="nc"&gt;MultipartFile&lt;/span&gt; &lt;span class="n"&gt;archivo&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jobId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importacionService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;encolar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archivo&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;ResponseEntity&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;accepted&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ImportacionResponse&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"PROCESANDO"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"/importaciones/"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;jobId&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;"/estado"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El usuario sube el archivo y en menos de un segundo recibe un ID de trabajo y una URL para consultar el progreso. No espera. No hay timeout. No hay pantalla de "cargando" durante 4 minutos.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP 202 Accepted&lt;/strong&gt; — "recibí tu pedido, te aviso cuando esté". Ese es el status code correcto para operaciones asíncronas. No 200. No 201. &lt;strong&gt;202&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paso 2: Procesá en background por batches
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Async&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"importacionExecutor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;procesar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;jobId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt; &lt;span class="n"&gt;archivoPath&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Importacion&lt;/span&gt; &lt;span class="n"&gt;importacion&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByJobId&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;jobId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEstado&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"EN_PROGRESO"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Stream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Registro&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;registros&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;archivoPath&lt;/span&gt;&lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Registro&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;procesados&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Registro&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;registros&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;validar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;)));&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;ValidationException&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;incrementarErrores&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// UN saveAll cada 1000&lt;/span&gt;
                &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
                &lt;span class="n"&gt;procesados&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
                &lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setProcesados&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;procesados&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;// Guardar ultimo batch parcial&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;isEmpty&lt;/span&gt;&lt;span class="o"&gt;())&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;saveAll&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEstado&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"COMPLETADO"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setEstado&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ERROR"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;importacion&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres cosas clave cambiaron:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stream en vez de lista&lt;/strong&gt;: no cargás 500K filas en memoria. Las leés de a poco. En Python es un generator. En Node es un readable stream. En Go es un scanner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Batches de 1000&lt;/strong&gt;: en vez de un INSERT por fila, hacés un &lt;code&gt;saveAll&lt;/code&gt; cada 1000 registros. La base de datos te lo agradece — un bulk insert de 1000 es &lt;em&gt;mucho&lt;/em&gt; más rápido que 1000 inserts individuales.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Progreso persistido&lt;/strong&gt;: el usuario puede consultar cuántas filas se procesaron y cuántas fallaron. Sin adivinar.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Paso 3: Configurá el thread pool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"importacionExecutor"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Executor&lt;/span&gt; &lt;span class="nf"&gt;importacionExecutor&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;ThreadPoolTaskExecutor&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolTaskExecutor&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setCorePoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setMaxPoolSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setQueueCapacity&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setThreadNamePrefix&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"import-"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setRejectedExecutionHandler&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CallerRunsPolicy&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
    &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;No uses el executor por defecto.&lt;/strong&gt; Si no configurás un pool, Spring crea un thread nuevo por cada tarea. Con 50 usuarios subiendo archivos a la vez, tenés 50 threads compitiendo por CPU y memoria. Eso no escala.&lt;/p&gt;

&lt;p&gt;El &lt;code&gt;CallerRunsPolicy&lt;/code&gt; es elegante: si la cola está llena, en vez de tirar un error, ejecuta la tarea en el thread del caller. Funciona como back-pressure natural. "Estoy ocupado, hacelo vos."&lt;/p&gt;

&lt;h2&gt;
  
  
  Decile al usuario qué está pasando
&lt;/h2&gt;

&lt;p&gt;La UX importa tanto como la arquitectura. El peor error es dejar al usuario adivinando.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"jobId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc-123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"estado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EN_PROGRESO"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"totalRegistros"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"procesados"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;125000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errores"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"porcentaje"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tiempoEstimado"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3 minutos"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El frontend hace polling cada 5 segundos (o mejor: usa WebSocket/SSE) y muestra una barra de progreso real. El usuario ve que algo está pasando. No se pone ansioso. No recarga la página. No genera requests duplicados.&lt;/p&gt;

&lt;p&gt;Esto aplica a cualquier framework y cualquier frontend. La respuesta JSON con progreso es universal.&lt;/p&gt;

&lt;h2&gt;
  
  
  La regla de los tres tiempos
&lt;/h2&gt;

&lt;p&gt;Si tu operación tarda &lt;strong&gt;segundos&lt;/strong&gt;, puede ser síncrona. Respondé en el request, nadie se queja.&lt;/p&gt;

&lt;p&gt;Si tarda &lt;strong&gt;minutos&lt;/strong&gt;, tiene que ser asíncrona. Recibí, encolá, procesá en background, notificá.&lt;/p&gt;

&lt;p&gt;Si tarda &lt;strong&gt;horas&lt;/strong&gt;, necesitás un sistema de colas dedicado (RabbitMQ, SQS, Kafka) o un framework de batch processing (Spring Batch, Celery, Temporal). Y probablemente necesitás retry, dead letter queue, y monitoreo.&lt;/p&gt;

&lt;p&gt;No es una cuestión de elegancia. Es que &lt;strong&gt;HTTP no fue diseñado para operaciones de larga duración&lt;/strong&gt;. El timeout del load balancer, el timeout del browser, el timeout del proxy — todo te va a explotar en la cara si intentás forzar una operación de minutos en un protocol de milisegundos.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO usar async
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Operaciones que tardan menos de 5 segundos&lt;/strong&gt; — La complejidad del async no se justifica. Dejalo síncrono.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuando el usuario necesita el resultado para continuar&lt;/strong&gt; — Si el paso siguiente depende del resultado, async no ayuda. El usuario va a quedarse esperando igual, pero haciendo polling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin infraestructura para monitoreo&lt;/strong&gt; — Los jobs async que fallan silenciosamente son un clásico de terror. Si no podés ver los jobs fallidos, estás volando a ciegas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Con archivos pequeños&lt;/strong&gt; — 100 registros no justifican toda esta maquinaria. Procesalos síncrono, respondé en 200ms, y seguí con tu vida.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Esto es el Día 8
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. Si tu endpoint tiene un timeout de 4 minutos, no necesitás un timeout más largo. Necesitás otra arquitectura.&lt;/p&gt;

&lt;p&gt;Y lo más importante: tratá al usuario como adulto. Decile "esto tarda 3 minutos" y mostrále el progreso. Es mil veces mejor que una pantalla en blanco con un spinner eterno.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si te está sirviendo, dejame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 7: ¿Quién te mandó a optimizar si ni siquiera mediste?</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:21:11 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-7-quien-te-mando-a-optimizar-si-ni-siquiera-mediste-52d1</link>
      <guid>https://dev.to/alafourcadev/dia-7-quien-te-mando-a-optimizar-si-ni-siquiera-mediste-52d1</guid>
      <description>&lt;p&gt;Después del post del Día 6 sobre caché, un amigo me escribió: "Ale, leí tu artículo y me puse a optimizar todo. Moví queries a vistas materializadas, metí un CDN, cambié el serializer de JSON. La app sigue tardando 3 segundos."&lt;/p&gt;

&lt;p&gt;Le pregunté: "¿Y mediste dónde está el cuello de botella?"&lt;/p&gt;

&lt;p&gt;Silencio.&lt;/p&gt;

&lt;p&gt;"¿Al menos sabés cuál de los 4 servicios que llama tu endpoint es el lento?"&lt;/p&gt;

&lt;p&gt;Más silencio.&lt;/p&gt;

&lt;p&gt;Ahí le dije lo que te voy a decir a vos: &lt;strong&gt;¿quién te mandó a optimizar si ni siquiera mediste?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto tiene nombre: se llama profiling
&lt;/h2&gt;

&lt;p&gt;Lo que mi amigo necesitaba no es una optimización. Es un &lt;strong&gt;diagnóstico&lt;/strong&gt;. Y la técnica para hacerlo existe desde hace décadas: se llama &lt;strong&gt;profiling&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Profiling es medir cuánto tiempo tarda cada parte de tu código. No adivinar, no intuir — medir con números reales. Es la diferencia entre un médico que te dice "tomá ibuprofeno, algo te debe doler" y uno que te hace estudios antes de recetarte algo.&lt;/p&gt;

&lt;p&gt;Todos los lenguajes y frameworks tienen herramientas para esto:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Java/Spring:&lt;/strong&gt; Micrometer + Actuator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Python:&lt;/strong&gt; cProfile, Py-Spy, Django Debug Toolbar&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js:&lt;/strong&gt; Clinic.js, built-in profiler, OpenTelemetry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Go:&lt;/strong&gt; pprof (viene integrado en el runtime)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;.NET:&lt;/strong&gt; dotnet-trace, Application Insights&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ruby/Rails:&lt;/strong&gt; rack-mini-profiler, Stackprof&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La herramienta cambia. El concepto es universal: &lt;strong&gt;antes de optimizar, medí.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  El primo peligroso de la optimización prematura
&lt;/h2&gt;

&lt;p&gt;Todos conocemos la frase de Knuth: "la optimización prematura es la raíz de todos los males." Pero hay un primo hermano igual de peligroso que nadie nombra: la &lt;strong&gt;optimización ciega&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;"Seguro es la base de datos." No mediste. "Debe ser la serialización JSON." No mediste. "El servicio externo tarda mucho." Tampoco mediste.&lt;/p&gt;

&lt;p&gt;Y así terminás optimizando cosas que tardan 2ms mientras el verdadero culpable — un servicio que tarda 3.2 segundos — sigue ahí, invisible, arruinándote la vida.&lt;/p&gt;

&lt;h2&gt;
  
  
  El caso de mi amigo
&lt;/h2&gt;

&lt;p&gt;Mirá el endpoint que me mandó. Tarda 3.3 segundos y nadie sabe por qué:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@RestController&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PedidoController&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/pedidos/{id}"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PedidoDTO&lt;/span&gt; &lt;span class="nf"&gt;obtenerPedido&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nd"&gt;@PathVariable&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;Pedido&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pedidoService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buscar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;           &lt;span class="c1"&gt;// ¿cuánto tarda?&lt;/span&gt;
        &lt;span class="nc"&gt;Cliente&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;clienteService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buscar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getClienteId&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt; &lt;span class="c1"&gt;// ¿cuánto?&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Producto&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;productos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;productoService&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;buscarPorIds&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getProductoIds&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;          &lt;span class="c1"&gt;// ¿cuánto?&lt;/span&gt;
        &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="n"&gt;descuento&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;descuentoService&lt;/span&gt;
            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;calcular&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cliente&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;productos&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;                   &lt;span class="c1"&gt;// ¿cuánto?&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PedidoDTO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pedido&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;productos&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;descuento&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cuatro llamadas. ¿Cuál es la lenta? No tenés idea. Y sin datos, estás tirando dardos con los ojos vendados.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo se hace en Spring Boot (pero el concepto es el mismo en cualquier stack)
&lt;/h2&gt;

&lt;p&gt;En Spring Boot, las herramientas de profiling son &lt;strong&gt;Actuator&lt;/strong&gt; (expone métricas por HTTP) y &lt;strong&gt;Micrometer&lt;/strong&gt; (instrumenta tu código con timers). Es el equivalente a pprof en Go o cProfile en Python.&lt;/p&gt;

&lt;p&gt;Agregá las dependencias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springframework.boot&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;spring-boot-starter-actuator&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;io.micrometer&lt;span class="nt"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;micrometer-registry-prometheus&lt;span class="nt"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configurá los endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;management&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;endpoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;exposure&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;include&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;health, metrics, prometheus&lt;/span&gt;
  &lt;span class="na"&gt;metrics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mi-ecommerce&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Con esto ya tenés &lt;code&gt;/actuator/metrics&lt;/code&gt; expuesto. Podés ver cuánto tardan tus endpoints, cuántas conexiones a la base de datos estás usando, el estado del thread pool, la memoria. Todo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Instrumentá cada operación
&lt;/h2&gt;

&lt;p&gt;El segundo paso es ponerle un timer a cada operación que sospechás. En cualquier lenguaje el patrón es el mismo: "empezá a contar, ejecutá la operación, pará de contar". En Spring Boot se hace con Micrometer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PedidoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;MeterRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="nc"&gt;PedidoRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PedidoService&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MeterRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PedidoRepository&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;registry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Pedido&lt;/span&gt; &lt;span class="nf"&gt;buscar&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"pedido.buscar"&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;record&lt;/span&gt;&lt;span class="o"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
            &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Hacé lo mismo con cada servicio. Ahora cuando consultás &lt;code&gt;/actuator/metrics/pedido.buscar&lt;/code&gt; ves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pedido.buscar"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"measurements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COUNT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1523&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TOTAL_TIME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2.41&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MAX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.008&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;2.41 segundos en total, 1523 llamadas, máximo 8ms. Este servicio no es el problema.&lt;/p&gt;

&lt;p&gt;Ahora mirás &lt;code&gt;descuento.calcular&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"descuento.calcular"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"measurements"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"COUNT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1523&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"TOTAL_TIME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4562.7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"statistic"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MAX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;3.2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4562 segundos acumulados. Máximo 3.2 segundos por llamada. &lt;strong&gt;Ahí está tu cuello de botella.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Resulta que el servicio de descuentos llama a una API externa que tarda un promedio de 3 segundos. Pero nadie lo sabía porque nadie midió.&lt;/p&gt;

&lt;h2&gt;
  
  
  @Timed: la versión declarativa
&lt;/h2&gt;

&lt;p&gt;Si no querés meter &lt;code&gt;registry.timer()&lt;/code&gt; en cada método, Spring tiene una anotación que hace lo mismo de forma más limpia:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DescuentoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Timed&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"descuento.calcular"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Tiempo de cálculo de descuentos"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
           &lt;span class="n"&gt;percentiles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="nf"&gt;calcular&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Cliente&lt;/span&gt; &lt;span class="n"&gt;cliente&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Producto&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;productos&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// tu lógica&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Los percentiles te dan información mucho más útil que el promedio. El p95 de 3.2 segundos te dice que el 5% de tus usuarios esperan más de 3 segundos. El promedio de 300ms te mentiría.&lt;/p&gt;

&lt;p&gt;No te olvides de registrar el aspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Bean&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;TimedAspect&lt;/span&gt; &lt;span class="nf"&gt;timedAspect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;MeterRegistry&lt;/span&gt; &lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;TimedAspect&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;registry&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  El waterfall real
&lt;/h2&gt;

&lt;p&gt;Una vez que medís todo, el panorama se ve así:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pedido.buscar        ████  8ms
cliente.buscar       ██████  15ms
producto.buscarIds   ████████  22ms
descuento.calcular   ████████████████████████████████████████  3200ms
                     |--- Aquí está el 98% del tiempo ---|
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sin medir, hubieras cacheado &lt;code&gt;producto.buscarIds&lt;/code&gt; (22ms) pensando que era el culpable. Con datos, sabés exactamente qué atacar.&lt;/p&gt;

&lt;h2&gt;
  
  
  La solución al cuello de botella real
&lt;/h2&gt;

&lt;p&gt;Ahora que sabés que &lt;code&gt;descuento.calcular&lt;/code&gt; es el problema, podés tomar decisiones informadas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;¿Se puede cachear el resultado del descuento? Sí, si los descuentos no cambian por cada request.&lt;/li&gt;
&lt;li&gt;¿Se puede hacer la llamada async? Sí, si podés mostrar el pedido sin el descuento y cargarlo después.&lt;/li&gt;
&lt;li&gt;¿Se puede reemplazar la API externa? Quizás, internalizando las reglas de descuento.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;La decisión correcta sale de los datos, no de la intuición.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO hacer profiling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;En producción sin cuidado&lt;/strong&gt; — Las métricas consumen recursos. Micrometer es liviano, pero si instrumentás cada línea de cada método vas a generar overhead. Medí lo que importa.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Con datos de desarrollo&lt;/strong&gt; — Tu máquina local con 3 registros no reproduce un problema de producción con 3 millones. Hacé profiling con datos representativos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Para micro-optimizaciones&lt;/strong&gt; — Si tu endpoint tarda 50ms en total y querés bajar a 45ms, probablemente no valga la pena el esfuerzo. Enfocate en los que tardan segundos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sin un baseline&lt;/strong&gt; — Si no sabés cuánto tardaba antes, no podés saber si mejoraste. Medí antes de tocar nada.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  La regla que me salvó cien veces
&lt;/h2&gt;

&lt;p&gt;Antes de optimizar cualquier cosa, respondé estas tres preguntas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;¿Dónde está el cuello de botella?&lt;/strong&gt; (Datos, no intuición.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;¿Cuánto impacto tiene?&lt;/strong&gt; (Si es el 2% del tiempo total, no importa.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;¿Cuál es el costo de optimizarlo?&lt;/strong&gt; (A veces la solución es más cara que el problema.)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;La optimización no es una actividad creativa. Es una actividad científica. Hipótesis, medición, conclusión. Y esto aplica igual si estás en Spring Boot, Django, Express o Rails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 7
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. La próxima vez que alguien diga "seguro es la base de datos", pedile los números.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si te está sirviendo, dejame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 6: Tu caché no funciona y es tu culpa</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:20:55 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-6-tu-cache-no-funciona-y-es-tu-culpa-e5m</link>
      <guid>https://dev.to/alafourcadev/dia-6-tu-cache-no-funciona-y-es-tu-culpa-e5m</guid>
      <description>&lt;p&gt;Pusiste &lt;code&gt;@Cacheable&lt;/code&gt; en cada método del servicio. La app sigue lenta. Bienvenido al club.&lt;/p&gt;

&lt;h2&gt;
  
  
  El error que todos cometemos
&lt;/h2&gt;

&lt;p&gt;La primera vez que descubrís caché, es como descubrir el martillo. De repente todo parece un clavo. Endpoint lento? &lt;code&gt;@Cacheable&lt;/code&gt;. Query pesada? &lt;code&gt;@Cacheable&lt;/code&gt;. El servicio externo tarda? &lt;code&gt;@Cacheable&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Y funciona. Al principio.&lt;/p&gt;

&lt;p&gt;Después llegan los bugs. Silenciosos. Difíciles de reproducir. El tipo de bugs que te hacen cuestionar tu carrera.&lt;/p&gt;

&lt;h2&gt;
  
  
  El desastre en acción
&lt;/h2&gt;

&lt;p&gt;Mirá este servicio de e-commerce:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"productos"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Producto&lt;/span&gt; &lt;span class="nf"&gt;obtenerProducto&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;productoRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"precios"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="nf"&gt;obtenerPrecio&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;precioRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findPrecioActual&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"stock"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;obtenerStock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inventarioClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consultarStock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Parece prolijo, ¿no? Cacheable en todo. Performance al máximo.&lt;/p&gt;

&lt;p&gt;Ahora imaginá esto: un usuario ve un producto a $50.000. Lo agrega al carrito. Mientras tanto, el precio sube a $55.000. El usuario compra. ¿Qué precio le cobrás? El de la base de datos: $55.000. Pero él vio $50.000.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Felicitaciones. Acabás de generar un reclamo, una devolución, y posiblemente un problema legal.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;¿Y el stock? Cacheaste que hay 3 unidades. Pero se vendieron todas hace 2 minutos. Ahora vendiste algo que no tenés.&lt;/p&gt;

&lt;p&gt;El caché no falló. &lt;strong&gt;Vos cacheaste lo que no debías.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Las reglas del caché que nadie te enseña
&lt;/h2&gt;

&lt;p&gt;El caché funciona cuando se dan &lt;strong&gt;dos condiciones juntas&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;El dato cambia pocas veces&lt;/strong&gt; — Categorías de producto, configuraciones del sistema, catálogos, roles de usuario.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El dato se lee muchas veces&lt;/strong&gt; — Endpoints que reciben cientos o miles de requests por minuto.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si falta una de las dos, el caché no te sirve. O peor: te perjudica.&lt;/p&gt;

&lt;p&gt;Los precios cambian. El stock cambia. Los datos de sesión cambian. &lt;strong&gt;Eso no se cachea.&lt;/strong&gt; O si se cachea, se hace con una estrategia muy deliberada y un TTL agresivamente corto.&lt;/p&gt;

&lt;h2&gt;
  
  
  La versión que sí funciona
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductoService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Categorías: cambian una vez por semana. Se leen miles de veces por día.&lt;/span&gt;
    &lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"categorias"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#categoriaId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Categoria&lt;/span&gt; &lt;span class="nf"&gt;obtenerCategoria&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;categoriaId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;categoriaRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findById&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;categoriaId&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;orElseThrow&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Configuración del sistema: cambia cuando alguien la modifica manualmente.&lt;/span&gt;
    &lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"configuracion"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;ConfiguracionTienda&lt;/span&gt; &lt;span class="nf"&gt;obtenerConfiguracion&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;configuracionRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findActiva&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Precio: NUNCA cacheado. Siempre fresco.&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;BigDecimal&lt;/span&gt; &lt;span class="nf"&gt;obtenerPrecio&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;precioRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findPrecioActual&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Stock: NUNCA cacheado. Siempre en tiempo real.&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;obtenerStock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;inventarioClient&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;consultarStock&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;productoId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y cuando alguien modifica una categoría:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@CacheEvict&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"categorias"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#categoriaId"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;actualizarCategoria&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;categoriaId&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;CategoriaRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Actualizar en base de datos&lt;/span&gt;
    &lt;span class="c1"&gt;// El caché se invalida automáticamente&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Ves la diferencia? No cacheamos todo. Cacheamos &lt;strong&gt;lo correcto&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las 4 preguntas antes de cachear
&lt;/h2&gt;

&lt;p&gt;Antes de poner &lt;code&gt;@Cacheable&lt;/code&gt; en cualquier cosa, hacete estas preguntas:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. ¿Con qué frecuencia cambia este dato?&lt;/strong&gt;&lt;br&gt;
Si cambia cada minuto, no lo cachees. Si cambia una vez por día, es candidato.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. ¿Con qué frecuencia se lee?&lt;/strong&gt;&lt;br&gt;
Si se lee 10 veces por hora, no vale la pena. Si se lee 10.000 veces por minuto, es candidato perfecto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. ¿Qué pasa si el dato está desactualizado?&lt;/strong&gt;&lt;br&gt;
Configuración de colores del sitio desactualizada 5 minutos? Nadie se muere. Precio desactualizado 5 minutos? Alguien pierde plata.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. ¿Cómo lo invalido?&lt;/strong&gt;&lt;br&gt;
Si no tenés una estrategia clara de invalidación, &lt;strong&gt;no lo cachees&lt;/strong&gt;. Un caché sin invalidación es una bomba de tiempo.&lt;/p&gt;
&lt;h2&gt;
  
  
  Qué cachear vs qué NO
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dato&lt;/th&gt;
&lt;th&gt;Cambia&lt;/th&gt;
&lt;th&gt;Se lee&lt;/th&gt;
&lt;th&gt;¿Cachear?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Categorías&lt;/td&gt;
&lt;td&gt;1x/mes&lt;/td&gt;
&lt;td&gt;1000x/día&lt;/td&gt;
&lt;td&gt;✅ SÍ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Configuraciones&lt;/td&gt;
&lt;td&gt;1x/semana&lt;/td&gt;
&lt;td&gt;500x/día&lt;/td&gt;
&lt;td&gt;✅ SÍ&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Precios&lt;/td&gt;
&lt;td&gt;cada minuto&lt;/td&gt;
&lt;td&gt;100x/día&lt;/td&gt;
&lt;td&gt;❌ NO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stock&lt;/td&gt;
&lt;td&gt;cada compra&lt;/td&gt;
&lt;td&gt;50x/día&lt;/td&gt;
&lt;td&gt;❌ NO&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sesión usuario&lt;/td&gt;
&lt;td&gt;cada request&lt;/td&gt;
&lt;td&gt;1x&lt;/td&gt;
&lt;td&gt;❌ NO&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  Cuándo NO cachear
&lt;/h2&gt;

&lt;p&gt;Esto aplica a cualquier caché. Redis, Memcached, CDN, browser cache, lo que sea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Datos financieros en tiempo real&lt;/strong&gt; — Precios, saldos, tasas de cambio. El costo de un dato stale es demasiado alto.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stock e inventario&lt;/strong&gt; — Vender algo que no tenés es peor que una query lenta.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datos de sesión o autenticación&lt;/strong&gt; — Un usuario ve los datos de otro. Pesadilla de seguridad.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resultados que dependen del momento&lt;/strong&gt; — Rankings en vivo, dashboards real-time, contadores de stock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datos que cambian con cada request&lt;/strong&gt; — Si cada llamada devuelve algo distinto, cachear no tiene sentido.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La regla es simple: &lt;strong&gt;si el costo de servir un dato viejo es mayor que el costo de una query lenta, no cachees.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  El TTL no es opcional
&lt;/h2&gt;

&lt;p&gt;Si vas a cachear, definí un TTL. Siempre. Un caché sin TTL es un dato que nunca se refresca hasta que reiniciés la aplicación o la memoria explote.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Cacheable&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"categorias"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"#id"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// + configuración de TTL en tu cache manager:&lt;/span&gt;
&lt;span class="c1"&gt;// categorias -&amp;gt; TTL: 1 hora&lt;/span&gt;
&lt;span class="c1"&gt;// configuracion -&amp;gt; TTL: 30 minutos&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El TTL es tu red de seguridad. Incluso si tu invalidación falla, el dato se refresca eventualmente.&lt;/p&gt;

&lt;h2&gt;
  
  
  El caché es una decisión de arquitectura
&lt;/h2&gt;

&lt;p&gt;El caché no es un decorator que tirás encima del código para que ande más rápido. Es una &lt;strong&gt;decisión de arquitectura&lt;/strong&gt; que implica tradeoffs reales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Consistencia vs. performance&lt;/li&gt;
&lt;li&gt;Memoria vs. latencia&lt;/li&gt;
&lt;li&gt;Complejidad vs. velocidad&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cada &lt;code&gt;@Cacheable&lt;/code&gt; que ponés es un contrato que dice: "acepto que este dato puede estar desactualizado por X tiempo, y las consecuencias son aceptables."&lt;/p&gt;

&lt;p&gt;Si no podés articular ese contrato, no cachees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 6
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. No teoría abstracta. Código que podés correr y medir.&lt;/p&gt;

&lt;p&gt;La próxima vez que estés por poner &lt;code&gt;@Cacheable&lt;/code&gt; en un método, pará 30 segundos y hacete las 4 preguntas. Te van a ahorrar semanas de debugging de bugs fantasma que nadie puede reproducir.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si te está sirviendo, dejame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 5: N+1 Queries — el bug que tu DBA ya sabe que tenés</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:19:26 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-5-n1-queries-el-bug-que-tu-dba-ya-sabe-que-tenes-4pc</link>
      <guid>https://dev.to/alafourcadev/dia-5-n1-queries-el-bug-que-tu-dba-ya-sabe-que-tenes-4pc</guid>
      <description>&lt;p&gt;Para mostrar 50 usuarios tu app hace 251 queries a la base de datos. Y vos ni te enteraste.&lt;/p&gt;

&lt;p&gt;No falla. No tira excepción. No aparece en ningún log por default. Simplemente tu página tarda 4 segundos en cargar y nadie sabe por qué. Bueno, tu DBA sí sabe. Y te odia.&lt;/p&gt;

&lt;h2&gt;
  
  
  El supermercado
&lt;/h2&gt;

&lt;p&gt;Imaginá que necesitás comprar 50 cosas. Vas al supermercado, comprás la primera, volvés a tu casa. Vas de nuevo, comprás la segunda, volvés. Y así 50 veces.&lt;/p&gt;

&lt;p&gt;Ridículo, ¿no? Nadie haría eso en la vida real.&lt;/p&gt;

&lt;p&gt;Bueno, tu ORM lo hace. Cada vez que accedés a una relación lazy, dispara un SELECT nuevo. Un viaje más a la base de datos. Uno por cada entidad. Y vos mirando el endpoint pensando "no sé por qué anda lento".&lt;/p&gt;

&lt;p&gt;Esto se llama el &lt;strong&gt;problema N+1&lt;/strong&gt; y es, sin exagerar, el bug de performance más común en cualquier aplicación que use un ORM. No importa si es Hibernate, Django, ActiveRecord, Entity Framework o Prisma. &lt;strong&gt;Todos los ORMs tienen este problema.&lt;/strong&gt; Si tu framework carga relaciones de forma lazy por default, estás expuesto.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ANTES: lazy loading te arruina el día
&lt;/h2&gt;

&lt;p&gt;Tenés un &lt;code&gt;Usuario&lt;/code&gt; con una lista de &lt;code&gt;Pedido&lt;/code&gt;. Clásico:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Usuario&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@Id&lt;/span&gt;
    &lt;span class="nd"&gt;@GeneratedValue&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nombre&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"usuario"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pedido&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// LAZY por default&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y en tu servicio hacés algo inocente:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Usuario&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;usuarios&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;usuarioRepository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Usuario&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;usuarios&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getNombre&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s"&gt;": "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getPedidos&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activá &lt;code&gt;spring.jpa.show-sql=true&lt;/code&gt; y mirá el horror:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query 1: traer todos los usuarios&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;usuario&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Query 2: pedidos del usuario 1&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;usuario_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Query 3: pedidos del usuario 2&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;usuario_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- Query 4: pedidos del usuario 3&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;usuario_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- ...&lt;/span&gt;
&lt;span class="c1"&gt;-- Query 51: pedidos del usuario 50&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;usuario_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;1 query para los usuarios + 50 queries para los pedidos = 51 queries.&lt;/strong&gt; Si cada usuario tiene 5 pedidos y cada pedido tiene ítems... ya estás en las 251 queries del título. Y eso para &lt;strong&gt;una sola request HTTP&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Multiplicá por 100 usuarios concurrentes y entendés por qué tu base de datos está llorando.&lt;/p&gt;

&lt;h2&gt;
  
  
  El DESPUÉS: un solo viaje al supermercado
&lt;/h2&gt;

&lt;p&gt;La solución es decirle a Hibernate: "traeme todo de una, no seas vago".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Opción 1: JOIN FETCH en JPQL&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT u FROM Usuario u JOIN FETCH u.pedidos"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Usuario&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllConPedidos&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Opción 2: @EntityGraph (más declarativo)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@EntityGraph&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attributePaths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"pedidos"&lt;/span&gt;&lt;span class="o"&gt;})&lt;/span&gt;
&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT u FROM Usuario u"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Usuario&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllConPedidos&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Resultado en la base de datos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- UNA sola query&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;usuario&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;
&lt;span class="k"&gt;LEFT&lt;/span&gt; &lt;span class="k"&gt;JOIN&lt;/span&gt; &lt;span class="n"&gt;pedido&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usuario_id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;251 queries se convierten en 1.&lt;/strong&gt; Un solo viaje. Todo lo que necesitás, de una sola vez.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ANTES&lt;/th&gt;
&lt;th&gt;DESPUÉS&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Queries&lt;/td&gt;
&lt;td&gt;251&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99.6%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo&lt;/td&gt;
&lt;td&gt;~4200ms&lt;/td&gt;
&lt;td&gt;~85ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;98%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No es una mejora marginal. Es la diferencia entre una app que funciona y una que da vergüenza.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo detectar N+1 antes de producción
&lt;/h2&gt;

&lt;p&gt;No esperés a que tu DBA te mande un mensaje pasivo-agresivo. Detectalo vos:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Activá el logging de SQL en desarrollo:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jpa&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;show-sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hibernate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;format_sql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si ves el mismo SELECT repitiéndose con diferentes parámetros, tenés un N+1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Usá las estadísticas de Hibernate:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;spring&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;jpa&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hibernate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;generate_statistics&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En el log vas a ver algo como:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Session Metrics {
    1234567 nanoseconds spent executing 251 JDBC statements
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;251 statements para un endpoint que debería hacer 1 o 2 queries. Ahí lo tenés. Evidencia irrefutable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Regla de oro:&lt;/strong&gt; si un endpoint ejecuta más de 10 queries, algo está mal. No hay excusa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO usar JOIN FETCH
&lt;/h2&gt;

&lt;p&gt;Antes de que salgas a ponerle JOIN FETCH a todo (sí, te vi), hay un caso donde te va a explotar en la cara:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="c1"&gt;// NUNCA hagas esto&lt;/span&gt;
&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT u FROM Usuario u JOIN FETCH u.pedidos JOIN FETCH u.direcciones"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Usuario&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findAllCompleto&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si un usuario tiene 5 pedidos y 3 direcciones, Hibernate genera un &lt;strong&gt;producto cartesiano&lt;/strong&gt;: 5 x 3 = 15 filas por usuario. Con 50 usuarios son 750 filas. Y la JVM tiene que deduplicar todo eso en memoria.&lt;/p&gt;

&lt;p&gt;La regla: &lt;strong&gt;JOIN FETCH con una sola colección a la vez.&lt;/strong&gt; Si necesitás múltiples colecciones, usá &lt;code&gt;@BatchSize&lt;/code&gt; o queries separadas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Entity&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Usuario&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"usuario"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@BatchSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Carga en lotes de 50 en vez de 1 por 1&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Pedido&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;pedidos&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;

    &lt;span class="nd"&gt;@OneToMany&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mappedBy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"usuario"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="nd"&gt;@BatchSize&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Direccion&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;direcciones&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;@BatchSize&lt;/code&gt; convierte 50 queries individuales en 1 query con un &lt;code&gt;IN&lt;/code&gt; clause. No es tan eficiente como JOIN FETCH, pero evita el producto cartesiano.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto no es solo Java
&lt;/h2&gt;

&lt;p&gt;Si usás Django y hacés &lt;code&gt;User.objects.all()&lt;/code&gt; y después accedés a &lt;code&gt;user.orders&lt;/code&gt; en un loop, tenés el mismo problema. La solución allá es &lt;code&gt;select_related&lt;/code&gt; y &lt;code&gt;prefetch_related&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Si usás Rails, es &lt;code&gt;includes(:orders)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Si usás Entity Framework, es &lt;code&gt;.Include(u =&amp;gt; u.Orders)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Si usás Prisma, es el &lt;code&gt;include: { orders: true }&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El patrón es universal.&lt;/strong&gt; Cambia la sintaxis, el problema es idéntico. Si tu ORM carga relaciones de forma lazy y vos iterás sin pensar, estás haciendo N+1. No importa el lenguaje, no importa el framework.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 5
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. No teoría abstracta. Código que podés correr y medir.&lt;/p&gt;

&lt;p&gt;La próxima vez que un endpoint ande lento, antes de escalar horizontalmente, antes de agregar un cache, antes de culpar a la base de datos... activá &lt;code&gt;show-sql&lt;/code&gt; y contá las queries.&lt;/p&gt;

&lt;p&gt;Te sorprendería la cantidad de problemas de performance que se resuelven con un JOIN FETCH bien puesto.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si te está sirviendo, dejame una estrella — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 4: Tu endpoint devuelve TODO. El frontend explota. La red llora.</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:19:08 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-4-tu-endpoint-devuelve-todo-el-frontend-explota-la-red-llora-3poi</link>
      <guid>https://dev.to/alafourcadev/dia-4-tu-endpoint-devuelve-todo-el-frontend-explota-la-red-llora-3poi</guid>
      <description>&lt;p&gt;Tu endpoint de productos funciona perfecto en desarrollo. 100 registros, respuesta instantánea. Pero en producción tenés 50,000 productos y cada request a &lt;code&gt;/api/products&lt;/code&gt; carga los 50,000 en memoria, los serializa a JSON, y manda 10MB por la red.&lt;/p&gt;

&lt;p&gt;El frontend hace un &lt;code&gt;JSON.parse()&lt;/code&gt; de 10MB. El navegador se congela. El usuario cierra la pestaña. Y vos mirando los logs pensando "en mi máquina funciona".&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real
&lt;/h2&gt;

&lt;p&gt;No paginar no es un shortcut. Es una bomba de tiempo.&lt;/p&gt;

&lt;p&gt;Con 50,000 registros sin paginar, tu endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Carga todo en memoria&lt;/strong&gt; — potencial &lt;code&gt;OutOfMemoryError&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Serializa todo a JSON&lt;/strong&gt; — CPU al 100%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transmite todo por la red&lt;/strong&gt; — ~10MB por request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El cliente espera 30 segundos&lt;/strong&gt; — timeout y usuarios frustrados&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y lo peor: escala linealmente. Hoy son 50,000, mañana son 200,000. El problema solo crece.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ANTES: devolver todo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"before"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;NoPaginationService&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAll&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Los 50,000. Todos. Sin piedad.&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;-- 50,000 registros. RIP memoria.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  El DESPUÉS: dos estrategias, cada una con su lugar
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Offset Pagination — la clásica
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"offset"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OffsetPaginationService&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;PageRequest&lt;/span&gt; &lt;span class="n"&gt;pageRequest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Sort&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;by&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findAllProjectedBy&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pageRequest&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;OFFSET&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spring Data JPA te da &lt;code&gt;Pageable&lt;/code&gt; out of the box. Le pasás página y tamaño, y te devuelve un &lt;code&gt;Page&amp;lt;T&amp;gt;&lt;/code&gt; con los datos, el total de páginas, el total de elementos, si hay siguiente... todo. Para una UI con paginitas numeradas, es perfecto.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pero tiene un problema.&lt;/strong&gt; Cuanto más alto el offset, más lenta la query. ¿Por qué? Porque PostgreSQL tiene que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Leer &lt;strong&gt;todos&lt;/strong&gt; los registros hasta el offset&lt;/li&gt;
&lt;li&gt;Ordenarlos&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Descartar&lt;/strong&gt; los primeros N&lt;/li&gt;
&lt;li&gt;Devolver solo los siguientes M&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Página 1: lee 20 registros. Página 2000: lee 40,020 registros para devolver 20.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cursor Pagination — la escalable
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CursorPaginationService&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;ProductService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;Object&lt;/span&gt; &lt;span class="nf"&gt;getProducts&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Long&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Product&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findFirstPage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByCursorAfter&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cursor&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;PageRequest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;));&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;hasNext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
        &lt;span class="c1"&gt;// construir respuesta con nextCursor&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;products&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;En vez de decir "saltá 1000 y dame 20", le decís "dame los 20 que vienen después del ID 1000". PostgreSQL usa el índice, va directo al registro 1001, y lee 20. Siempre. No importa si estás en la página 1 o en la 2000.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Página 1 (offset=0):
  Offset:  5ms    |  Cursor:  5ms

Página 100 (offset=2000):
  Offset:  15ms   |  Cursor:  5ms

Página 1000 (offset=20000):
  Offset:  150ms  |  Cursor:  5ms

Página 2500 (offset=50000):
  Offset:  400ms+ |  Cursor:  5ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;¿Ves el patrón? Offset se degrada. Cursor se mantiene constante. &lt;strong&gt;O(n) vs O(1).&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Con 50,000 registros la diferencia es 400ms vs 5ms. Con 5 millones de registros, offset puede tardar segundos mientras cursor sigue en 5ms. Es una diferencia arquitectónica, no una micro-optimización.&lt;/p&gt;

&lt;h2&gt;
  
  
  ¿Cuál uso?
&lt;/h2&gt;

&lt;p&gt;No hay una respuesta universal. Depende de tu caso de uso:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Escenario&lt;/th&gt;
&lt;th&gt;Estrategia&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dashboard con tabla y paginitas numeradas&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Offset&lt;/strong&gt; — el usuario necesita saltar a la página 47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scroll infinito (feed, timeline)&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; — rendimiento constante en cada scroll&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API pública&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; — no querés que un cliente pida página 999999&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exportar datos en lotes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Cursor&lt;/strong&gt; — procesás de a chunks sin degradar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Panel admin con pocos registros&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Offset&lt;/strong&gt; — la simplicidad gana, metadata completa&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;La regla general: &lt;strong&gt;si el usuario necesita saltar a una página específica, usá offset. Si siempre avanza secuencialmente, usá cursor.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  La respuesta de cursor en la práctica
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;CursorPage&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;nextCursor&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;previousCursor&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;hasNext&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;boolean&lt;/span&gt; &lt;span class="n"&gt;hasPrevious&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;El truco está en pedir &lt;code&gt;size + 1&lt;/code&gt; registros. Si te vuelven 21 cuando pediste 20, sabés que hay más. Devolvés 20 y ponés &lt;code&gt;hasNext: true&lt;/code&gt; con el cursor del último elemento. El cliente manda ese cursor en la siguiente request. Elegante y sin queries extra para contar totales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO paginar
&lt;/h2&gt;

&lt;p&gt;Sí, hay casos donde paginar agrega complejidad innecesaria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Datasets que siempre van a ser chicos.&lt;/strong&gt; Si tu tabla de categorías tiene 15 registros y nunca va a tener más de 50, un &lt;code&gt;findAll()&lt;/code&gt; está bien. No sobre-ingenieres.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Búsquedas con filtros muy selectivos.&lt;/strong&gt; Si tu query siempre devuelve menos de 100 resultados porque los filtros son específicos, la paginación agrega overhead sin beneficio.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Datos que necesitás completos para calcular algo.&lt;/strong&gt; Si tenés que sumar todos los montos de un reporte, paginá en la base de datos y procesá en streaming, no pagines en la API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Endpoints internos entre microservicios.&lt;/strong&gt; Si el consumidor siempre necesita todo y está en la misma red, la paginación agrega latencia por el ida y vuelta extra.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pero si tu endpoint es público, si el dataset crece, si un usuario puede llegar con un browser: &lt;strong&gt;paginá. Siempre.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto no es solo Java
&lt;/h2&gt;

&lt;p&gt;Si usás Django, tenés &lt;code&gt;Paginator&lt;/code&gt; para offset y podés implementar cursor con &lt;code&gt;django-cursor-pagination&lt;/code&gt; o manualmente con filtros en el queryset.&lt;/p&gt;

&lt;p&gt;Si usás Rails, &lt;code&gt;kaminari&lt;/code&gt; y &lt;code&gt;will_paginate&lt;/code&gt; hacen offset. Para cursor, filtrás con &lt;code&gt;.where("id &amp;gt; ?", cursor).limit(size)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Si usás Express con Prisma, es &lt;code&gt;skip/take&lt;/code&gt; para offset y &lt;code&gt;cursor&lt;/code&gt; como parámetro nativo de Prisma.&lt;/p&gt;

&lt;p&gt;Si usás FastAPI con SQLAlchemy, es &lt;code&gt;.offset().limit()&lt;/code&gt; vs &lt;code&gt;.filter(Model.id &amp;gt; cursor).limit()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;El patrón es el mismo en todos los lenguajes.&lt;/strong&gt; Offset usa &lt;code&gt;OFFSET&lt;/code&gt; en SQL (o su equivalente) y se degrada linealmente. Cursor usa un &lt;code&gt;WHERE&lt;/code&gt; con un valor de referencia y mantiene rendimiento constante. No importa el framework — la base de datos se comporta igual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 4
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. Código que podés correr, medir, y ver cómo se degrada offset mientras cursor se mantiene firme.&lt;/p&gt;

&lt;p&gt;Si tu endpoint devuelve todo sin paginar, hoy es el día de arreglar eso. Tu base de datos, tu red, y tus usuarios te lo van a agradecer.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;💻 Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si te está sirviendo, dejame una ⭐ — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
    <item>
      <title>Día 3: Agregaste un índice y la consulta sigue lenta. El problema no era el índice.</title>
      <dc:creator>Alejandro Lafourcade Despaigne</dc:creator>
      <pubDate>Thu, 23 Apr 2026 20:18:48 +0000</pubDate>
      <link>https://dev.to/alafourcadev/dia-3-agregaste-un-indice-y-la-consulta-sigue-lenta-el-problema-no-era-el-indice-4hb7</link>
      <guid>https://dev.to/alafourcadev/dia-3-agregaste-un-indice-y-la-consulta-sigue-lenta-el-problema-no-era-el-indice-4hb7</guid>
      <description>&lt;p&gt;Agregaste un índice en la columna &lt;code&gt;status&lt;/code&gt;. Te sentiste bien. Pusheaste a producción. La query sigue tardando 3 segundos.&lt;/p&gt;

&lt;p&gt;El DBA te mira. Vos mirás el índice. El índice existe. Pero PostgreSQL lo ignora completamente. Como si no estuviera. Y vos sin entender por qué.&lt;/p&gt;

&lt;h2&gt;
  
  
  El problema real
&lt;/h2&gt;

&lt;p&gt;10,000 órdenes. Una query simple: buscar por estado y rango de fechas. Debería ser instantáneo. Pero no lo es.&lt;/p&gt;

&lt;p&gt;El índice está ahí. Lo podés ver con &lt;code&gt;\di&lt;/code&gt; en psql. Está creado, está sano, ocupa espacio en disco. Pero tu query hace &lt;strong&gt;Seq Scan&lt;/strong&gt; — lee las 10,000 filas una por una como si el índice no existiera.&lt;/p&gt;

&lt;p&gt;Spoiler: el índice no era el problema. Era &lt;strong&gt;cómo lo usabas&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  El ANTES: LOWER() te arruina el día
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT o FROM Order o WHERE LOWER(o.status) = LOWER(:status) "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"AND o.createdAt BETWEEN :start AND :end"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusIgnoreCaseAndDateRange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Se ve razonable, ¿no? "Por las dudas hago LOWER() para que sea case-insensitive." El problema es que ese &lt;code&gt;LOWER()&lt;/code&gt; le dice a PostgreSQL: "olvidate del índice".&lt;/p&gt;

&lt;p&gt;¿Por qué? Porque el índice está ordenado por el valor &lt;strong&gt;original&lt;/strong&gt; de la columna: &lt;code&gt;PENDING&lt;/code&gt;, &lt;code&gt;SHIPPED&lt;/code&gt;, &lt;code&gt;DELIVERED&lt;/code&gt;. Pero vos le estás pidiendo que busque por &lt;code&gt;LOWER(status)&lt;/code&gt; — un valor &lt;strong&gt;transformado&lt;/strong&gt;. PostgreSQL no puede usar un índice sobre &lt;code&gt;status&lt;/code&gt; para buscar sobre &lt;code&gt;LOWER(status)&lt;/code&gt;. Son cosas diferentes.&lt;/p&gt;

&lt;p&gt;Entonces hace lo único que puede: leer &lt;strong&gt;cada fila&lt;/strong&gt;, aplicarle &lt;code&gt;LOWER()&lt;/code&gt;, y comparar. Fila por fila. Las 10,000. Eso es un Seq Scan.&lt;/p&gt;

&lt;h2&gt;
  
  
  El DESPUÉS: normalizá antes, no durante
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;
&lt;span class="nd"&gt;@Profile&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"after"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OptimizedQueryService&lt;/span&gt; &lt;span class="kd"&gt;implements&lt;/span&gt; &lt;span class="nc"&gt;OrderService&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;

    &lt;span class="nd"&gt;@Override&lt;/span&gt;
    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findOrdersByStatusAndDateRange&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;normalizedStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toLowerCase&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;repository&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;findByStatusAndDateRangeOptimized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;normalizedStatus&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="nd"&gt;@Query&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SELECT o FROM Order o WHERE o.status = :status "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
       &lt;span class="s"&gt;"AND o.createdAt BETWEEN :start AND :end"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Order&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;findByStatusAndDateRangeOptimized&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"status"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"start"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;
    &lt;span class="nd"&gt;@Param&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"end"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="nc"&gt;LocalDateTime&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La diferencia: normalizamos el dato &lt;strong&gt;al insertarlo&lt;/strong&gt; (guardar siempre en minúsculas) y normalizamos el parámetro &lt;strong&gt;antes de la query&lt;/strong&gt; (en Java, no en SQL). Así PostgreSQL compara columna vs valor directamente, y el índice funciona.&lt;/p&gt;

&lt;h2&gt;
  
  
  EXPLAIN ANALYZE: tu mejor amigo
&lt;/h2&gt;

&lt;p&gt;No adivinés. Preguntale a la base de datos qué está haciendo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query LENTA&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2024-12-31'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Seq Scan on orders  (cost=0.00..285.00 rows=5000 width=64)
  Filter: ((lower(status) = 'pending') AND ...)
  Rows Removed by Filter: 5000
  Planning Time: 0.15 ms
  Execution Time: 45.23 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Seq Scan.&lt;/strong&gt; Esa es la palabra que tenés que temer. Significa que PostgreSQL está leyendo toda la tabla.&lt;/p&gt;

&lt;p&gt;Ahora la query optimizada:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Query RÁPIDA&lt;/span&gt;
&lt;span class="k"&gt;EXPLAIN&lt;/span&gt; &lt;span class="k"&gt;ANALYZE&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="k"&gt;BETWEEN&lt;/span&gt; &lt;span class="s1"&gt;'2024-01-01'&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="s1"&gt;'2024-12-31'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Index Scan using idx_orders_status_created on orders
  Index Cond: ((status = 'pending') AND ...)
  Planning Time: 0.12 ms
  Execution Time: 0.05 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Index Scan.&lt;/strong&gt; PostgreSQL fue directo a los datos que necesitaba.&lt;/p&gt;

&lt;h2&gt;
  
  
  Los números
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ANTES&lt;/th&gt;
&lt;th&gt;DESPUÉS&lt;/th&gt;
&lt;th&gt;Mejora&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Tipo de Scan&lt;/td&gt;
&lt;td&gt;Seq Scan&lt;/td&gt;
&lt;td&gt;Index Scan&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiempo&lt;/td&gt;
&lt;td&gt;~45ms&lt;/td&gt;
&lt;td&gt;~0.05ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;900x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Filas escaneadas&lt;/td&gt;
&lt;td&gt;10,000&lt;/td&gt;
&lt;td&gt;~50&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200x&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Y eso con 10,000 registros. En producción con millones de filas, la diferencia entre Seq Scan e Index Scan es la diferencia entre un endpoint que responde y uno que hace timeout.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cómo leer un EXPLAIN ANALYZE (sin dormirte)
&lt;/h2&gt;

&lt;p&gt;No necesitás un doctorado. Buscá estas tres cosas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Seq Scan&lt;/strong&gt; = malo. Significa que lee toda la tabla. Si ves esto en una tabla grande, tenés un problema.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Index Scan&lt;/strong&gt; = bueno. Significa que usa el índice y va directo a los datos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rows Removed by Filter&lt;/strong&gt; = desperdicio. Son filas que leyó pero descartó. Cuanto más alto el número, peor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Si tu query tiene Seq Scan y Rows Removed by Filter alto, no necesitás más RAM ni más CPU. Necesitás revisar tu WHERE clause.&lt;/p&gt;

&lt;h2&gt;
  
  
  Las funciones que matan tus índices
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;LOWER()&lt;/code&gt; no es la única culpable. Cualquier función aplicada a una columna en el WHERE invalida el índice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;UPPER(column)&lt;/code&gt; — mismo problema que LOWER&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TRIM(column)&lt;/code&gt; — si necesitás trim, limpiá los datos al insertar&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CAST(column AS ...)&lt;/code&gt; — cuidado con conversiones implícitas&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXTRACT(YEAR FROM column)&lt;/code&gt; — usá rangos de fechas en vez de extraer partes&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;column + 1 = 5&lt;/code&gt; — reescribí como &lt;code&gt;column = 4&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;La regla de oro: &lt;strong&gt;la columna indexada debe aparecer sola en un lado de la comparación.&lt;/strong&gt; Si le envolvés una función, el índice no se puede usar.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cuándo NO optimizar la query
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tablas chicas.&lt;/strong&gt; Si tu tabla tiene 100 filas, un Seq Scan tarda microsegundos. PostgreSQL a veces elige Seq Scan a propósito porque es más rápido que buscar en el índice para tablas pequeñas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Queries que corren una vez al día.&lt;/strong&gt; Un reporte nocturno que tarda 2 segundos no necesita un índice. No optimices lo que no duele.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuando el índice ralentiza las escrituras.&lt;/strong&gt; Cada índice que agregás hace que los INSERT y UPDATE sean más lentos. Si tu tabla tiene escritura intensiva, pensalo dos veces.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cuando el selectividad es baja.&lt;/strong&gt; Si el 80% de tus órdenes son &lt;code&gt;PENDING&lt;/code&gt;, el índice en &lt;code&gt;status&lt;/code&gt; no ayuda mucho — PostgreSQL va a leer casi toda la tabla de todas formas y prefiere Seq Scan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Esto no es solo Java
&lt;/h2&gt;

&lt;p&gt;Si usás Django con PostgreSQL y hacés &lt;code&gt;Order.objects.filter(status__iexact='pending')&lt;/code&gt;, Django genera un &lt;code&gt;UPPER()&lt;/code&gt; en la query. Mismo problema, misma solución: normalizá los datos.&lt;/p&gt;

&lt;p&gt;Si usás Rails con &lt;code&gt;where("LOWER(status) = ?", status.downcase)&lt;/code&gt;, estás invalidando el índice.&lt;/p&gt;

&lt;p&gt;Si usás cualquier ORM con cualquier base de datos relacional, la regla es la misma: &lt;strong&gt;las funciones en el WHERE matan los índices.&lt;/strong&gt; No importa si es PostgreSQL, MySQL, Oracle o SQL Server. El optimizador no puede usar un índice sobre una columna transformada.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt; es la versión PostgreSQL. MySQL tiene &lt;code&gt;EXPLAIN&lt;/code&gt;, SQL Server tiene &lt;code&gt;SET STATISTICS IO ON&lt;/code&gt; y los execution plans gráficos. Cada motor tiene su herramienta. Aprendé a usarla antes de agregar índices a ciegas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Esto es el Día 3
&lt;/h2&gt;

&lt;p&gt;Este artículo es parte de &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt; — una serie de problemas reales de arquitectura con soluciones reales. No teoría. Código que podés correr y medir.&lt;/p&gt;

&lt;p&gt;La próxima vez que una query ande lenta, antes de agregar un índice, corré &lt;code&gt;EXPLAIN ANALYZE&lt;/code&gt;. Puede que el índice ya exista y simplemente no lo estés usando. Diagnosticá primero, optimizá después.&lt;/p&gt;

&lt;p&gt;Seguí la saga completa en &lt;strong&gt;#100ArchitectureDays&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;💻 Todo el código está en &lt;a href="https://github.com/alafourcadev/100-architecture-days" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. Si te está sirviendo, dejame una ⭐ — es gratis y ayuda a que más gente lo encuentre.&lt;/p&gt;

</description>
      <category>java</category>
      <category>springboot</category>
      <category>architecture</category>
      <category>100architecturedays</category>
    </item>
  </channel>
</rss>
