Spring Initializr te ha entregado una carpeta con ficheros que probablemente no has abierto todavía. Entender qué hace cada uno no es un trámite: la mayoría de los problemas desconcertantes de los primeros días —un controlador que devuelve 404, una propiedad que no se lee, una dependencia que no aparece— se explican por la estructura del proyecto y por el lugar donde has puesto una clase. En esta lección recorreremos el árbol de directorios, leeremos el pom.xml línea a línea, veremos qué es realmente un starter, entenderemos por qué el paquete raíz es crítico y decidiremos la organización de paquetes definitiva de CicloUrbana.

Contenido

  1. El árbol de directorios generado
  2. src/main/java y src/main/resources
  3. src/test/java, target/ y el wrapper de Maven
  4. Anatomía del pom.xml, línea a línea
  5. Qué es un starter y cuáles usaremos en el curso
  6. El paquete raíz y el escaneo de componentes
  7. Organizar el código: por capas o por funcionalidad
  8. La estructura de paquetes de CicloUrbana
  9. El ciclo de vida de Maven
  10. Errores Comunes y Consejos
  11. Ejercicios

  1. El árbol de directorios generado

Este es el proyecto tal y como está tras la lección anterior:

ciclourbana/
├── .mvn/
│   └── wrapper/
│       └── maven-wrapper.properties
├── mvnw                       ← wrapper para Linux y macOS
├── mvnw.cmd                   ← wrapper para Windows
├── pom.xml                    ← definición del proyecto Maven
├── .gitignore
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/ciclourbana/
│   │   │       ├── CicloUrbanaApplication.java
│   │   │       └── estaciones/
│   │   │           ├── Estacion.java
│   │   │           └── EstacionController.java
│   │   └── resources/
│   │       ├── application.properties
│   │       ├── static/
│   │       └── templates/
│   └── test/
│       └── java/
│           └── com/ciclourbana/
│               └── CicloUrbanaApplicationTests.java
└── target/                    ← generado por Maven, no se versiona

Esta disposición no la inventa Spring Boot: es el layout estándar de Maven, respetado por todo el ecosistema Java. La consecuencia práctica es que cualquier desarrollador Java sabe orientarse en tu proyecto sin explicaciones.

La separación fundamental es entre main (lo que se empaqueta y se despliega) y test (lo que se ejecuta al construir pero nunca llega al JAR).

  1. src/main/java y src/main/resources

src/main/java

Contiene todo el código fuente de producción. La estructura de carpetas debe reflejar exactamente la estructura de paquetes: la clase com.ciclourbana.estaciones.EstacionController tiene que estar en src/main/java/com/ciclourbana/estaciones/EstacionController.java. No es una convención opcional; el compilador de Java lo exige.

src/main/resources

Contiene los ficheros no compilables que deben acabar dentro del JAR. Maven los copia tal cual a target/classes, lo que significa que en tiempo de ejecución están en la raíz del classpath.

Carpeta o fichero Qué contiene Módulo del curso
application.properties Configuración de la aplicación 02-04, 02-05
application-dev.properties Configuración específica de un perfil 07-02
static/ Recursos servidos tal cual: HTML, CSS, JS, imágenes —
templates/ Plantillas de servidor (Thymeleaf) —
banner.txt Banner ASCII de arranque 01-05
db/migration/ Scripts SQL de Flyway 04-08

Dos detalles importantes:

  • static/: cualquier fichero que pongas aquí se sirve automáticamente desde la raíz. Un static/logo.png es accesible en http://localhost:8080/logo.png. Esto lo hace una autoconfiguración de Spring Web.
  • templates/: solo tiene sentido si añades un motor de plantillas. CicloUrbana es una API REST pura, así que esta carpeta quedará vacía. Puedes borrarla sin consecuencias.

application.properties

Nace vacío. Vamos a darle contenido útil desde ya:

# src/main/resources/application.properties

# Nombre de la aplicación: aparece en los logs y en Actuator
spring.application.name=ciclourbana

# Puerto del servidor embebido (8080 es el valor por defecto)
server.port=8080

