Durante diez módulos hemos construido la red de bicicletas eléctricas de Ribalta capa por capa. Primero un endpoint que devolvía cuatro estaciones en memoria; después el contenedor de Spring y sus beans; el contrato REST con sus DTOs y sus errores; la persistencia sobre PostgreSQL con Flyway; la seguridad con JWT y reglas por dato; las pruebas; la operabilidad; el despliegue; y por fin la observabilidad, el criterio y la limpieza. Cada lección añadió una pieza y miró casi siempre hacia la siguiente.
Nunca hemos mirado CicloUrbana entera. Esta lección lo hace: la arquitectura completa en un diagrama, la estructura real del repositorio, el viaje de una sola petición desde el balanceador hasta la métrica —citando en cada paso dónde se estudió—, el mapa de qué construyó cada módulo, las decisiones de diseño con sus alternativas y sus contrapartidas, el pom.xml y la configuración finales, cómo poner el proyecto en marcha desde cero y hacia dónde seguir llevándolo. Si has seguido el curso, aquí vas a reconocerlo todo; lo nuevo es verlo junto.
Contenido
- La arquitectura final
- El repositorio completo
- El viaje de una petición:
POST /api/v1/alquileres - Mapa del curso: qué construyó cada módulo
- Decisiones de diseño y sus alternativas
- El
pom.xmlfinal - La configuración final
- Poner en marcha el proyecto desde cero
- Recorrido de extremo a extremo con peticiones
.http - Lista de comprobación final de puesta en producción
- Hacia dónde seguir con este proyecto
- Errores Comunes y Consejos
- Ejercicios
- La arquitectura final
flowchart TB
subgraph CLI["Clientes"]
M["App móvil<br/>ciudadanos de Ribalta"]
P["Panel del ayuntamiento<br/>operarios y administración"]
end
subgraph BORDE["Borde"]
ALB["ALB / Ingress<br/>TLS, límite de tasa · 08-03, 08-04"]
end
subgraph APP["CicloUrbana · 3 réplicas · 07-04, 08-04"]
F["Cadena de filtros<br/>FiltroTraza · FiltroAutenticacionJwt · 03-06, 05-04"]
C["Controladores REST<br/>DTOs y validación · 03-02, 03-05"]
S["Servicios<br/>@Transactional · @PreAuthorize · 04-07, 05-05"]
R["Repositorios<br/>Spring Data JPA · 04-05"]
T["Tareas y asincronía<br/>@Scheduled · @Async · 07-03"]
AC["Actuator<br/>sondas, métricas · 07-01, 09-03"]
F --> C --> S --> R
S --> T
end
subgraph DAT["Datos"]
BD[("PostgreSQL 16<br/>esquema con Flyway · 04-08")]
RD[("Redis<br/>caché distribuida · 09-02")]
end
subgraph EXT["Servicios externos"]
PG["Pasarela de pagos<br/>RestClient + Resilience4j · 07-06"]
end
subgraph OBS["Observabilidad · módulo 9"]
PR["Prometheus"]
LO["Loki"]
TE["Tempo"]
OT["OTel Collector"]
GR["Grafana"]
OT --> TE
PR --> GR
LO --> GR
TE --> GR
end
M --> ALB
P --> ALB
ALB --> F
R --> BD
S --> RD
T --> PG
AC -.->|"scrape /actuator/prometheus"| PR
APP -.->|"logs JSON a stdout"| LO
APP -.->|"OTLP"| OT
Cuatro observaciones sobre el dibujo, porque un diagrama sin lectura es decoración.
La aplicación es un solo desplegable. Los cinco bloques de dentro no son servicios: son capas y módulos dentro del mismo proceso. Es el monolito modular de 07-05, y las flechas entre ellos son llamadas a métodos, no llamadas de red.
Las dependencias apuntan hacia dentro y hacia abajo. Filtros → controladores → servicios → repositorios → base de datos. Ninguna flecha sube. Es la regla de dependencia de 10-01, verificada automáticamente con las reglas de ArchUnit de 10-03.
La observabilidad es un lateral, no una capa. No está en el camino de la petición: la aplicación publica métricas, logs y trazas, y otros los recogen. Si Prometheus cae, CicloUrbana sigue funcionando.
Lo único que puede tumbar el servicio es PostgreSQL. La caché de Redis degrada a Caffeine, la pasarela tiene cortacircuitos y respaldo de cobro diferido, y el resto es opcional. Esa asimetría es deliberada y está detrás de que la sonda readiness sí consulte la base de datos y la de liveness no.
- El repositorio completo
ciclourbana/
├── .github/workflows/
│ ├── ci.yml # compilar, probar, JaCoCo, Spotless · 08-05
│ └── cd.yml # imagen, Trivy, despliegue a pre y prod · 08-05
├── helm/ciclourbana/
│ ├── Chart.yaml · values.yaml · values-pre.yaml · values-prod.yaml
│ └── templates/ # deployment, service, ingress, hpa, job-migracion · 08-04
├── observabilidad/ # prometheus.yml, loki.yml, tempo.yml,
│ # otel-collector.yml, tableros de Grafana · 09-04, 09-06
├── carga/carga-hora-punta.js # escenario de k6 · 09-01
├── peticiones/ciclourbana.http # colección de extremo a extremo · apartado 9
├── src/main/java/com/ciclourbana/
│ ├── CicloUrbanaApplication.java # raíz del escaneo · 01-04
│ ├── comun/ # transversal
│ │ ├── ConfiguracionComun.java # Clock, RestClient.Builder · 02-01
│ │ ├── EntidadAuditable.java # createdAt/By, updatedAt/By · 04-03
│ │ ├── PaginaResponse.java # envoltorio propio de Page · 04-05
│ │ ├── FiltroTraza.java # traceId en MDC y cabecera · 03-06, 09-06
│ │ ├── ManejadorGlobalExcepciones.java # ProblemDetail RFC 7807 · 03-06
│ │ ├── CicloUrbanaException.java + jerarquía · 03-06
│ │ └── MetricasCicloUrbana.java # métricas de negocio · 09-03
│ ├── estaciones/ # Estacion, Repositorio, Service,
│ │ └── dto/ # Controller, Mapper + DTOs · módulos 3 y 4
│ ├── bicicletas/ # Bicicleta, EstadoBicicleta,
│ │ └── dto/ # @MatriculaBicicleta, RevisorFlota
│ ├── alquileres/ # Alquiler, CalculadoraTarifa y las tres
│ │ └── dto/ # tarifas, SelectorTarifa, CaducadorAlquileres
│ ├── usuarios/ # Usuario, roles, monedero
│ ├── seguridad/ # ConfiguracionSeguridad, ServicioJwt,
│ │ # FiltroAutenticacionJwt, UsuarioAutenticado,
│ │ # SeguridadAlquileres, AuditoriaSeguridad · módulo 5
│ ├── pagos/ # ClientePasarelaPagos + Resilience4j · 07-06
│ └── config/ # ConfiguracionTareas, ConfiguracionAsincronia,
│ # ConfiguracionCache, ConfiguracionOpenApi
├── src/main/resources/
│ ├── application.yml # común a todos los entornos · 07-02
│ ├── application-{dev,test,pre,prod}.yml # solo las diferencias
│ ├── logback-spring.xml # JSON a stdout con traceId · 09-05
│ └── db/migration/V1__…V9__…sql # esquema versionado · 04-08
├── src/test/java/com/ciclourbana/ # espejo del árbol de producción · 06-01
│ ├── …Test.java # unitarias y rodajas (Surefire)
│ ├── …IT.java # integración con Testcontainers (Failsafe)
│ ├── PruebaIntegracionBase.java # contexto compartido · 06-05
│ └── ReglasArquitecturaTest.java # ArchUnit · 10-03
├── Dockerfile # multietapa, usuario ciclo · 07-04
├── docker-compose.yml # app + PostgreSQL + Redis
├── docker-compose.observabilidad.yml # Prometheus, Grafana, Loki, Tempo, Collector
├── .dockerignore · .gitignore · .env.ejemplo
├── mvnw · mvnw.cmd · .mvn/
└── pom.xmlDos cosas merecen atención. src/test es un espejo exacto de src/main, lo que permite acceder a miembros con visibilidad de paquete sin abrir la clase de producción y hace el árbol navegable. Y la separación *Test / *IT no es cosmética: Surefire ejecuta los primeros en ./mvnw test en segundos, y Failsafe los segundos en ./mvnw verify con PostgreSQL real.
- El viaje de una petición:
POST /api/v1/alquileres
POST /api/v1/alquileresMarta abre la aplicación en la estación «Plaza Mayor», elige una bicicleta y pulsa «alquilar». Esto es lo que ocurre.
sequenceDiagram
autonumber
participant M as App móvil
participant L as ALB / Ingress
participant FS as Filtros de Spring Security
participant DS as DispatcherServlet
participant CT as AlquilerController
participant SV as AlquilerService (proxy)
participant BD as PostgreSQL
participant EV as NotificadorAlquiler
participant PG as Pasarela de pagos
M->>L: POST /api/v1/alquileres · Bearer …
L->>FS: TLS terminado · X-Forwarded-*
FS->>FS: FiltroTraza: span raíz + traceId en MDC
FS->>FS: FiltroAutenticacionJwt: valida firma y exp
FS->>DS: SecurityContext con UsuarioAutenticado
DS->>CT: binding a IniciarAlquilerRequest
CT->>CT: @Valid: matrícula, ids, tarifa
CT->>SV: iniciar(peticion)
SV->>SV: @PreAuthorize: isAuthenticated()
SV->>BD: BEGIN (READ_COMMITTED)
SV->>BD: SELECT … FOR UPDATE (mejor bicicleta)
SV->>SV: SelectorTarifa: calcula el importe
SV->>BD: INSERT alquiler · UPDATE bicicleta
SV->>BD: COMMIT
SV-->>CT: AlquilerResponse (mapeado dentro de la tx)
CT-->>M: 201 Created · Location · X-Trace-Id
Note over EV,PG: AFTER_COMMIT, ya fuera del hilo HTTP
SV->>EV: evento AlquilerIniciado
EV->>PG: autorizar cobro (@Async + cortacircuitos)
El detalle paso a paso, con su lección:
| # | Qué ocurre | Pieza | Lección |
|---|---|---|---|
| 1 | TLS termina en el balanceador, que reenvía con X-Forwarded-Proto |
ALB / Ingress | 08-03, 08-04 |
| 2 | La petición entra en la cadena de filtros de servlet | SecurityFilterChain |
05-01 |
| 3 | FiltroTraza abre el span raíz y pone traceId/spanId en el MDC |
FiltroTraza |
03-06, 09-06 |
| 4 | FiltroAutenticacionJwt valida la firma HS256 y la caducidad, y construye el UsuarioAutenticado |
ServicioJwt |
05-04 |
| 5 | AuthorizationFilter comprueba que la ruta exige autenticación y la tiene |
authorizeHttpRequests |
05-02 |
| 6 | El DispatcherServlet resuelve el handler y deserializa el cuerpo con Jackson |
@RequestBody |
03-02 |
| 7 | Bean Validation comprueba el DTO: @NotNull, @Positive, @MatriculaBicicleta |
IniciarAlquilerRequest |
03-04 |
| 8 | El controlador delega en el servicio; no hay lógica aquí | AlquilerController |
03-03 |
| 9 | El proxy del servicio evalúa @PreAuthorize antes de entrar |
@EnableMethodSecurity |
05-05 |
| 10 | El mismo proxy abre la transacción: conexión del pool, autoCommit=false |
@Transactional |
04-07 |
| 11 | SELECT … FOR UPDATE reserva la mejor bicicleta disponible y serializa la carrera |
@Lock(PESSIMISTIC_WRITE) |
04-07 |
| 12 | SelectorTarifa elige la CalculadoraTarifa del usuario y calcula el importe |
TarifaEstandar y hermanas |
02-02 |
| 13 | INSERT del alquiler y UPDATE de la bicicleta por dirty checking |
Spring Data JPA | 04-05 |
| 14 | El mapeo a AlquilerResponse ocurre dentro de la transacción |
AlquilerMapper |
03-05 |
| 15 | COMMIT; la conexión vuelve al pool |
HikariCP | 04-02 |
| 16 | @TransactionalEventListener(AFTER_COMMIT) dispara el evento ya confirmado |
AlquilerIniciado |
04-07 |
| 17 | El cobro sale en otro hilo, con MDC y SecurityContext propagados |
@Async("ejecutorCorreo") |
07-03 |
| 18 | La llamada a la pasarela lleva traceparent, tiempo de espera y cortacircuitos |
RestClient + Resilience4j |
07-06 |
| 19 | El controlador responde 201 con Location y X-Trace-Id |
ResponseEntity |
03-03 |
| 20 | MetricasCicloUrbana incrementa ciclourbana.alquileres.iniciados{tarifa,estacion} |
Micrometer | 09-03 |
| 21 | Se escribe una línea JSON a stdout con el traceId, y el span se exporta por OTLP |
Logback + Tracing | 09-05, 09-06 |
Lo que se ve al juntarlo. Los pasos 9 y 10 los hace el mismo proxy, y por eso la trampa de la autoinvocación se lleva por delante la transacción y la comprobación de permisos a la vez. El paso 14 no es un detalle de estilo: con open-in-view: false es la única forma de que la respuesta se construya sin LazyInitializationException. Y los pasos 16 a 18 son la razón de que Marta reciba su 201 en decenas de milisegundos mientras el cobro tarda casi dos segundos en otro hilo.
- Mapa del curso: qué construyó cada módulo
| Módulo | Qué se construyó | Clases principales | Ficheros del repositorio |
|---|---|---|---|
| 1 Introducción | El proyecto y su ciclo de vida | CicloUrbanaApplication, CargadorEstacionesDemo |
pom.xml, mvnw, src/main/java, application.properties |
| 2 Conceptos básicos | El contenedor, la configuración tipada y el sistema de tarifas | CalculadoraTarifa, TarifaEstandar, TarifaEstudiante, SelectorTarifa, TarifasProperties, RedProperties |
comun/ConfiguracionComun.java, application.yml |
| 3 REST | El contrato público completo | EstacionController, DTOs record, EstacionMapper, ManejadorGlobalExcepciones, FiltroTraza, @MatriculaBicicleta |
*/dto/, comun/, ConfiguracionOpenApi |
| 4 Datos | La persistencia real | Estacion, Bicicleta, Alquiler, EntidadAuditable, repositorios, PaginaResponse |
db/migration/V1…V9, application.yml (Hikari, JPA) |
| 5 Seguridad | Autenticación y autorización | ConfiguracionSeguridad, ServicioJwt, FiltroAutenticacionJwt, UsuarioAutenticado, SeguridadAlquileres, AuditoriaSeguridad |
seguridad/, application-prod.yml |
| 6 Pruebas | La red de seguridad | TarifaEstandarTest, EstacionControllerTest, AlquilerFlujoCompletoIT, PruebaIntegracionBase |
src/test/, JaCoCo y Failsafe en pom.xml |
| 7 Operabilidad | Iniciativa propia y empaquetado | CaducadorAlquileres, RecalculadorOcupacion, RevisorFlota, NotificadorAlquiler, DecoradorMdc, ClientePasarelaPagos |
Dockerfile, docker-compose.yml, V7__shedlock.sql |
| 8 Despliegue | La entrega automática | — | .github/workflows/, helm/ |
| 9 Rendimiento | Ver y medir | MetricasCicloUrbana, FiltroContadorConsultas, EstacionResumen |
observabilidad/, carga/, logback-spring.xml |
| 10 Criterio | La reflexión y las reglas automáticas | ReglasArquitecturaTest, TarifaConRecargoPorExceso |
Spotless en pom.xml, docs/decisiones/ |
- Decisiones de diseño y sus alternativas
Toda decisión de arquitectura compra algo y paga algo. Estas son las seis principales de CicloUrbana, con lo que costaron.
| Decisión | Alternativa descartada | Por qué | Contrapartida que asumimos |
|---|---|---|---|
| Monolito modular | Microservicios | Un equipo, un dominio acoplado, transacciones locales y una traza de pila que lo explica todo | Escalar significa replicar la aplicación entera, aunque solo los alquileres tengan carga |
| JWT sin estado | Sesión con cookie | Tres réplicas sin sesión pegajosa ni almacén compartido; el móvil no maneja cookies con comodidad | Un token no se puede revocar antes de que caduque; hace falta caducidad corta y refresco rotatorio |
| Flyway | ddl-auto: update |
El esquema se revisa en la pull request, se versiona y se reproduce igual en los cuatro entornos | Cada cambio de entidad exige escribir la migración a mano; y renombrar cuesta tres despliegues |
| MapStruct | ModelMapper o mapeo manual | unmappedTargetPolicy=ERROR convierte el campo olvidado en error de compilación, sin coste en ejecución |
Un procesador de anotaciones más y código generado que hay que saber leer |
| Caffeine + Redis | Solo Redis | Caffeine responde en nanosegundos y sin red para lo que tolera divergencia de segundos | Dos niveles de caché que invalidar; hay datos que pueden diferir unos segundos entre réplicas |
| Testcontainers | H2 en modo PostgreSQL | Índices parciales, tipos, funciones y bloqueos se comportan de verdad como en producción | La suite de integración tarda minutos y necesita Docker en la máquina y en la canalización |
Y dos decisiones menores que ilustran el mismo modo de pensar. open-in-view: false compra que ninguna consulta se dispare durante la serialización, y paga con la obligación de mapear a DTO dentro de la transacción —lo que, de paso, refuerza una práctica que queríamos igualmente—. La denegación por defecto compra que un olvido produzca un 403 visible en vez de un endpoint abierto, y paga con que cada ruta nueva exija una regla explícita.
Ninguna de estas seis es universalmente correcta. Lo que las hace defendibles es que se tomaron con la contrapartida a la vista y quedaron escritas.
- El
pom.xml final
pom.xml finalLas dependencias acumuladas, agrupadas por el módulo que las introdujo. La columna de versión es la información importante: solo llevan versión propia las que el BOM de Spring Boot no gestiona, y son exactamente las que hay que revisar a mano en cada actualización.
| Módulo | Dependencias (org.springframework.boot: salvo indicación) |
Ámbito | Versión |
|---|---|---|---|
| 3 REST | spring-boot-starter-web, spring-boot-starter-validation |
compile | BOM |
| 3 | org.springdoc:springdoc-openapi-starter-webmvc-ui |
compile | 2.6.0 |
| 3 | org.mapstruct:mapstruct |
compile | 1.6.3 |
| 4 Datos | spring-boot-starter-data-jpa, org.flywaydb:flyway-core, flyway-database-postgresql |
compile | BOM |
| 4 | org.postgresql:postgresql |
runtime | BOM |
| 5 Seguridad | spring-boot-starter-security |
compile | BOM |
| 5 | io.jsonwebtoken:jjwt-api (+ jjwt-impl y jjwt-jackson en runtime) |
mixto | 0.12.6 |
| 7 Operación | spring-boot-starter-actuator, spring-boot-starter-aop |
compile | BOM |
| 7 | net.javacrumbs.shedlock:shedlock-spring + shedlock-provider-jdbc-template |
compile | 5.16.0 |
| 7 | io.github.resilience4j:resilience4j-spring-boot3 |
compile | 2.2.0 |
| 9 Rendimiento | spring-boot-starter-cache, com.github.ben-manes.caffeine:caffeine, spring-boot-starter-data-redis |
compile | BOM |
| 9 | io.micrometer:micrometer-registry-prometheus |
runtime | BOM |
| 9 | io.micrometer:micrometer-tracing-bridge-otel, io.opentelemetry:opentelemetry-exporter-otlp |
compile | BOM |
| 9 | net.logstash.logback:logstash-logback-encoder |
compile | 8.0 |
| 6 Pruebas | spring-boot-starter-test, spring-security-test, spring-boot-testcontainers, org.testcontainers:postgresql |
test | BOM |
| 10 Criterio | com.tngtech.archunit:archunit-junit5 |
test | 1.3.0 |
| 1 Desarrollo | spring-boot-devtools (runtime), spring-boot-configuration-processor |
optional | BOM |
Y la parte del pom.xml que de verdad hay que leer con atención, porque es la que rompe la construcción ante un problema concreto:
<parent> <!-- Módulo 1: versiones coherentes -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.5</version>
</parent>
<groupId>com.ciclourbana</groupId>
<artifactId>ciclourbana</artifactId>
<version>2.4.0</version>
<properties><java.version>21</java.version></properties>
<build>
<plugins>
<plugin><groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId></plugin>
<plugin> <!-- MapStruct: el campo olvidado no compila (03-05) -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path><groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version></path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amapstruct.unmappedTargetPolicy=ERROR</arg>
<arg>-Amapstruct.defaultComponentModel=spring</arg>
<arg>-parameters</arg> <!-- necesario para #idUsuario en SpEL (05-05) -->
</compilerArgs>
</configuration>
</plugin>
<plugin> <!-- Failsafe: las *IT en verify, no en test (06-01) -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions><execution>
<goals><goal>integration-test</goal><goal>verify</goal></goals>
</execution></executions>
</plugin>
<plugin> <!-- JaCoCo: mapa del rojo, no puntuación (06-01) -->
<groupId>org.jacoco</groupId><artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.12</version>
<executions>
<execution><goals><goal>prepare-agent</goal></goals></execution>
<execution><phase>verify</phase><goals><goal>report</goal></goals></execution>
</executions>
</plugin>
<plugin> <!-- Spotless: el estilo no se discute (10-03) -->
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId><version>2.44.0</version>
<configuration><java><palantirJavaFormat/><removeUnusedImports/></java></configuration>
<executions><execution><phase>validate</phase>
<goals><goal>check</goal></goals></execution></executions>
</plugin>
<plugin> <!-- Dependency-Check: falla ante severidad alta (05-05) -->
<groupId>org.owasp</groupId><artifactId>dependency-check-maven</artifactId>
<version>10.0.4</version>
<configuration><failBuildOnCVSS>7</failBuildOnCVSS></configuration>
</plugin>
</plugins>
</build>Los seis plugins no son adorno: cada uno rompe la construcción ante un problema concreto —un campo sin mapear, una prueba de integración fallida, código sin formatear, una vulnerabilidad alta—. Es la idea de fondo de 10-01: mover la comprobación de la cabeza de una persona a la construcción.
- La configuración final
# application.yml — común a todos los entornos
spring:
application:
name: ciclourbana # se convierte en service.name de las trazas (09-06)
threads:
virtual:
enabled: true # hilos virtuales de Java 21 (07-03)
jackson:
default-property-inclusion: non_null
deserialization: { fail-on-unknown-properties: false } # tolerancia del contrato (03-05)
datasource:
url: ${URL_BASE_DATOS} # sin valor por defecto: obligatorio (10-01)
username: ${USUARIO_BASE_DATOS}
password: ${CLAVE_BASE_DATOS}
hikari:
pool-name: ciclourbana-pool
maximum-pool-size: 10 # (núcleos × 2) + discos (09-01)
minimum-idle: 10
connection-timeout: 3000 # fallar rápido, no encolar
leak-detection-threshold: 20000
jpa:
open-in-view: false # decisión estructural del módulo 4
hibernate: { ddl-auto: validate } # el esquema lo gobierna Flyway
properties:
hibernate:
jdbc.batch_size: 50
order_inserts: true
batch_versioned_data: true
flyway: { enabled: true, locations: classpath:db/migration }
cache:
type: caffeine
caffeine: { spec: "maximumSize=1000,expireAfterWrite=10m,recordStats" }
task:
execution:
pool: { core-size: 8, max-size: 24, queue-capacity: 200 }
thread-name-prefix: async-ciclo-
shutdown: { await-termination: true, await-termination-period: 30s }
scheduling:
pool: { size: 4 } # el planificador por defecto tiene UN hilo (07-03)
thread-name-prefix: tarea-ciclo-
lifecycle:
timeout-per-shutdown-phase: 40s # mayor que los await-termination-period
server:
shutdown: graceful # apagado ordenado (01-05, 07-03)
forward-headers-strategy: framework # TLS termina en el balanceador (05-05)
error: { include-stacktrace: never, include-message: never }
compression:
enabled: true
mime-types: application/json,application/problem+json
min-response-size: 1KB
management:
server:
port: 8081 # puerto de gestión separado (07-01)
endpoints:
web:
exposure:
include: health,info,metrics,prometheus,loggers,scheduledtasks
endpoint:
health:
probes: { enabled: true } # /health/liveness y /health/readiness
show-details: when_authorized
tracing:
sampling: { probability: 1.0 } # el muestreo lo decide el Collector, por cola (09-06)
otlp:
tracing:
endpoint: ${OTLP_ENDPOINT:http://otel-collector:4318/v1/traces}
metrics:
tags: { aplicacion: ciclourbana }
ciclourbana: # propiedades propias, validadas (02-05)
ciudad: Ribalta
tarifa:
desbloqueo: 0.50
precio-minuto: 0.12
precio-minuto-estudiante: 0.08
red:
capacidad-minima: 8
umbral-bateria: 20
duracion-maxima-alquiler: PT2H
estaciones-destacadas: [Plaza Mayor, Universidad]
alquileres:
caducador:
intervalo: PT10M
jwt:
secreto: ${JWT_SECRETO} # obligatorio, nunca versionado
expiracion: PT15M
expiracion-refresco: P7DY las diferencias por entorno, que es lo único que cambia:
| Propiedad | dev |
test |
pre |
prod |
|---|---|---|---|---|
| Base de datos | Docker local | Testcontainers | RDS de preproducción | RDS con réplica |
flyway.locations |
+ db/demo |
db/migration |
db/migration |
db/migration |
logging.level.com.ciclourbana |
DEBUG |
INFO |
INFO |
INFO |
| Swagger UI | Activo | Activo | Activo | springdoc.api-docs.enabled: false |
| CORS | http://localhost:5173 |
— | https://pre.ribalta.es |
https://panel.ribalta.es |
| Caché | Caffeine | Deshabilitada | Redis | Redis |
tracing.sampling |
1.0 local |
0.0 |
1.0 → Collector |
1.0 → Collector con cola |
| Actuator expuesto | "*" |
health |
Con ADMIN |
health,info,prometheus |
- Poner en marcha el proyecto desde cero
# 1. Clonar y preparar los secretos locales
git clone https://github.com/ayuntamiento-ribalta/ciclourbana.git
cd ciclourbana
cp .env.ejemplo .env # rellenar CLAVE_BASE_DATOS y JWT_SECRETO
openssl rand -base64 48 # generar un secreto JWT de 256+ bits
# 2. Levantar la infraestructura (PostgreSQL 16 + Redis)
docker compose up -d
docker compose ps # esperar a que el healthcheck marque "healthy"
# 3. Construcción completa: formato, unitarias, *IT con Testcontainers, cobertura
./mvnw verify
open target/site/jacoco/index.html
# 4. Arrancar en desarrollo
./mvnw spring-boot:run -Dspring-boot.run.profiles=dev
# 5. Comprobar que está viva y con el esquema al día
curl -s localhost:8081/actuator/health/readiness | jq
curl -s localhost:8081/actuator/info | jq '.build.version' # 2.4.0
# 6. (Opcional) La pila de observabilidad completa
docker compose -f docker-compose.observabilidad.yml up -d
# Grafana en http://localhost:3000 · Prometheus 9090 · Tempo 3200El paso 3 merece un comentario: ./mvnw verify levanta un PostgreSQL efímero con Testcontainers y aplica las nueve migraciones sobre él antes de ejecutar las *IT. Si ese comando pasa en tu máquina, el proyecto está sano de arriba abajo.
- Recorrido de extremo a extremo con peticiones
.http
.http### peticiones/ciclourbana.http
@host = http://localhost:8080/api/v1
### 1. Registro de una ciudadana de Ribalta
POST {{host}}/auth/registro
Content-Type: application/json
{ "nombre": "Marta Serra", "correo": "[email protected]",
"contrasena": "Bicicleta-2026!", "tipoTarifa": "ESTANDAR" }
> {% client.test("201 y sin datos sensibles", () => {
client.assert(response.status === 201);
client.assert(response.body.contrasenaHash === undefined); }); %}
### 2. Login: devuelve el token de acceso y el de refresco
# @name login
POST {{host}}/auth/login
Content-Type: application/json
{ "correo": "[email protected]", "contrasena": "Bicicleta-2026!" }
> {% client.global.set("token", response.body.tokenAcceso); %}
### 3. Estaciones con bicicletas disponibles (público, paginado)
GET {{host}}/estaciones?pagina=0&tamanio=20
Authorization: Bearer {{token}}
### 4. Detalle de «Plaza Mayor», con sus bicicletas ancladas
GET {{host}}/estaciones/1
Authorization: Bearer {{token}}
### 5. Iniciar el alquiler: 201 con Location y X-Trace-Id
# @name alquiler
POST {{host}}/alquileres
Authorization: Bearer {{token}}
Content-Type: application/json
{ "estacionOrigenId": 1, "matricula": "RB-0142" }
> {% client.global.set("alquilerId", response.body.id);
client.global.set("traza", response.headers.valueOf("X-Trace-Id")); %}
### 6. Finalizar en «Universidad»: calcula el importe y libera la bicicleta
PATCH {{host}}/alquileres/{{alquilerId}}/finalizar
Authorization: Bearer {{token}}
Content-Type: application/json
{ "estacionDestinoId": 4 }
### 7. La factura del alquiler
GET {{host}}/alquileres/{{alquilerId}}
Authorization: Bearer {{token}}
### 8. Comprobación de seguridad: el alquiler de otro ciudadano da 403
GET {{host}}/alquileres/9999
Authorization: Bearer {{token}}Y las tres comprobaciones que cierran el recorrido, que son las que demuestran que la observabilidad del módulo 9 funciona de verdad:
# La métrica de negocio se ha movido
curl -s localhost:8081/actuator/metrics/ciclourbana.alquileres.iniciados | jq '.measurements'
# El log de la petición es consultable por su traceId (Loki, 09-05)
# {app="ciclourbana"} | json | traceId = "<el X-Trace-Id del paso 5>"
# Y la traza completa está en Tempo, con el span del cobro (09-06)
curl -s "http://localhost:3200/api/traces/<traceId>" | jq '.batches | length'Si los tres devuelven datos, las tres señales están conectadas: la métrica detecta, la traza localiza y el log explica.
- Lista de comprobación final de puesta en producción
| # | Comprobación | Verificación objetiva |
|---|---|---|
| 1 | Cero secretos en el repositorio y en su historial | Detector de secretos con --log-opts="--all"; rotar lo encontrado |
| 2 | El perfil prod se activa de verdad |
Buscar en el log de arranque The following 1 profile is active: "prod" |
| 3 | ./mvnw verify en verde, con las *IT incluidas |
La canalización de ci.yml, no la máquina de nadie |
| 4 | Migraciones aplicadas y compatibles hacia atrás | flyway:info y la prueba del apartado 5 con la versión anterior corriendo |
| 5 | Denegación por defecto y reglas por dato | Prueba automatizada de rutas sin token y de identificadores cruzados |
| 6 | Swagger, consola y Actuator cerrados | curl a /swagger-ui.html, /h2-console y /actuator/env: 404 o 401 |
| 7 | HTTPS obligatorio con HSTS y certificado válido | curl -I sobre HTTP: debe rechazar o redirigir |
| 8 | Sondas liveness y readiness diferenciadas |
El manifiesto: livenessProbe no consulta la base de datos |
| 9 | Recursos y HPA dimensionados | kubectl describe hpa y la prueba de carga de k6 contra pre |
| 10 | Logs en JSON con traceId, sin credenciales |
Buscar eyJ, Bearer y un correo conocido en un flujo completo: cero |
| 11 | Métricas y alertas activas | Un panel con tráfico real y una alerta que se dispara al provocarla |
| 12 | Revertir es un comando cronometrado | Ejecutarlo de verdad en pre y anotar el tiempo |
| 13 | Copias de seguridad de PostgreSQL restauradas | Restaurar una copia en un entorno aparte; una copia no probada no existe |
| 14 | Sin vulnerabilidades altas | dependency-check con failBuildOnCVSS=7 en verde |
| 15 | Revisión de seguridad profesional | Informe firmado de auditoría y prueba de penetración |
El punto 13 no apareció en ninguna lección y es de los que más caro salen: una copia de seguridad que nunca se ha restaurado no es una copia de seguridad, es una esperanza. Y el 15 repite la advertencia del módulo 5: todo lo construido en este curso es un punto de partida didáctico y necesita revisión profesional antes de exponerse a Internet.
- Hacia dónde seguir con este proyecto
CicloUrbana está terminada como material de curso, no como producto. Estas siete extensiones están ordenadas por dificultad y cada una combina lecciones que ya conoces.
| # | Extensión | Dificultad | Qué hay que combinar |
|---|---|---|---|
| 1 | Informes con exportación a CSV del histórico de alquileres | Baja | Consultas y proyecciones 04-06, paginación por keyset 09-01, respuesta en flujo y @Async 07-03 |
| 2 | Panel de operario con la vista interna de la flota | Baja | DTOs separados 03-05, rol OPERARIO y @PreAuthorize 05-05, pruebas de seguridad 06-04 |
| 3 | Integración con un mapa y búsqueda por cercanía | Media | RestClient con cortacircuitos 07-06, caché de geocodificación 09-02, índice geoespacial 04-08 |
| 4 | Reserva anticipada de bicicleta, con caducidad a los 10 minutos | Media | Bloqueo pesimista y estados 04-07, tarea con ShedLock 07-03, nueva migración e invariantes en el dominio 10-03 |
| 5 | Notificaciones push al móvil | Media | Eventos AFTER_COMMIT 04-07, ejecutor propio y mamparo 07-03, tolerancia a fallos 07-06 |
| 6 | Tarifas dinámicas por demanda en hora punta | Alta | Otra CalculadoraTarifa y decoradores 02-02, métricas de ocupación 09-03, @ConfigurationProperties 02-05, pruebas parametrizadas 06-02 |
| 7 | Extraer la facturación a un servicio propio | Muy alta | El módulo 7 entero, trazabilidad previa 09-06, patrón outbox y saga, contrato versionado 03-01 |
Sobre la número 7, que es la que más gente quiere hacer primero: es la última de la lista por una razón. Repartir el sistema en dos procesos convierte una transacción local en una saga con compensaciones, una traza de pila en una investigación entre dos despliegues, y una llamada a método en una llamada de red que puede fallar. Hazla solo cuando tengas trazabilidad distribuida funcionando y una razón concreta —escalado o equipo independiente—, no como ejercicio de estilo.
Y una recomendación sobre el método: para cualquiera de las siete, empieza por escribir la prueba del comportamiento que quieres, sigue por la migración si toca el esquema, y deja el controlador para el final. Es el orden que menos código tira a la basura.
Errores Comunes y Consejos
Creer que el diagrama es la arquitectura. El diagrama del apartado 1 es una vista; la arquitectura real es lo que hacen las dependencias del código. Por eso existen las reglas de ArchUnit: son el único diagrama que no puede mentir.
Copiar el pom.xml entero a un proyecto nuevo. Trae Redis, ShedLock, Resilience4j y Testcontainers a un proyecto que quizá no los necesita, y cada dependencia es superficie de mantenimiento y de vulnerabilidades. Empieza por web, data-jpa y test, y añade cuando duela algo.
Levantar la observabilidad entera en local siempre. Prometheus, Grafana, Loki, Tempo y el Collector consumen memoria y tiempo de arranque. En el día a día basta con la aplicación y PostgreSQL; la pila completa se levanta cuando se está trabajando en observabilidad.
Olvidar que el .env no está en el repositorio. Es lo primero que rompe a quien clona el proyecto por primera vez. Un .env.ejemplo versionado con las claves y sin los valores ahorra esa media hora a cada persona nueva.
Consejo: recorre el apartado 3 con un depurador. Poner un punto de interrupción en AlquilerService.iniciar y subir por la pila de llamadas hasta el filtro es el ejercicio que más consolida todo el curso: se ven, en una sola pantalla, el proxy, la transacción, el SecurityContext y el MDC.
Consejo: guarda el registro de decisiones. La tabla del apartado 5 —decisión, alternativa, motivo, contrapartida— es el documento que más se agradece dentro de un año, y el único que responde a «¿por qué está hecho así?».
Consejo: mide antes de extender. Cualquiera de las siete extensiones del apartado 11 cambia el perfil de carga. Guarda la línea base de k6 de hoy: será la referencia para saber si la extensión ha costado algo.
Ejercicios
Ejercicio 1: seguir el hilo de un fallo
En producción, a las 09:12, la app móvil empieza a recibir 500 en POST /api/v1/alquileres. El cuerpo del error es un ProblemDetail con "code": "ERROR_INTERNO" y un traceId. Describe, paso a paso y con las herramientas concretas del proyecto, cómo investigarías el incidente desde ese traceId hasta la causa raíz, indicando qué pieza de qué módulo usarías en cada paso y qué descartarías con cada una.
Ejercicio 2: diseñar la reserva anticipada
Diseña la extensión número 4 del apartado 11: un ciudadano puede reservar una bicicleta concreta durante 10 minutos antes de recogerla; pasado ese tiempo, la reserva caduca y la bicicleta vuelve a estar disponible. Especifica el cambio en el modelo, la migración, el contrato de la API, el control de concurrencia, la tarea de caducidad, las reglas de seguridad, las métricas y las pruebas que escribirías.
Ejercicio 3: revisión de arquitectura
El ayuntamiento de una ciudad vecina quiere reutilizar CicloUrbana. Sus números: 40 estaciones (frente a 4), 300 000 alquileres al mes, dos equipos de desarrollo y un requisito nuevo —integración en tiempo real con el sistema de transporte público—. Analiza qué decisiones del apartado 5 seguirían siendo correctas, cuáles habría que revisar y en qué orden abordarías la adaptación.
Soluciones
Solución 1
Paso 1 — de la alerta al cuadro de mando (09-04). Antes de tocar el traceId, mirar la fila RED del panel: ¿es un pico de errores o un goteo? ¿empezó con el despliegue de las 09:05? Con esto se distingue un fallo sistemático de uno puntual, y se decide si hay que revertir ya.
Paso 2 — el log de esa petición exacta (09-05). En Loki: {app="ciclourbana", entorno="prod"} | json | traceId = "...". Devuelve la historia completa de la petición, incluida la traza de pila que no se le envió al cliente. La mayoría de los incidentes se cierran aquí, con el nombre de la excepción y la línea.
Paso 3 — la cascada de spans (09-06). En Tempo, la misma traza. Aquí se responde algo que el log no dice: dónde se fue el tiempo y qué falló. Una barra roja en el span de la pasarela descarta que el problema sea nuestro; veinte spans select idénticos delatan un N+1 nuevo; un hueco de 900 ms sin instrumentar apunta a espera de conexión o a una pausa de GC.
Paso 4 — descartar recursos (09-03). hikaricp.connections.pending, jvm.gc.pause y resilience4j.circuitbreaker.state en el instante del fallo. Si pending está en 60, el problema no es la lógica: algo retiene conexiones —una llamada HTTP dentro de una transacción es el sospechoso número uno—.
Paso 5 — reproducir (06-05). Con la causa identificada, escribir la prueba que la reproduce antes de arreglar nada. Si el fallo depende de datos reales, una *IT con Testcontainers y el estado exacto que lo provoca.
Paso 6 — decidir (08-05). Si el fallo llegó con el despliegue de las 09:05, revertir primero y diagnosticar después: el tiempo de restauración es una métrica DORA y la reversión es un comando. Si es un problema externo, la decisión es de negocio —degradar, abrir el cortacircuitos, esperar—, como en el caso práctico de 09-06.
Lo que el ejercicio enseña: el recorrido va de lo general a lo particular y de lo barato a lo caro. La métrica cuesta un vistazo, la traza un clic, el log una consulta, y el depurador media mañana. Empezar por el depurador es el error más común.
Solución 2
Modelo. EstadoBicicleta gana el valor RESERVADA. Aparece la entidad Reserva(id, usuario, bicicleta, estacion, creada, expira, estado) con EstadoReserva { ACTIVA, CONSUMIDA, CADUCADA, CANCELADA }. La reserva es un agregado propio y no un campo de Bicicleta, porque tiene ciclo de vida, histórico y reglas propias.
Migración V10__reservas.sql. Tabla con secuencia (allocationSize = 50, nunca IDENTITY, por los lotes de 09-01), claves ajenas a usuarios, bicicletas y estaciones, columnas de auditoría, CHECK sobre el estado, índice (expira) WHERE estado = 'ACTIVA' —parcial, unas decenas de filas frente a millones— y un índice único parcial que impide dos reservas activas del mismo usuario, la misma técnica del uk_alquileres_usuario_en_curso. El enum de la columna estado de bicicletas amplía su CHECK: cambio compatible hacia atrás.
Contrato. POST /api/v1/reservas con { "bicicletaId": 12 } devuelve 201 con Location y ReservaResponse(id, matricula, estacion, expira, segundosRestantes). DELETE /api/v1/reservas/{id} cancela (204). Y POST /api/v1/alquileres acepta opcionalmente reservaId: si viene, consume la reserva en lugar de buscar bicicleta.
Concurrencia. Es el punto delicado. SELECT … FOR UPDATE sobre la bicicleta con timeout de 3 s, comprobación de que sigue DISPONIBLE, y cambio a RESERVADA en la misma transacción. Sin bloqueo, dos ciudadanos reservan la misma bicicleta y uno se lleva un 409 innecesario cuando el sistema podría haberle ofrecido otra.
Caducidad. CaducadorReservas, hermano de CaducadorAlquileres: @Scheduled(fixedDelayString = "${ciclourbana.reservas.caducador.intervalo:PT30S}") con @SchedulerLock —tres réplicas, una ejecución—, lógica en un método público invocable para poder probarla, Clock inyectado y idempotencia: marcar como caducada una reserva ya caducada no cambia nada.
Seguridad. @PreAuthorize("isAuthenticated()") para crear; @PreAuthorize("hasAnyRole('OPERARIO','ADMIN') or @seguridadReservas.esPropietario(#idReserva, principal)") para cancelar y consultar, con un bean nuevo idéntico en forma a SeguridadAlquileres. El listado filtra en la consulta, nunca con @PostFilter.
Métricas. Un Counter ciclourbana.reservas.creadas{estacion} y otro ciclourbana.reservas.caducadas, con la etiqueta de estación —cuatro valores— y nunca el identificador de usuario. La tasa de caducidad es el indicador de negocio interesante: si es alta, la ventana de 10 minutos es demasiado corta o demasiado larga.
Pruebas. Unitaria de Reserva.caducar() con Clock.fixed y del caso límite de los 10 minutos exactos; @DataJpaTest del índice único parcial —dos reservas activas del mismo usuario deben violar la restricción—; rodaja de seguridad con dos ciudadanos cruzando identificadores; y una *IT del flujo completo reservar → alquilar → finalizar, más otra de reservar → esperar → comprobar que caducó, con el reloj controlado.
La decisión de diseño que hay que saber defender: la reserva es un agregado propio. Modelarla como dos campos en Bicicleta parece más barato y hace imposible el histórico, complica la caducidad y mete estado transitorio en una entidad que debería ser estable.
Solución 3
Lo que sigue siendo correcto sin discusión. Flyway, DTOs, denegación por defecto, @Transactional en el servicio, Testcontainers, logs estructurados y observabilidad: son decisiones cuyo beneficio crece con el tamaño, no decrece. MapStruct pasa de opcional a claramente rentable, porque el número de DTOs se multiplica.
Lo que hay que revisar, con su motivo.
| Decisión | Estado con los números nuevos | Qué hacer |
|---|---|---|
| Monolito modular | Sigue siendo correcto, y por poco | 300 000 alquileres al mes son ~7 por minuto de media: ridículo para un monolito. Lo que empuja no es la carga sino dos equipos; la respuesta razonable es Spring Modulith y fronteras internas más estrictas antes que dividir |
| Caffeine local | Insuficiente | Con más réplicas, 40 estaciones y datos que cambian, la divergencia entre cachés locales se nota. Redis como nivel compartido, Caffeine delante para lo inmutable |
| Pool de 10 conexiones | A recalcular | Depende del servidor nuevo, no del anterior: fórmula, prueba de carga y pending como señal |
| JWT de 15 minutos | Correcto, con matices | Con dos equipos y más clientes, hace falta gestión de claves y rotación; y probablemente un proveedor de identidad en lugar de firmar nosotros |
estaciones sin índice geoespacial |
Insuficiente | Con 40 estaciones y búsqueda por cercanía, PostGIS o al menos un índice sobre las coordenadas |
| Integración con transporte público | Decisión nueva | Es el primer caso real de mensajería: horarios en tiempo real no encajan en peticiones síncronas. Kafka o similar, con el patrón outbox |
El orden de la adaptación, que es lo que el ejercicio pregunta de verdad:
- Medir con los datos nuevos antes de tocar nada. Cargar 40 estaciones y 300 000 alquileres en
prey ejecutar el escenario de k6. Casi todas las decisiones anteriores se resuelven con datos, no con opiniones. - Índices y consultas. Lo que se degrada con el volumen se degrada primero, y es lo más barato de arreglar.
- Fronteras de módulo antes que equipos. Con dos equipos, el conflicto no es técnico sino de propiedad del código: reglas de ArchUnit por módulo y fronteras explícitas antes de repartir el trabajo.
- Caché distribuida y pool redimensionado, con la prueba de carga como juez.
- La integración de transporte público, la última, porque introduce una tecnología nueva —mensajería— y conviene hacerlo sobre una base ya estable y medida.
Lo que no haría: dividir en microservicios desde el principio «porque ahora es más grande». Cuarenta estaciones y siete alquileres por minuto no son un problema de escala: son un problema de organización, y ese se resuelve con fronteras internas antes que con procesos separados.
Conclusión
CicloUrbana está completa y, por primera vez, la has visto entera. Tienes la arquitectura final en un diagrama que se lee: un solo desplegable con módulos internos, dependencias que apuntan siempre hacia dentro, la observabilidad como lateral y no como capa, y una única dependencia capaz de tumbar el servicio —PostgreSQL—, lo que explica por qué la sonda readiness la consulta y la de liveness no. Tienes la estructura real del repositorio, con los paquetes por funcionalidad, el árbol de pruebas como espejo del de producción, la separación *Test/*IT y todo lo que rodea al código: Dockerfile, docker-compose, helm/, .github/workflows, observabilidad/ y carga/.
Y tienes el viaje de una petición contado en veintiún pasos, del balanceador a la métrica, con la lección de cada uno. Ese recorrido es el mejor resumen del curso porque en él aparece todo: el filtro que abre el span, el JWT que se valida, el binding y la validación del DTO, el proxy que evalúa el permiso y abre la transacción en el mismo punto —de ahí que la autoinvocación se lleve las dos por delante—, el bloqueo pesimista sobre la última bicicleta de «Plaza Mayor», el mapeo dentro de la transacción que hace posible open-in-view: false, el AFTER_COMMIT que saca el cobro del hilo HTTP y la métrica de negocio que cierra el ciclo.
Tienes también el mapa de qué construyó cada módulo, el pom.xml final donde se ve qué versiones gestiona el BOM y cuáles hay que revisar a mano, la configuración consolidada con la tabla de lo único que cambia entre entornos, los comandos para poner el proyecto en marcha desde cero y la colección .http que recorre registro, login, consulta, alquiler, factura y la comprobación de que un ciudadano no puede leer el alquiler de otro —seguida de las tres consultas que demuestran que la métrica, el log y la traza están unidos por el mismo traceId—.
Y tienes las dos piezas que separan un proyecto de curso de un producto: la tabla de decisiones con sus contrapartidas, que es el documento que responde dentro de un año a «¿por qué está hecho así?», y la lista de comprobación final con sus quince puntos, incluido el que ninguna lección había mencionado —una copia de seguridad que nunca se ha restaurado no es una copia de seguridad— y el que se repite desde el módulo 5: esto es un punto de partida didáctico y necesita revisión profesional antes de exponerse a Internet.
El proyecto no termina aquí, y esa es la idea. Las siete extensiones del apartado 11 están ordenadas por dificultad y cada una te dice qué lecciones combinar, desde el informe con exportación hasta extraer la facturación a un servicio propio —la última de la lista, y con motivo—. Construir cualquiera de ellas sobre esta base es la mejor forma de convertir lo aprendido en algo tuyo.
Queda una última lección, y es la única del curso que no habla de CicloUrbana. Porque saber Spring Boot no es haber terminado un curso: es haber empezado a poder leer la documentación con criterio, distinguir un recurso actualizado de uno que confunde, planificar una actualización de versión mayor sin miedo y elegir qué aprender después entre veinte caminos posibles. Recursos para Aprendizaje Adicional se ocupa de eso: la documentación oficial y cómo leerla, el código fuente como la mejor fuente, cómo mantenerse al día con el ciclo de publicación de Spring, el ecosistema Java, los libros que de verdad valen la pena, las certificaciones con una valoración honesta, la comunidad, la práctica deliberada, los temas naturales para el siguiente paso y una hoja de ruta razonada para los próximos seis meses.
Curso de Spring Boot
Módulo 1: Introducción a Spring Boot
- ¿Qué es Spring Boot?
- Configuración de tu Entorno de Desarrollo
- Creando tu Primera Aplicación Spring Boot
- Entendiendo la Estructura del Proyecto
- El Arranque y el Ciclo de Vida de la Aplicación
Módulo 2: Conceptos Básicos de Spring Boot
- Anotaciones de Spring Boot
- Inyección de Dependencias en Spring Boot
- Ámbito y Ciclo de Vida de los Beans
- Configuración de Spring Boot
- Propiedades de Spring Boot
- Autoconfiguración y Starters por Dentro
Módulo 3: Construyendo Servicios Web RESTful
- Introducción a los Servicios Web RESTful
- Creando Controladores REST
- Manejo de Métodos HTTP
- Validación de Datos de Entrada
- DTOs y Mapeo entre Capas
- Manejo de Excepciones en REST
- Documentar la API con OpenAPI
Módulo 4: Acceso a Datos con Spring Boot
- Introducción a Spring Data JPA
- Configuración de Fuentes de Datos
- Creación de Entidades JPA
- Relaciones entre Entidades
- Uso de Repositorios de Spring Data
- Métodos de Consulta en Spring Data JPA
- Transacciones y Gestión de la Persistencia
- Migraciones de Esquema con Flyway
Módulo 5: Seguridad en Spring Boot
- Introducción a Spring Security
- Configuración de Spring Security
- Autenticación y Autorización de Usuarios
- Implementación de Autenticación JWT
- Seguridad a Nivel de Método y Endurecimiento de la API
Módulo 6: Pruebas en Spring Boot
- Introducción a las Pruebas
- Pruebas Unitarias con JUnit
- Simulación con Mockito
- Pruebas de Integración
- Pruebas con Testcontainers
Módulo 7: Funciones Avanzadas de Spring Boot
- Spring Boot Actuator
- Perfiles de Spring Boot
- Tareas Programadas y Ejecución Asíncrona
- Spring Boot con Docker
- Spring Boot y Microservicios
- Comunicación entre Servicios y Tolerancia a Fallos
Módulo 8: Despliegue de Aplicaciones Spring Boot
- Introducción al Despliegue
- Desplegando en Heroku
- Desplegando en AWS
- Desplegando en Kubernetes
- Integración y Entrega Continua
Módulo 9: Rendimiento y Monitoreo
- Ajuste de Rendimiento
- Caché con Spring Cache
- Monitoreo con Spring Boot Actuator
- Uso de Prometheus y Grafana
- Gestión de Registros y Logs
- Trazabilidad Distribuida
