1. De dónde viene el legacy en los repositorios
Si alguna vez has buscado en Google «Spring Data JPA repository example», sabes cómo suele ocurrir: abres un artículo, ves anotaciones familiares, copias un fragmento de código y, en 15 minutos, tienes un método que funciona. El problema es que algunos de esos artículos fueron escritos para Spring Data JPA de hace cinco (o incluso diez) años. El código puede compilarse, pero el estilo y la semántica ya no coinciden con la API moderna, especialmente si tenemos como base Spring Boot 4 + Spring Data JPA 4.x.
Aquí es importante captar una idea: legacy no es un insulto, sino una marca de tiempo. Como una vieja foto con un teléfono de botones: en el momento de la toma era normal, pero hoy parece extraño planificar la arquitectura de una aplicación «como en 2012». En Spring Data esto se nota especialmente, porque cambiaron los nombres de los métodos, se agregaron nuevos tipos de retorno, se aclaró la semántica de los objetos de referencia y aparecieron formas más cuidadosas de describir la lectura de datos.
Para este punto, las derived queries, JPQL, projections y native queries ya son conocidas por sí mismas. El dolor generalmente no comienza por la falta de herramientas, sino por lo contrario: hay muchas, y el repositorio rápidamente se convierte en un vertedero si se mezclan sin lógica. Por eso primero necesitamos no otra sintaxis, sino un radar normal para los smells.
De ahí nace la primera gran causa de los repositorios legacy: el «código de internet» a menudo está escrito para otra versión y para otro nivel de madurez del equipo. Un ejemplo educativo pudo haber simplificado o «mezclado todo en un solo lugar» para mostrar 5 características en 10 minutos. Pero cuando ese ejemplo se convierte en tu repositorio principal en el proyecto, comienza a vivir su propia vida y a expandirse.
La segunda causa es más simple y triste: el repositorio es el lugar donde es conveniente «tirar un método más». Es casi como la carpeta Downloads en el sistema operativo: primero hay un archivo, luego veinte, y luego tienes miedo de entrar. El repositorio se convierte en vertedero de métodos aleatorios no porque el desarrollador sea vago, sino porque el repositorio rara vez tiene un «dueño de API» que vigile la forma y la disciplina.
Ahora es justo lo que necesitamos: un radar así, aprender a reconocer repositorios que formalmente funcionan pero ya arrastran el proyecto al legacy, y separarlos de las API donde la interfaz muestra la intención.
2. Repositorio estilo legacy como API
Un repositorio en Spring Data JPA no es solo una «interfaz que sabe ir a la base de datos». Es un contrato público de tu capa de datos. La capa de servicio (y a veces la capa de consulta) llamará a los métodos del repositorio igual que llamas a los métodos de la biblioteca estándar: esperando que sean predecibles, legibles y que no den sorpresas.
Repositorio estilo legacy es un repositorio que formalmente funciona, pero parece que fue escrito «sobre la marcha» y «según la situación». Por lo general, carece de una lógica unificada en dos cosas: cómo elegir el mecanismo de consultas y cómo elegir los tipos de retorno. Al final, en una misma interfaz coexisten derived queries, JPQL, SQL nativo, paginación, proyecciones e incluso métodos extraños «por si acaso», y por los nombres y firmas es difícil entender cuál es el «camino principal» y cuál es un «experimento de una sola vez».
Un repositorio así tiene un efecto desagradable: no se rompe de inmediato. Se rompe en el momento en que empiezas a desarrollarlo. El equipo abre la interfaz, ve «aquí ya se puede todo», y añade un par de métodos más en el estilo que le resulta más familiar. En una semana el repositorio se convierte en una mezcla de tres sabores diferentes de café (y todos instantáneos), y en un mes nadie recuerda por qué una lista se devuelve como List<Product>, la segunda como Page<Product>, la tercera como List<ProductCatalogRow> y la cuarta como List<Object[]> de algún artículo antiguo. Y lo más molesto: la mayoría de los problemas aquí no son sobre SQL, sino sobre diseño de API.
Para «palpar» el problema, imagina que el repositorio es un menú de cafetería. Un menú moderno se lee rápido: secciones, nombres de platos claros, porciones predecibles. Un menú legacy es una hoja A4 donde entre el borscht y los postres aparece de repente una instrucción «cómo cambiar los limpiaparabrisas del coche», porque «total, ya estamos escribiendo algo aquí». Parece útil, pero un poco inquietante.
A continuación, analizaremos los principales síntomas de un repositorio legacy. Suelen venir en conjunto, como un «menú combinado».
3. Síntoma nº 1: giant repository
Al principio parece incluso conveniente. Tienes ProductRepository, y allí agregas el método «encontrar por estado», «encontrar caros», «leer todo con SQL», «búsqueda por nombre con paginación». Todo en un mismo lugar: belleza. Pero si en la interfaz no hay lógica sobre por qué los métodos son exactamente esos, comienza a parecerse al cajón de la cocina donde hay cucharas, un destornillador, pilas, dos tornillos y una llave de alguien. Cuando necesitas encontrar algo urgente, sacas todo. Con los repositorios ocurre lo mismo.
Veamos un ejemplo típico de «parece normal, pero ya es alarmante»:
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Derived query: se lee simple, pero es solo uno de los «lenguajes» en esta interfaz
List<Product> findByStatus(ProductStatus status);
// JPQL: ya otro mecanismo de consulta (y otro estilo de lectura)
@Query("""
select p
from Product p
where p.price > :min
""")
List<Product> findExpensive(@Param("min") BigDecimal min);
// Native SQL: tercer mecanismo en el mismo repositorio
// Importante: el ejemplo es deliberadamente «malo» en estilo (select *) para mostrar el síntoma
@Query(value = """
select *
from product
""", nativeQuery = true)
List<Product> readEverythingRaw();
// Derived query + pagination: otra variación del contrato (Page + Pageable)
Page<Product> findByNameContainingIgnoreCase(String text, Pageable pageable);
}
Formalmente todo está bien. Pero esto ya es un ejemplo de interfaz donde cuatro métodos usan tres «lenguajes de lectura» diferentes: derived query, JPQL y SQL nativo. La cuestión no es que no se pueda hacer. La cuestión es que dentro de un mes abrirás esta interfaz y te preguntarás: «¿cuál es el estilo principal aquí?». Si la respuesta es «cualquiera que sea más rápido de escribir», ya estás en el camino del giant repository.
¿Cómo se ve normalmente un fragmento más saludable? No tiene que ser perfecto y ciertamente no pretende abarcar toda la interfaz. Su objetivo es más simple: que las firmas muestren la intención y que la interfaz no provoque un crecimiento accidental. El paso mínimo saludable es mantener los métodos en una forma legible y en tipos de retorno que expresen directamente el significado.
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Una clave de negocio -> un resultado (y puede que no exista)
Optional<Product> findBySku(String sku);
// Contrato separado para contar, sin carga innecesaria de entidades
long countByStatus(ProductStatus status);
// Lista + ordenación explícita (el contrato «cómo ordenar» no se oculta)
List<Product> findByStatus(ProductStatus status, Sort sort);
}
Aquí no hay magia. Pero hay disciplina: un método busca exactamente un producto por clave de negocio (y honestamente dice que puede no estar), el segundo método cuenta, el tercero devuelve una lista con ordenación explícita. Esta interfaz plantea menos preguntas y está mejor protegida contra «extensiones accidentales».
Para relacionar esto con nuestro proyecto shop-data-jpa, es útil recordar la decisión arquitectónica: package-by-feature. Esto ya es un antídoto contra el giant repository a nivel de estructura. Tenemos catalog, inventory, ordering, y no son solo carpetas bonitas. Es una pista: si de repente quieres poner en ProductRepository un método «encontrar pedidos por email» — probablemente solo te hayas equivocado de paquete.
A veces el giant repository aparece incluso dentro de una sola funcionalidad, cuando el repositorio intenta ser al mismo tiempo «repositorio CRUD», «centro de informes», «búsqueda de administración» y «lugar para experimentos con SQL nativo». En esta etapa ayuda una regla simple: si el método se lee como «subsistema independiente», es mejor diseñarlo como un modelo de lectura separado (projection/record) y darle un nombre normal, en lugar de ocultar el significado en el caos.
4. Síntoma nº 2: devolver entidades en todas partes
Un hábito muy común de internet se ve así: «el repositorio es sobre entidades, por lo tanto debe devolver entidades». Y luego todo, incluso el catálogo para la administración, incluso la lista para el desplegable de la UI, incluso el resumen del pedido — todo devuelve entidades completas. Parece lógico… hasta que el caso de uso solo pide 2 o 3 campos.
En ese momento, devolver la entidad «por defecto» se convierte no en universalidad, sino en un default perezoso. El repositorio deja de distinguir entre write-model y lectura ligera, y el código llamante obtiene el modelo de persistencia completo donde se necesita un modelo de lectura corto.
No porque la entidad sea mala, sino porque es una forma cómoda de no pensar en la forma del resultado.
Imaginemos dos escenarios en catalog:
1) Necesitamos un producto para cambiar su precio (es decir, realmente trabajamos con el write-model). Entonces la entidad está bien.
2) Necesitamos una lista de productos en el catálogo: nombre + precio. Cargar todo lo demás (estado, relaciones, detalles, etc.) a menudo no tiene sentido. Aquí es más lógico usar una projection.
Aquí está la forma más simple de mostrar un «giro moderno» a nivel de un solo método:
import java.math.BigDecimal;
// Projection: contrato «leemos solo los campos necesarios», no «arrastramos toda la entidad»
public interface ProductCatalogRow {
String getName();
BigDecimal getPrice();
}
Y ahora el método del repositorio que devuelve no una entidad, sino un modelo de lectura:
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {
// Importante: devolvemos no Product, sino un modelo de lectura para el catálogo (lectura parcial)
List<ProductCatalogRow> findByStatus(ProductStatus status);
}
Observa que el método sigue siendo derived — y eso es normal. Pero por la firma se nota que es una «lectura de catálogo», no una carga de toda la entidad para futuros cambios. Esta es una de las señales del estilo moderno: expresas la intención no solo con el nombre, sino también con la forma del resultado.
Si quieres comprobarte rápidamente, puedes hacerte una pregunta casi como en una entrevista, pero a ti mismo: «Si ahora devuelvo la entidad, ¿entiendo para qué necesito todos sus campos y relaciones?». Si la respuesta es «bueno, por si acaso» — es ese momento en que el repositorio comienza a convertirse en un legacy-layer.
5. Síntoma nº 3: method-name monster
Las derived queries son una herramienta excelente. Pero tienen un lado engañoso: dan la sensación de que se puede «agregar un poquito más» al nombre del método. Y luego otro poco. Y luego otro. Y entonces tienes un método que no cabe en la pantalla y cuya lectura parece descifrar un hechizo antiguo.
Aquí hay un «monstruo» típico que aparece a menudo cuando tenemos muchas condiciones:
import java.math.BigDecimal;
import java.util.List;
public interface ProductRepository {
// El nombre del método comienza a codificar toda la consulta, y la firma deja de leerse como una API
List<Product> findByStatusAndCategoryIdAndPriceBetweenAndNameContainingIgnoreCaseOrderById(
ProductStatus status, // filtro por estado
Long categoryId, // filtro por categoría
BigDecimal min, // límite inferior de precio
BigDecimal max, // límite superior de precio
String text // búsqueda por nombre
);
}
Técnicamente Spring Data lo procesará. Pero como API, ya es un mal contrato. Viola la regla principal de legibilidad: la firma deja de ser una «descripción rápida de la intención» y se convierte en un «lenguaje de consultas incrustado directamente en los nombres de Java». Ese código es difícil de discutir en equipo y difícil de modificar. Cualquier nueva condición convierte el nombre en una construcción aún más larga, y el método comienza a parecer un chatbot que no puede parar.
Una de las señales del estilo moderno es saber parar. Si el nombre deja de leerse, es mejor pasar honestamente a una consulta explícita. Aunque escribas un poco más, ganas en claridad.
import java.math.BigDecimal;
import java.util.List;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
public interface ProductRepository {
// Consulta explícita en lugar de una «novela» en el nombre del método:
// el nombre expresa el intent, y los detalles del filtro viven en JPQL
@Query("""
select p
from Product p
where p.status = :status
and p.category.id = :categoryId
and p.price between :min and :max
and lower(p.name) like lower(concat('%', :text, '%'))
order by p.id
""")
List<Product> findCatalogChunk(@Param("status") ProductStatus status,
@Param("categoryId") Long categoryId,
@Param("min") BigDecimal min,
@Param("max") BigDecimal max,
@Param("text") String text);
}
El sentido aquí no es que JPQL sea «mejor». El sentido es que la API vuelve a ser humana. El nombre findCatalogChunk es claro: «leer un trozo del catálogo». Y los detalles del filtro viven donde deben estar — en el texto de la consulta.
Y observa: todavía no estamos eligiendo una «regla universal para seleccionar la herramienta» — eso será la siguiente parte del día. Ahora solo estamos fijando el síntoma: si el nombre derived se ha convertido en un monstruo, muy a menudo es una señal de estilo legacy que crece sin control.
6. Síntoma nº 4: fantasmas de la API antigua
Una de las formas más simples de distinguir el código moderno del internet-legacy es mirar los nombres de los métodos. En Spring Data JPA hay métodos que existían en versiones antiguas, luego fueron deprecados y luego reemplazados por otros más claros.
Puedes encontrar en artículos (y, peor aún, en proyectos ajenos) llamadas como estas:
// En ejemplos antiguos solían hacerlo así (y es una señal de «código de otra época»)
Product p = productRepository.getOne(productId); // legacy-style (versiones antiguas)
¿Por qué es una señal de alarma? Porque normalmente significa no solo «el método está obsoleto», sino también «el autor del ejemplo vive en otro modelo mental del repositorio». A menudo, junto con estas llamadas vienen otros hábitos: devolver entidades en todas partes, native query «porque es más fácil» y un repositorio de 200 métodos.
El estilo moderno intenta nombrar las cosas de manera que el nombre muestre la intención. Por eso, en lugar del histórico getOne, en la API actual se ha consolidado el más honesto getReferenceById (más adelante analizaremos la semántica en detalle según el plan del día). El sentido ahora nos importa solo uno: no arrastrar nombres viejos al código nuevo sin entenderlos.
Es útil tener en mente una tabla simple de «qué encontrarás en vídeos antiguos y cómo se escribe ahora» (no como dogma, sino como referencia):
| En ejemplos antiguos | En estilo moderno | Por qué es importante |
|---|---|---|
| getOne(id) | getReferenceById(id) | Más claro que es precisamente «reference» |
| getById(id) | getReferenceById(id) | Elimina la ambigüedad de «¿cargamos o dimos una referencia?» |
| métodos con get… sin contexto | find…, , |
El nombre debe expresar el contrato |
Una vez más: esto no es una «religión de nombres». Es una forma de hacer que el repositorio sea predecible y esté protegido contra copypaste.
7. Síntoma nº 5: native query por defecto
Native query es una herramienta útil. En el curso le hemos dedicado un lugar especial: consultas de informes, cosas específicas del vendor, a veces selecciones complejas que son incómodas de expresar con JPQL. Pero precisamente por eso, native query debe ser como un cuchillo afilado en la cocina: se puede y se debe usar, pero no intentes untar mantequilla en el pan con él.
El patrón legacy se ve así: «No conozco / no me gusta JPQL, por eso hago todo con SQL». Y entonces aparecen en el repositorio métodos como:
import java.util.List;
import org.springframework.data.jpa.repository.Query;
public interface ProductRepository {
// Native SQL «por defecto»: ejemplo de síntoma, no una recomendación
@Query(value = """
select * from product
""", nativeQuery = true)
List<Product> readEverythingRaw(); // «total, funciona»
}
Esto es peligroso no solo porque select * es un mal hábito. Es peligroso porque de repente vinculas el repositorio al esquema físico: a los nombres de tablas, columnas, dialecto. Puede estar justificado… pero entonces debes entender para qué lo haces, y mantener el scope estrecho. De lo contrario, conviertes Spring Data JPA en un «almacén de cadenas SQL» y la capa JPA se vuelve solo una decoración.
El estilo moderno en nuestro curso suena pragmático: native query es una excepción, no el default. Si la consulta se puede expresar con derived query, JPQL o projection, elegimos el nivel más alto. Si la consulta es realmente de informe o específica, ok, native. Pero entonces la escribimos con disciplina: nombre de método claro, resultado explícito (a menudo projection), SQL cuidado, no «leer todo lo que haya».
8. Señales del estilo moderno de repositorio
Después de todos los «síntomas», es fácil caer en el extremo opuesto: «ahora todo hay que escribirlo solo con una herramienta correcta». Eso es igualmente una trampa. Un repositorio saludable no se sostiene en una API favorita, sino en tres preguntas simples.
Primero, qué hace exactamente el método. El nombre debe expresar la intención, no obligar al lector a adivinar para qué existe ese método.
Segundo, qué forma de resultado espera el caso de uso. Un objeto, una lista, un contador, un modelo de lectura ligero — son contratos diferentes. Si el tipo de retorno se elige «por si acaso», la interfaz se vuelve rápidamente impredecible.
Tercero, cuál es el mecanismo más simple que expresa honestamente esa consulta. Derived query es buena mientras el nombre se lea. JPQL es buena cuando la condición ya es más fácil escribirla explícitamente. Projection es buena cuando no se necesita el objeto completo. Native query es buena donde es realmente un escenario SQL-first, no un hábito. Si estas tres respuestas no se leen en la interfaz, el giant repository es casi seguro.
Una vez que los smells son visibles, entonces ya se puede elegir la herramienta según el caso de uso, no por costumbre o por el recuerdo de un artículo aleatorio de internet.
9. Errores típicos al trabajar con repositorios
Error nº 1: copiar código de un artículo antiguo sin verificar la versión y la vigencia de la API.
Esto ocurre casi automáticamente: ves getOne(), getById(), algunos métodos extraños y piensas que «si el autor lo escribió, es correcto». En la base moderna (Spring Boot 4 / Spring Data JPA 4) esos ejemplos o no compilan, o te llevan a una semántica obsoleta. El hábito correcto es al menos echar un vistazo rápido a la API actual y recordar que en internet hay muchos ejemplos de «otra época».
Error nº 2: meter todo en una misma interfaz «porque es más rápido».
Al principio acelera el desarrollo, luego se convierte en deuda: abres el repositorio y no entiendes qué es el contrato básico y qué son deseos aleatorios de diferentes tareas. El repositorio comienza a crecer como una bola de nieve, y cualquier mejora requiere «cinco minutos para encontrar dónde está qué». El estilo moderno no prohíbe diferentes mecanismos de consulta, pero exige significado y estructura.
Error nº 3: devolver entity de cualquier lectura por costumbre.
La entidad es excelente cuando realmente trabajas con el modelo de dominio dentro de la capa de datos. Pero tan pronto como el caso de uso pide un modelo de lectura corto (catálogo, resumen, informe), devolver entidades en todas partes empieza a parecer un default perezoso. Además, empuja a que los servicios comiencen a depender de campos y relaciones superfluas. Las projections existen precisamente para que el repositorio pueda ser preciso en la forma del resultado.
Error nº 4: escribir nombres derived «porque se puede», incluso cuando ya no se leen.
Las derived queries son buenas mientras el nombre del método es una frase legible. Cuando el nombre se convierte en una novela, la API deja de ser API y se convierte en «texto de consulta codificado en CamelCase». En ese momento es mejor pasar honestamente a JPQL (@Query) o a una herramienta más adecuada, que seguir alimentando al monstruo.
Error nº 5: hacer de native query la primera opción, no una excepción puntual.
El SQL nativo a veces es necesario. Pero cuando el repositorio se convierte en un conjunto de líneas select * from ..., pierdes las ventajas de la capa JPA, empeoras la legibilidad y aumentas el acoplamiento con el esquema. El estilo moderno no es una prohibición del native, sino disciplina: usarlo donde realmente está justificado, y no sustituir todo lo demás con él.
GO TO FULL VERSION