# Nivel de log del propio proyecto: DEBUG durante el desarrollo
logging.level.com.ciclourbana=DEBUG

Cada línea es un par clave=valor. Spring Boot define cientos de claves con valores por defecto sensatos; aquí solo declaras aquello en lo que quieres apartarte de esos valores. La lección 02-05 profundiza en las propiedades, incluida la alternativa YAML.

  1. src/test/java, target/ y el wrapper de Maven

src/test/java

Contiene las pruebas. Initializr genera una:

package com.ciclourbana;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class CicloUrbanaApplicationTests {

    @Test
    void contextLoads() {
    }
}

Aunque el cuerpo esté vacío, esta prueba no es inútil: @SpringBootTest arranca el contexto completo de Spring. Si una dependencia falta, un bean no se puede construir o una propiedad obligatoria no existe, la prueba falla. Es una comprobación de humo muy barata que detecta errores de configuración antes de desplegar. El módulo 6 se dedica por entero a las pruebas.

Las clases de test no se incluyen en el JAR final.

target/

Es la carpeta de salida de Maven. Se regenera entera con cada construcción, por eso está en el .gitignore y nunca debe versionarse.

target/
├── classes/               ← tus .class + los recursos copiados
├── test-classes/          ← pruebas compiladas
├── ciclourbana-0.0.1-SNAPSHOT.jar          ← el fat jar
├── ciclourbana-0.0.1-SNAPSHOT.jar.original ← el JAR "normal"
└── surefire-reports/      ← informes de ejecución de pruebas

Cuando algo se comporta de forma inexplicable, ./mvnw clean borra target y elimina los restos de compilaciones anteriores. Es el primer remedio a probar.

mvnw, mvnw.cmd y .mvn/wrapper

Ya los conoces de la lección 01-02. El contenido relevante:

# .mvn/wrapper/maven-wrapper.properties
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar

Estos tres ficheros sí se versionan en Git: son los que garantizan que cualquiera construya el proyecto con la misma versión de Maven.

  1. Anatomía del pom.xml, línea a línea

El pom.xml (Project Object Model) es el corazón del proyecto. Vamos a leerlo entero.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <!-- (1) HERENCIA: de dónde vienen las versiones y la configuración base -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/> <!-- buscar el parent en el repositorio, no en disco -->
    </parent>

    <!-- (2) IDENTIDAD del proyecto: las "coordenadas" Maven -->
    <groupId>com.ciclourbana</groupId>
    <artifactId>ciclourbana</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>ciclourbana</name>
    <description>Gestión de la red de bicicletas eléctricas de Ribalta</description>

    <!-- (3) PROPIEDADES: variables reutilizables -->
    <properties>
        <java.version>21</java.version>
    </properties>

    <!-- (4) DEPENDENCIAS: qué librerías necesita el proyecto -->
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <!-- (5) CONSTRUCCIÓN: plugins que participan en el empaquetado -->
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

(1) El parent spring-boot-starter-parent

Es, con diferencia, el bloque más importante y el peor entendido. Aporta cuatro cosas:

  1. Gestión de versiones (dependencyManagement). Declara la versión adecuada de más de 250 librerías —Spring, Jackson, Hibernate, Tomcat, JUnit, Mockito, Log4j...— probadas y validadas para funcionar juntas en esa versión de Boot. Por eso tus dependencias no llevan <version>: la hereda de aquí. Esto elimina de un plumazo los conflictos de versiones que atormentaban a los proyectos Java.
  2. Configuración de plugins. Deja preconfigurados el compilador, el plugin de recursos, Surefire (pruebas) y el spring-boot-maven-plugin.
  3. Valores por defecto sensatos. Codificación UTF-8 en fuentes y recursos, y la versión de Java tomada de la propiedad java.version.
  4. Filtrado de recursos. Permite usar @propiedad@ dentro de application.properties para inyectar valores del pom.xml.

Puedes ver la lista completa de versiones gestionadas:

./mvnw help:effective-pom | less

