1. El precio de una dependencia
Cuando solo empiezas, una línea en build.gradle.kts parece inocente: bueno, qué tiene de malo añadir una biblioteca — y ya está. Pero en un proyecto Boot la dependencia casi nunca llega sola. Arrastra dependencias transitivas, puede desplazar las versiones de las bibliotecas comunes, y a veces cambia el comportamiento de la aplicación simplemente porque en el classpath apareció un nuevo conjunto de clases. Y eso no es un «mal Spring», sino la mecánica habitual del mundo Java: el classpath es un caldero común, y si le añadiste un nuevo ingrediente, el sabor cambia para toda la sopa, no solo para una cucharada.
Aquí es importante separar dos tipos de cambios. El primero — simplemente ampliaste las capacidades del proyecto: añadiste algo encima, sin romper el conjunto base de versiones, porque Spring Boot las gestiona a través de su BOM. El segundo — sin darte cuenta empezaste a gestionar versiones a mano o añadiste una biblioteca que entra en conflicto con lo que Boot ya trajo. En el segundo caso el proyecto empieza a comportarse como un coche tras un «tuneo en el garaje a partir de un vídeo de Internet»: parece que va, pero el sonido es extraño, las luces se encienden y en las curvas quieres rezar.
Y otro pensamiento importante: los starters son una abstracción conveniente de nivel «escenario», pero al final se despliegan en un conjunto concreto de módulos. Por eso «añadir el starter de forma segura» significa no simplemente escribir la línea, sino hacerlo de tal forma que no se rompa el modelo gestionado, no se dupliquen las partes ya existentes del grafo y no se arrastren al proyecto versiones manuales aleatorias. Ahora reuniremos un algoritmo claro que permite añadir dependencias con tranquilidad — sin magia, sin pánico y sin el ritual «bueno, a ver qué pasa...».
2. Adición segura de dependencias
Si te llevas de esta lección solo una cosa, que sea el orden de acciones, no el «comando de Gradle más bonito». El error del principiante normalmente no es que no sepa el exclude. El error es que empieza a «tratar los síntomas» antes de mirar las causas: añade versiones, excluye módulos, cambia starters — y todo eso antes de ver el panorama real del grafo de dependencias. Por eso el algoritmo correcto empieza no por el código y ni siquiera por Gradle, sino por formular qué exactamente necesita el proyecto.
Imagina que necesitas «añadir la capacidad X». No importa qué X: diagnóstico, validación, ciclo rápido de desarrollo u otra cosa. Primero formulas el escenario, luego buscas la dependencia de nivel Boot correcta, normalmente un starter, la añades sin versión, la pones en la configuración Gradle correcta (sobre ellas — en la siguiente sección) y de inmediato verificas dos cosas: cómo cambió el árbol de dependencias y si compila el proyecto. Si algo fue mal, no «añades otra versión», sino que vuelves y averiguas quién está en conflicto con quién.
Conviene tener en mente un «camino» simple como esquema:
flowchart TD
A[Se necesita nuevo comportamiento] --> B["¿Hay un starter para el escenario?"]
B -->|Sí| C[Añadimos el starter sin versión]
B -->|No| D[Añadimos la biblioteca y verificamos: ¿está gestionada?]
C --> E["Miramos el dependency tree / dependencyInsight"]
D --> E
E --> F["Verificamos el build: test/bootRun"]
F --> G{"¿Todo está bien?"}
G -->|Sí| H[Fijamos el cambio]
G -->|No| I[Revertimos y analizamos el conflicto por los hechos]
Para que ese esquema no siga siendo un ritual, ten la regla práctica de nivel junior para ese tipo de proyectos Boot:
Los starters oficiales de Boot y los artefactos de Boot como spring-boot-devtools o spring-boot-configuration-processor en el marco del curso por defecto los conectamos sin versión.
Si la biblioteca es externa y no parece parte del conjunto gestionado por Boot, empezamos con una versión explícita.
Si la dependencia sin versión Gradle no puede resolver normalmente, esto es una señal práctica: el modelo gestionado no te proporciona esa versión.
Después de añadir de todos modos verificamos por los hechos runtimeClasspath o dependencyInsight, y no confiamos en la palabra de una sola línea en build.gradle.kts.
Fíjate: aquí no hay el paso «en la desesperación ponemos versiones a mano». La versión es una herramienta, pero debe ser la última, no la primera. Y sí: «verificamos el build» no es «algún día después». Las dependencias son el fundamento. Si el fundamento se agrietó, el segundo piso no se construye.
En el contexto de catalog-service ese algoritmo es especialmente importante, porque ensamblamos una plantilla de servicio Boot. Y la plantilla es valiosa porque puede ampliarse después. Si en la etapa temprana aprendemos a añadir dependencias con cuidado, más tarde será mucho más fácil: nuevas capacidades empezarán a aparecer de forma controlada, y no como un conjunto aleatorio de bibliotecas que algún día se pelearon entre sí.
3. Configuraciones de dependencias en Gradle
El error más frecuente del principiante — pensar que todas las dependencias son iguales. Mola, «si la biblioteca se necesita, la escribes en implementation y adelante». Pero Gradle, y junto con él Boot, específicamente nos da configuraciones diferentes: qué se necesita en el runtime de la aplicación, qué solo para los tests, qué solo durante el desarrollo, y qué generalmente se necesita solo al compilador como «generador» de código o metadatos. No es burocracia, sino una forma de mantener el proyecto limpio y predecible.
A continuación hay un pequeño orientador. No como «hay que aprendérselo de memoria», sino como útil para entender por qué la misma biblioteca en lugares diferentes significa cosas diferentes:
| Configuración Gradle | Dónde se usa | Qué significa en palabras sencillas | Ejemplo del mundo Boot |
|---|---|---|---|
| implementation | compilación + lanzamiento de la aplicación | «Es parte de la aplicación» | spring-boot-starter-actuator, spring-boot-starter-webmvc |
| testImplementation | compilación + lanzamiento de tests | «Se necesita solo para los tests» | spring-boot-starter-test |
| runtimeOnly | solo lanzamiento | «En compilación no se necesita, pero en runtime sí» | se encuentra menos frecuentemente al principio del curso |
| developmentOnly | desarrollo local (IDE/bootRun) | «Ayudante para el desarrollo, no lo arrastramos a producción» | spring-boot-devtools |
| annotationProcessor | compilación (generación de código/metadatos) | «No es una biblioteca de la aplicación, sino una herramienta del compilador» | spring-boot-configuration-processor |
Si lo digo muy en términos burdos, entonces implementation — es «lo que llevamos en la mochila de excursión», testImplementation — «esto está en casa en el entrenamiento», developmentOnly — «estos son los patines: ayudan a aprender, pero no los llevamos a la carrera», y annotationProcessor — «esto no es comida, sino el cocinero que ayuda a prepararla».
Para nuestro proyecto catalog-service es importante que las dependencias estén «donde corresponde» y no se extiendan. Por ejemplo, spring-boot-devtools — una cosa útil, pero no debe por defecto acabar en el artefacto final. Y spring-boot-configuration-processor en absoluto no debe percibirse como dependencia de runtime — lo necesita el compilador/IDE para generar metadatos, no para el trabajo de la aplicación.
Precisamente por eso «añadir el starter de forma segura» es también «ponerlo en la configuración correcta». De lo contrario puedes obtener un proyecto que parece arranca, pero su artefacto creció de forma inesperada o, al contrario, los tests caen por falta de dependencias que pusiste por error en el lugar equivocado.
4. Añadimos spring-boot-starter-actuator
Ahora al máximo de forma práctica: qué aspecto tiene la adición segura del starter si queremos ampliar el proyecto con otra capacidad de plataforma y al mismo tiempo no meternos en la gestión manual de versiones. Aquí lo importante no son los propios endpoints del Actuator, sino la habilidad: añadir la dependencia, verificar el árbol, asegurarse de que las versiones las gestiona Boot, y que el proyecto después de eso vive.
Tomamos el baseline actual del día — spring-boot-starter-webmvc más spring-boot-starter-test — y miramos por separado qué cambia si sobre él añadimos un starter oficial para diagnóstico. Es un cambio aislado del modelo de dependencias: no declaramos el Actuator como nuevo estado obligatorio del proyecto, sino que miramos cómo hacer crecer el build en una línea.
dependencies {
// No indicamos versión: Spring Boot gestiona las versiones de los starters a través de su BOM
implementation("org.springframework.boot:spring-boot-starter-actuator")
}
Fíjate: todavía no escribimos la versión. No es «pereza», sino el estilo consciente de Boot. La versión del starter y de todas sus bibliotecas internas está alineada a través de la gestión de dependencias de Spring Boot.
Ahora dos formas rápidas de verificar que todo se conectó como esperamos. La primera — mirar el árbol de dependencias exactamente para el classpath de runtime: así es más sencillo que leer todo el informe seguido.
# Miramos el árbol de dependencias del runtimeClasspath (lo que realmente entrará al runtime)
./gradlew dependencies --configuration runtimeClasspath
# ... en la salida buscamos spring-boot-starter-actuator y su rama
La segunda — verificar de forma puntual una biblioteca concreta que llegó de forma transitiva, y ver por qué se eligió exactamente esa versión. Por ejemplo, si del plan del curso sabemos que Boot trae Micrometer, podemos hacer así:
# Averiguamos de forma puntual: quién trajo exactamente micrometer-core y por qué se eligió esa versión
./gradlew dependencyInsight --dependency micrometer-core --configuration runtimeClasspath
Aquí hay un momento psicológico importante. El principiante a menudo espera que tras añadir el starter vea «una biblioteca». Y verá un trozo entero del grafo. Es normal. El starter es un curated dependency set: trae un conjunto de componentes que normalmente se necesitan juntos. El árbol puede crecer notablemente — y eso no es señal de que hiciste algo malo. La mala señal — cuando el árbol creció y no puedes explicar por qué: «bueno, se hizo así de alguna forma».
Tras verificar el árbol hacemos la última cosa que nos convierte de «editor de build.gradle.kts» en ingeniero: nos aseguramos de que el proyecto compila. En la etapa temprana no hace falta complicar — basta con:
./gradlew test # BUILD SUCCESSFUL
Si el build pasó, significa que el modelo de dependencias está concordado. Y lo más importante: añadiste una nueva capacidad sin romper el baseline y sin convertir el build en una tabla de números de versión manuales.
5. Versiones manuales como medida extrema
Ahora sobre el escenario más popular de rotura: «añadí la versión porque la vi en Internet». Internet, claro, es maravilloso, pero tiene una peculiaridad: no sabe que tienes Spring Boot 4, no Spring Boot 2. No sabe que tienes Jackson 3, no Jackson 2. Y en general le da igual qué tienes debajo. Por eso la receta «inserta exactamente esa dependencia con exactamente esa versión» en un proyecto Boot a menudo actúa como un servicio mediocre: parece que ayudó, pero el umbral de la puerta ahora es del tamaño de ese oso.
El ejemplo malo clásico se ve aproximadamente así: el desarrollador quiere «configurar el JSON» y decide añadir jackson-databind a mano, y además con la versión de un artículo antiguo:
dependencies {
// El starter lo conectamos sin versión (eso está bien)
implementation("org.springframework.boot:spring-boot-starter-webmvc")
// Y aquí empieza la «segunda verdad»: versión manual, y además potencialmente no de la línea gestionada de Boot
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.1")
}
El problema aquí no es en la propia anotación de la versión. El problema es que es una línea diferente de Jackson. En el baseline de Boot 4 — Jackson 3, y puede ser con otro groupId y con otras expectativas de compatibilidad de módulos. El resultado puede ser distinto: desde «compiló, pero cayó al arrancar con NoSuchMethodError» hasta «todo funciona hasta que tocas cierta funcionalidad, y luego cae». Y este es el tipo de error más desagradable — «flotante».
Cómo pensar correctamente aquí. Si necesitas JSON en Boot, entonces casi siempre la respuesta correcta es no añadir jackson-databind por separado, sino añadir o dejar el starter que trae el soporte JSON de forma concordada. En nuestro curso eso se hace a través del starter web, pero incluso sin él el principio es el mismo: no conectes la biblioteca de bajo nivel si tienes un starter que ya resuelve el escenario completamente.
Si ya añadiste manualmente la «equivocada», la corrección normalmente es aburrida: eliminar la dependencia manual, volver al modelo gestionado y de nuevo mirar el árbol:
dependencies {
// Dejamos el starter: él tirará el conjunto concordado de dependencias, incluido JSON
implementation("org.springframework.boot:spring-boot-starter-webmvc")
// jackson-databind a mano no añadimos
}
Y después de nuevo ./gradlew dependencies --configuration runtimeClasspath y verificación de qué versión de Jackson llegó realmente del conjunto gestionado de Boot.
Y aquí aparece la importante regla para el nivel junior que realmente salva los proyectos: si la biblioteca ya llega del starter, añadirla por separado — casi siempre un error. A veces hay excepciones, por ejemplo ajuste fino o sustitución de la implementación, pero esas excepciones deben ser explicables y verificables, no «por si acaso».
6. Dependencias opcionales y producción
Con el concepto de «dependencia opcional» en el principiante normalmente se produce una pequeña confusión. Parece como si opcional significara algo del tipo «puede ponerse, puede no ponerse, el mundo no se hundirá». Y eso es verdad, pero lo más importante es otra cosa: la dependencia opcional normalmente se necesita no en todas las capas y no en todos los modos de vida de la aplicación. En un proyecto Boot es especialmente importante distinguir las dependencias que deben entrar en el artefacto final, de las que se necesitan solo para que te sea más cómodo vivir en el IDE.
Tomamos dos ejemplos típicos del mundo Boot: DevTools y el procesador de configuración. El primero ayuda en el desarrollo, el segundo ayuda al IDE y al build a generar metadatos para la configuración. Ambos son útiles, pero ambos no deben percibirse como «parte de la aplicación en runtime».
En Gradle eso se expresa no con palabras «opcional» (como en Maven), sino por dónde exactamente los ponemos.
DevTools — un buen ejemplo de dependencia que se necesita solo en el desarrollo, y en el build «de combate» debe estar ausente:
dependencies {
// Se necesita solo para el desarrollo local (bootRun/IDE), al artefacto de producción no debe llegar
developmentOnly("org.springframework.boot:spring-boot-devtools")
}
El procesador de configuración — ejemplo de dependencia que necesita el compilador como herramienta:
dependencies {
// Lo necesita el compilador/IDE para generar metadatos, no es dependencia de runtime de la aplicación
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
}
Aquí puede notarse una cosa interesante: ambas líneas no usan versión. Y eso de nuevo es el estilo normal de Boot: esas dependencias también están gestionadas por el dependency management de Boot.
¿Por qué eso es importante para catalog-service ahora mismo, aunque todavía no sientas la utilidad de esas herramientas? Porque ensamblamos no simplemente un proyecto, sino una plantilla con los buenos hábitos: las dependencias deben conectarse según el sentido. Cuando luego ensamblas bootJar, lanzas java -jar y transfieres la plantilla al siguiente proyecto, no tendrás que cortarle «colas innecesarias». Simplemente no aparecerán allí desde el principio.
Otro detalle práctico que salva los nervios: si la dependencia no debe ser «parte de producción», no intentes resolverlo con la disciplina del tipo «bueno, simplemente no lo lanzaremos así». Es mejor resolverlo con la estructura de las configuraciones Gradle. Es como el cinturón de seguridad: no se pone porque planees accidentes, sino porque eres una persona.
7. exclude y sustitución de starters
Cuando el estudiante se entera por primera vez de exclude, mucho quiere ir y «limpiar el árbol de dependencias». ¡Es tan largo! ¡Seguro que hay mucho superfluo! Y aquí lo principal es detenerse a tiempo. Un árbol largo de dependencias no es una enfermedad. La enfermedad — los conflictos de versiones, la duplicación por sentido y las sobrescrituras manuales incontroladas. exclude es una herramienta de intervención puntual, cuando sabes exactamente qué estás excluyendo y para qué. Si no puedes explicar la exclusión en una frase, mejor no hacerla.
El escenario más comprensible de sustitución a nivel de starters — cuando sustituyes una «implementación del escenario» por otra. Por ejemplo, decides usar el starter alternativo de logging. En el curso no hacemos eso como paso obligatorio — nos mantenemos en los defaults de Boot — pero como ejemplo de sustitución controlada es perfecto:
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webmvc") {
// Quitamos el starter de logging por defecto para reemplazarlo por el alternativo
exclude(group = "org.springframework.boot", module = "spring-boot-starter-logging")
}
// Conectamos el starter alternativo de logging (sin versiones manuales)
implementation("org.springframework.boot:spring-boot-starter-log4j2")
}
Fíjate en los «buenos modales» de ese trozo de configuración. Aquí no hay versiones manuales. Aquí es visible explícitamente lo que hacemos: excluimos el módulo concreto y añadimos el starter alternativo. Es controlable, explicable y verificable a través del dependency tree. La variante mala tendría otro aspecto: «añadiré artefactos de log4j2 a mano, excluiré algo, y si no funciona — añadiré un par de exclusiones más». Así nacen los proyectos donde el build.gradle.kts recuerda a un conjuro de fantasía: parece poderoso, pero nadie sabe qué pasará al pronunciarlo.
Tras cualquier exclude, especialmente relacionado con los starters, volvemos obligatoriamente a los hechos: de nuevo miramos ./gradlew dependencies --configuration runtimeClasspath y nos aseguramos de que la sustitución ocurrió exactamente como esperamos. Y de nuevo lanzamos el build. exclude es una operación, no cosmética. Tras la operación el paciente debe respirar.
Si mantienes esa disciplina, incluso los cambios complejos del modelo de dependencias dejan de asustar. Se vuelven paso a paso: un cambio — un hecho en el árbol — un lanzamiento del build.
8. Errores típicos al trabajar con starters
Error n.º 1: añadir el starter con versión manual «para mayor seguridad».
Eso parece lógico solo hasta el primer conflicto. En un proyecto Boot la versión del starter casi siempre debe venir del modelo gestionado. La versión manual en la línea de dependencia hace el build menos predecible, porque creas una segunda fuente de verdad: Boot dice una cosa, tú — otra. En la próxima actualización de Boot eso casi garantizadamente empezará a divergir.
Error n.º 2: conectar la biblioteca de bajo nivel junto al starter que ya la trae de forma transitiva.
Así aparece la «doble verdad». Hoy Gradle eligió una versión, mañana — otra, y obtienes errores de runtime inesperados. Si necesitas el escenario, busca el starter. Si el starter ya existe, no dupliques sus interioridades a mano hasta que entiendas para qué lo haces y cómo verificarás el resultado.
Error n.º 3: poner todo en implementation porque «así es más sencillo».
Eso convierte rápidamente el proyecto en uno pesado, ruidoso y poco portátil. DevTools en implementation — ejemplo clásico: localmente es cómodo, pero luego es raro que el artefacto sea diferente y se comporte de otra manera. El procesador de configuración en implementation también carece de sentido: lo necesita el compilador, no la aplicación. La configuración correcta de la dependencia es parte de la arquitectura del build.
Error n.º 4: usar exclude como forma de hacer el árbol «bonito».
Un árbol bonito no es el objetivo. El objetivo — un classpath compatible y predecible. Si excluyes dependencias solo porque «hay demasiadas», probablemente cortarás algo necesario, y luego empezarás a arreglar los síntomas con todavía más muletas. exclude debe ser explicable como sustitución o eliminación de un conflicto concreto.
Error n.º 5: cambiar varias cosas en el modelo de dependencias en un paso y luego no entender qué exactamente rompió el build.
Añadiste el starter, simultáneamente cambiaste la versión de alguna biblioteca, y además añadiste un par de exclude — y ahora tienes un proyecto que no compila, y el culpable es «Spring». En la práctica el culpable es el proceso de cambios. Haz un cambio — verifica el árbol — verifica el build. Es aburrido, pero aburrido significa control.
GO TO FULL VERSION