Ese comando muestra el POM "efectivo": el tuyo fusionado con todo lo que hereda del parent. La primera vez impresiona ver cuánto trabajo te están ahorrando esas cinco líneas.

Alternativa sin parent: si tu organización ya usa su propio POM padre corporativo, puedes importar solo la gestión de versiones mediante el BOM (Bill of Materials):

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>3.3.5</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

(2) Las coordenadas del proyecto

Toda librería Java se identifica por tres valores:

Coordenada Valor en CicloUrbana Significado
groupId com.ciclourbana La organización, en notación de dominio invertido
artifactId ciclourbana El nombre del artefacto
version 0.0.1-SNAPSHOT La versión

El sufijo -SNAPSHOT significa "en desarrollo, puede cambiar". Maven trata los snapshots de forma especial: los vuelve a descargar periódicamente en lugar de cachearlos para siempre. Al publicar una versión estable se quita el sufijo (1.0.0).

Las tres coordenadas determinan el nombre del JAR: ciclourbana-0.0.1-SNAPSHOT.jar.

(3) Las propiedades

<properties>
    <java.version>21</java.version>
</properties>

java.version la lee el parent para configurar el compilador con -source 21 -target 21. Aquí puedes definir tus propias variables y usarlas con la sintaxis ${nombre}:

<properties>
    <java.version>21</java.version>
    <springdoc.version>2.6.0</springdoc.version>
</properties>

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>${springdoc.version}</version>
</dependency>

Esa dependencia lleva <version> porque no está gestionada por el parent de Spring Boot: es una librería de terceros. Es la excepción que confirma la regla.

(4) Los ámbitos de las dependencias

El elemento <scope> decide cuándo está disponible una dependencia:

Scope Compilación Pruebas Ejecución ¿En el JAR? Ejemplo
compile (por defecto) Sí Sí Sí Sí spring-boot-starter-web
runtime No Sí Sí Sí Driver JDBC de PostgreSQL
test No Sí No No spring-boot-starter-test
provided Sí Sí No No API de Servlet en un WAR

spring-boot-devtools combina runtime con <optional>true</optional>: no se compila contra ella, no se propaga a proyectos que dependan del tuyo y Spring Boot la desactiva al detectar que se ejecuta desde un fat jar.

(5) El spring-boot-maven-plugin

Es el plugin que convierte un JAR normal en el fat jar ejecutable. Aporta:

  • El objetivo repackage, enlazado a la fase package, que reempaqueta el JAR con BOOT-INF/ y el JarLauncher.
  • El objetivo spring-boot:run, que arranca la aplicación sin empaquetar.
  • El objetivo build-image, que construye una imagen Docker sin escribir un Dockerfile (módulo 7).

Sin este plugin, ./mvnw package produciría un JAR de 12 KB inservible por sí solo.

  1. Qué es un starter y cuáles usaremos en el curso

Un starter es una dependencia Maven sin código propio: solo un pom.xml que declara un conjunto coherente de dependencias. Su valor es que alguien ya ha decidido por ti qué librerías necesitas y en qué versiones.

Compruébalo:

./mvnw dependency:tree

Salida abreviada:

[INFO] com.ciclourbana:ciclourbana:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:3.3.5:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:3.3.5:compile
[INFO] |  |  +- org.springframework.boot:spring-boot:jar:3.3.5:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-autoconfigure:jar:3.3.5:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:3.3.5:compile
[INFO] |  |  \- org.yaml:snakeyaml:jar:2.2:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-json:jar:3.3.5:compile
[INFO] |  |  \- com.fasterxml.jackson.core:jackson-databind:jar:2.17.2:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-tomcat:jar:3.3.5:compile
[INFO] |  |  \- org.apache.tomcat.embed:tomcat-embed-core:jar:10.1.31:compile
[INFO] |  +- org.springframework:spring-web:jar:6.1.14:compile
[INFO] |  \- org.springframework:spring-webmvc:jar:6.1.14:compile

Una línea en tu pom.xml se ha convertido en más de treinta artefactos coordinados.

Fíjate en spring-boot-starter: es el starter base del que dependen todos los demás. Aporta el núcleo, la autoconfiguración y el sistema de logs. Siempre está presente aunque no lo declares.

Estos son los starters que irán apareciendo en CicloUrbana:

Starter Qué aporta Módulo
spring-boot-starter-web Spring MVC, Jackson, Tomcat embebido, validación 1 y 3
spring-boot-devtools Reinicio automático, LiveReload 1
spring-boot-starter-test JUnit 5, Mockito, AssertJ, Spring Test 1 y 6
spring-boot-starter-validation Bean Validation con Hibernate Validator 3
spring-boot-starter-data-jpa Spring Data JPA, Hibernate, HikariCP 4
spring-boot-starter-security Spring Security, filtros, cifrado de contraseñas 5
spring-boot-starter-actuator Salud, métricas, endpoints de gestión 7 y 9
spring-boot-starter-aop Programación orientada a aspectos 9

Existen también starters de terceros, que por convención invierten el orden del nombre: los oficiales son spring-boot-starter-* y los de terceros <nombre>-spring-boot-starter (por ejemplo mybatis-spring-boot-starter).

  1. El paquete raíz y el escaneo de componentes

Aquí está la causa de uno de los errores más frustrantes del principiante.

La anotación @SpringBootApplication incluye @ComponentScan, que le dice a Spring: "busca clases anotadas con @Component, @Service, @Repository, @Controller o @RestController a partir del paquete de esta clase y hacia abajo".

CicloUrbanaApplication está en com.ciclourbana, así que Spring escanea com.ciclourbana y todos sus subpaquetes. Lo que quede fuera es invisible.

flowchart TD
    A["com.ciclourbana<br/>CicloUrbanaApplication"] --> B["com.ciclourbana.estaciones ✅"]
    A --> C["com.ciclourbana.bicicletas ✅"]
    A --> D["com.ciclourbana.alquileres ✅"]
    A --> E["com.ciclourbana.comun ✅"]
    F["com.empresa.utilidades ❌<br/>fuera del paquete raíz:<br/>Spring NO lo escanea"]

    style F fill:#ffe0e0,stroke:#c00

El síntoma típico es un 404 en un endpoint cuyo código parece impecable: el controlador existe pero Spring nunca lo registró, porque estaba fuera del árbol escaneado.

Reglas prácticas:

  1. Coloca siempre CicloUrbanaApplication en el paquete raíz, por encima de todos los paquetes funcionales.
  2. No la pongas en el paquete por defecto (sin package). Spring escanearía el classpath entero, lo que dispara el tiempo de arranque y provoca errores impredecibles. Spring Boot avisa explícitamente de esto.
  3. Si necesitas escanear un paquete externo —una librería compartida de tu empresa—, amplía el escaneo:
@SpringBootApplication(scanBasePackages = {"com.ciclourbana", "com.ribalta.comun"})
public class CicloUrbanaApplication {
    public static void main(String[] args) {
        SpringApplication.run(CicloUrbanaApplication.class, args);
    }
}

Esta es la razón concreta de por qué "la clase principal va en el paquete raíz" no es una manía estética, sino un requisito de funcionamiento.

  1. Organizar el código: por capas o por funcionalidad

Hay dos formas de estructurar los paquetes, y la elección condiciona el mantenimiento del proyecto durante años.

Por capas (layer-based)

com.ciclourbana
├── controller/
│   ├── EstacionController.java
│   ├── BicicletaController.java
│   └── AlquilerController.java
├── service/
│   ├── EstacionService.java
│   └── AlquilerService.java
├── repository/
│   ├── EstacionRepository.java
│   └── AlquilerRepository.java
└── model/
    ├── Estacion.java
    └── Alquiler.java

Por funcionalidad (feature-based o package by feature)

com.ciclourbana
├── estaciones/
│   ├── EstacionController.java
│   ├── EstacionService.java
│   ├── EstacionRepository.java
│   └── Estacion.java
├── alquileres/
│   ├── AlquilerController.java
│   ├── AlquilerService.java
│   ├── AlquilerRepository.java
│   └── Alquiler.java
└── comun/

Comparativa

Criterio Por capas Por funcionalidad
Encontrar todo lo de "alquileres" Hay que abrir 4 paquetes Está en un solo paquete
Cohesión Baja: el paquete service mezcla dominios sin relación Alta: cada paquete es un tema
Encapsulación Nula: todo debe ser public para cruzar capas Se puede usar visibilidad de paquete
Escalabilidad Los paquetes crecen sin límite Crece el número de paquetes, cada uno acotado
Extraer un microservicio Difícil: hay que rebuscar en todas las capas Fácil: te llevas el paquete entero
Familiaridad Muy extendida en tutoriales Recomendada por la comunidad para proyectos reales

CicloUrbana usará organización por funcionalidad. La razón decisiva es la última fila de la tabla: en el módulo 7 hablaremos de microservicios, y con esta estructura extraer "alquileres" a un servicio independiente es casi copiar una carpeta.

  1. La estructura de paquetes de CicloUrbana

Esta es la organización definitiva que el proyecto irá completando módulo a módulo:

flowchart TD
    R["com.ciclourbana<br/>CicloUrbanaApplication"]

    R --> EST["estaciones<br/>Estacion, EstacionController<br/>EstacionService, EstacionRepository"]
    R --> BIC["bicicletas<br/>Bicicleta, EstadoBicicleta<br/>BicicletaController, BicicletaService"]
    R --> ALQ["alquileres<br/>Alquiler, Tarifa, Incidencia<br/>AlquilerController, AlquilerService"]
    R --> USU["usuarios<br/>Usuario, Rol<br/>UsuarioController, UsuarioService"]
    R --> SEG["seguridad<br/>ConfiguracionSeguridad<br/>FiltroJwt, ServicioTokens"]
    R --> COM["comun<br/>Excepciones, manejador global<br/>utilidades compartidas"]

    ALQ -.usa.-> BIC
    ALQ -.usa.-> EST
    ALQ -.usa.-> USU
    SEG -.usa.-> USU

Descripción de cada paquete y en qué módulo se llenará:

Paquete Contenido previsto Se construye en
estaciones Estaciones de anclaje, capacidad, ubicación Módulos 1, 3 y 4
bicicletas Bicicletas eléctricas, estado y batería Módulos 3 y 4
alquileres Alquileres, tarifas e incidencias Módulos 3, 4 y 9
usuarios Usuarios de la plataforma y sus roles Módulos 4 y 5
seguridad Configuración de Spring Security, filtros JWT Módulo 5
comun Excepciones propias, manejador global de errores, utilidades Módulo 3 en adelante

Las flechas punteadas del diagrama muestran las dependencias legítimas entre paquetes. Una regla que conviene respetar desde el principio: el paquete comun no debe depender de ningún paquete funcional. Si comun importa algo de alquileres, deja de ser común y aparecen dependencias circulares difíciles de deshacer.

De momento solo existe estaciones, con Estacion y EstacionController. Es lo correcto: los paquetes se crean cuando hay algo que meter dentro, no antes.

  1. El ciclo de vida de Maven

Maven organiza la construcción en fases ordenadas. Al invocar una fase se ejecutan todas las anteriores.

Comando Qué hace Cuándo usarlo
./mvnw clean Borra la carpeta target Cuando sospechas de restos de compilaciones previas
./mvnw compile Compila src/main/java a target/classes Comprobar rápidamente que compila
./mvnw test Compila y ejecuta las pruebas de src/test/java Antes de cada commit
./mvnw package Todo lo anterior + genera el fat jar en target Para obtener el artefacto desplegable
./mvnw install Todo lo anterior + copia el JAR a ~/.m2/repository Cuando otro proyecto local depende de este
./mvnw verify Todo lo anterior + comprobaciones de calidad En integración continua
flowchart LR
    A["validate"] --> B["compile"] --> C["test"] --> D["package"] --> E["verify"] --> F["install"] --> G["deploy"]
    H["clean"] -.independiente.-> A

clean pertenece a un ciclo distinto, por eso se combina explícitamente:

# La combinación más habitual: construcción limpia y completa
./mvnw clean package

# Saltar las pruebas (útil puntualmente, peligroso como costumbre)
./mvnw clean package -DskipTests

# Ejecutar solo una clase de prueba
./mvnw test -Dtest=CicloUrbanaApplicationTests

# Modo offline: usar solo la caché local
./mvnw -o clean package

Sobre -DskipTests: compila las pruebas pero no las ejecuta. Existe también -Dmaven.test.skip=true, que ni siquiera las compila y por tanto oculta errores de compilación en el código de test. Prefiere el primero.

Errores Comunes y Consejos

  • Poner el controlador fuera del paquete raíz. Es la causa número uno de "mi endpoint devuelve 404 y no entiendo por qué". Comprueba siempre que el paquete de la clase empieza por com.ciclourbana.
  • Añadir <version> a dependencias que gestiona el parent. Rompes la coherencia del conjunto y puedes provocar NoSuchMethodError en ejecución, un error especialmente difícil de diagnosticar.
  • Versionar la carpeta target. Ensucia el repositorio con megabytes de artefactos regenerables. El .gitignore de Initializr ya la excluye; no lo toques.
  • Confundir src/main/resources con src/main/java. Un application.properties colocado en src/main/java no se copia al classpath y simplemente se ignora, sin ningún aviso.
  • No versionar mvnw y .mvn/. Al clonar, nadie podría construir sin instalar Maven. Deben estar en Git.
  • Consejo — inspecciona el árbol de dependencias. ./mvnw dependency:tree responde a "¿de dónde sale esta librería?" y a los conflictos de versiones. Es la herramienta de diagnóstico más útil de Maven.
  • Consejo — usa help:effective-pom una vez. Ver el POM efectivo aclara de golpe qué está haciendo el parent por ti.
  • Consejo — un paquete por concepto de negocio, no por tecnología. Si te encuentras creando un paquete utils que crece sin control, es señal de que falta identificar un concepto de dominio.

Ejercicios

Ejercicio 1

Ejecuta ./mvnw dependency:tree en tu proyecto y responde: ¿cuántos artefactos arrastra spring-boot-starter-web? ¿Qué versión de Tomcat embebido se está usando? ¿De qué starter viene Jackson?

Ejercicio 2

Crea deliberadamente el error del paquete raíz: mueve EstacionController al paquete com.otraempresa.web, arranca la aplicación y comprueba qué ocurre. Después arréglalo de dos formas distintas y explica cuál es preferible.

Ejercicio 3

Prepara la estructura de paquetes de CicloUrbana creando los paquetes vacíos previstos y un fichero package-info.java en cada uno que documente su responsabilidad. Justifica por qué comun no debe depender de ningún otro paquete.

Soluciones

Solución 1

./mvnw dependency:tree -Dincludes=org.springframework.boot:spring-boot-starter-web

Respuestas típicas con Spring Boot 3.3.5:

  • Número de artefactos: alrededor de 30 dependencias transitivas. Puedes contarlas con:
./mvnw dependency:list | grep -c ":compile"
  • Tomcat embebido: org.apache.tomcat.embed:tomcat-embed-core:10.1.31. Es Tomcat 10.1, la primera rama que usa el espacio de nombres jakarta.*, coherente con Spring Boot 3.
  • Jackson: llega a través de spring-boot-starter-json, que a su vez es dependencia de spring-boot-starter-web. La cadena es:
spring-boot-starter-web
  └─ spring-boot-starter-json
       └─ com.fasterxml.jackson.core:jackson-databind

Esto explica que el endpoint de la lección anterior devolviera JSON sin que añadieras ninguna dependencia: venía incluida.

Solución 2

Al mover la clase:

package com.otraempresa.web;   // fuera del árbol de com.ciclourbana

@RestController
@RequestMapping("/api/v1/estaciones")
public class EstacionController { /* ... */ }

La aplicación arranca sin ningún error —esto es lo desconcertante— pero:

curl -i http://localhost:8080/api/v1/estaciones
# HTTP/1.1 404

El controlador no se ha registrado porque el escaneo de componentes nunca visitó com.otraempresa.web.

Arreglo 1: ampliar el escaneo.

@SpringBootApplication(scanBasePackages = {"com.ciclourbana", "com.otraempresa.web"})
public class CicloUrbanaApplication { /* ... */ }

Arreglo 2: devolver la clase a su sitio.

package com.ciclourbana.estaciones;

El segundo es claramente preferible. El primero funciona, pero introduce una excepción a la convención que hay que recordar y documentar; con el tiempo aparecen más paquetes sueltos y el escaneo se vuelve imprevisible. La ampliación de scanBasePackages está justificada solo cuando se integra una librería externa cuyo paquete no puedes cambiar.

Solución 3

mkdir -p src/main/java/com/ciclourbana/{estaciones,bicicletas,alquileres,usuarios,seguridad,comun}

Un ejemplo de package-info.java, un fichero especial de Java cuya única función es documentar un paquete:

/**
 * Gestión de las estaciones de anclaje de la red de Ribalta.
 *
 * <p>Contiene el modelo de estación, su controlador REST bajo
 * {@code /api/v1/estaciones}, la lógica de negocio asociada y,
 * a partir del módulo 4, su repositorio de persistencia.</p>
 *
 * <p>Este paquete puede depender de {@code comun}, pero no de
 * {@code alquileres} ni de {@code seguridad}.</p>
 */
package com.ciclourbana.estaciones;

Y el del paquete común:

/**
 * Código transversal compartido por el resto de paquetes:
 * excepciones de negocio, manejador global de errores,
 * utilidades de fecha y constantes de la API.
 *
 * <p>REGLA: este paquete NO debe importar nada de
 * {@code estaciones}, {@code bicicletas}, {@code alquileres},
 * {@code usuarios} ni {@code seguridad}.</p>
 */
package com.ciclourbana.comun;

Justificación de la regla: comun es la base sobre la que se apoyan los demás paquetes. Si dependiera de alquileres, se crearía un ciclo (alquileres → comun → alquileres) con tres consecuencias graves: sería imposible razonar sobre el orden de inicialización, no se podría extraer comun a una librería reutilizable, y cualquier cambio en alquileres obligaría a recompilar y volver a probar prácticamente todo el proyecto. Las dependencias deben fluir siempre de lo específico a lo general, nunca al revés.

Conclusión

Ya no hay ficheros misteriosos en el proyecto. Sabes qué hay en src/main/java, src/main/resources, src/test/java y target; has leído el pom.xml entero y entiendes que el parent es quien gestiona las versiones de más de doscientas librerías, que un starter es una lista curada de dependencias sin código propio, y que el spring-boot-maven-plugin es quien fabrica el fat jar. Sobre todo, sabes por qué CicloUrbanaApplication debe vivir en el paquete raíz: el escaneo de componentes parte de ahí, y lo que quede fuera será invisible para Spring. Y has fijado la estructura por funcionalidad —estaciones, bicicletas, alquileres, usuarios, seguridad, comun— que acompañará al proyecto hasta el módulo 10.

En la siguiente lección, El Arranque y el Ciclo de Vida de la Aplicación, entraremos en SpringApplication.run(...) para ver paso a paso qué ocurre entre que pulsas Run y aparece "Started CicloUrbanaApplication": la creación del contexto, el escaneo, la autoconfiguración, el arranque de Tomcat y los eventos que puedes aprovechar para ejecutar tu propio código en el momento justo.

Curso de Spring Boot

Módulo 1: Introducción a Spring Boot

Módulo 2: Conceptos Básicos de Spring Boot

Módulo 3: Construyendo Servicios Web RESTful

Módulo 4: Acceso a Datos con Spring Boot

Módulo 5: Seguridad en Spring Boot

Módulo 6: Pruebas en Spring Boot

Módulo 7: Funciones Avanzadas de Spring Boot

Módulo 8: Despliegue de Aplicaciones Spring Boot

Módulo 9: Rendimiento y Monitoreo

Módulo 10: Mejores Prácticas y Consejos

© Copyright 2026. Todos los derechos reservados