Las dos lecciones anteriores respondieron a «¿está bien hecha?» desde dos ángulos: qué conviene hacer y qué conviene evitar. Falta el tercero, que no produce ningún error y sin embargo decide el futuro del proyecto: cómo se lee.
Un AlquilerService puede cumplir las treinta y ocho comprobaciones de 10-01, no contener ni uno de los errores de 10-02 y ser aun así un método de doscientas líneas con tres banderas booleanas, variables llamadas data y temp, comentarios que repiten lo que el código ya dice y un dominio que es una bolsa de setters. Nada de eso falla hoy. Lo que falla es dentro de seis meses, cuando el ayuntamiento de Ribalta pida tarifas dinámicas y nadie se atreva a tocar ese método.
Esta lección trata de eso, con un criterio único que atraviesa todos los apartados y que conviene fijar antes de empezar: el destinatario del código no es el compilador, es el próximo que lo lea. Y ese próximo, con frecuencia, eres tú dentro de un año, sin acordarte de nada.
Contenido
- Qué significa código limpio aquí
- Nombres
- Funciones y clases
- Una refactorización completa:
AlquilerService - SOLID en Spring, con ejemplos de Ribalta
- Comentarios
- Manejo de errores
- Inmutabilidad
Optionalbien usado- El modelo de dominio anémico
- Arquitectura verificable con ArchUnit
- Estilo y automatización
- Refactorización segura
- Deuda técnica
- Errores Comunes y Consejos
- Ejercicios
- Qué significa código limpio aquí
«Código limpio» suele confundirse con «código bonito», y no es lo mismo. Un criterio operativo, que se puede aplicar sin discutir de gustos:
| Propiedad | Pregunta que responde | Cómo se comprueba |
|---|---|---|
| Legible | ¿Se entiende qué hace sin ejecutarlo? | Alguien ajeno lo lee y lo explica |
| Localizable | ¿Sé dónde tocar para cambiar X? | Se busca el sitio de un cambio hipotético |
| Modificable | ¿Cambiar una cosa obliga a cambiar cinco? | Se cuenta el radio de un cambio real |
| Comprobable | ¿Puedo escribir una prueba sin acrobacias? | Se intenta escribirla |
La cuarta es la más objetiva y la que más información da. Si para probar un método hay que simular métodos estáticos, instanciar seis colaboradores o manipular el reloj del sistema, el problema no es la prueba: es el diseño. Fue la razón de inyectar un Clock desde 02-01 y de que SeguridadAlquileres sea un bean normal en lugar de una expresión SpEL de tres líneas. Y una advertencia sobre el alcance: nada de esta lección es cuestión de estilo personal —la indentación, las llaves y la longitud de línea se resuelven con una herramienta en el apartado 12 y no se discuten en las revisiones—. Lo que sigue es diseño.
- Nombres
Nombrar es la actividad más frecuente al programar y la que más rendimiento da por minuto invertido.
2.1. La convención del curso
CicloUrbana tiene una regla explícita que conviene enunciar porque no es universal: el dominio se nombra en español —Estacion, Bicicleta, Alquiler, calcularImporte, bicicletasDisponibles— y solo se conserva el inglés en lo que pertenece al framework o a una librería: anotaciones, tipos de Spring, findById, Pageable. El motivo no es patriótico, es de lenguaje ubicuo: el ayuntamiento de Ribalta habla de estaciones, anclajes y tarifas, y cuando el código usa esas mismas palabras, la traducción mental entre la conversación y el programa desaparece. Lo que hay que evitar a toda costa es el híbrido: EstacionService.getEstacionByNombre() obliga a cambiar de idioma dos veces en una línea.
| Elemento | Convención | Ejemplo de CicloUrbana |
|---|---|---|
| Clase | Sustantivo, PascalCase |
AlquilerService, SelectorTarifa |
| Interfaz | Sustantivo o capacidad, sin prefijo I |
CalculadoraTarifa, ValidacionAlquiler |
| Método | Verbo en infinitivo | calcular, buscarConDisponibilidad, esPropietario |
| Booleano | es..., tiene..., puede... |
estaLlena(), puedeAlquilarse() |
| Variable y constante | Sustantivo concreto; MAYUSCULAS_CON_GUION |
anclajesLibres, MINUTOS_GRATIS |
| Paquete | Minúsculas, funcionalidad | com.ciclourbana.alquileres |
| Prueba | Frase que describe la regla | conQuinceMinutosExactosElAlquilerSigueSiendoGratuito |
2.2. Nombres que revelan intención
La diferencia entre un nombre y un buen nombre es si obliga a leer la implementación.
// ❌ Hay que leer el cuerpo para saber qué devuelve
public List<Estacion> getData(int x) { ... }
public List<Estacion> proceso2(boolean b) { ... }
// ✅ El nombre es la documentación
public List<Estacion> buscarConDisponibilidad(int bicicletasMinimas) { ... }
public List<Estacion> buscarOperativasEnHoraPunta() { ... }getData() falla en tres frentes: no dice qué datos, no dice de dónde salen y no dice según qué criterio se filtran. buscarConDisponibilidad(int bicicletasMinimas) responde a los tres, y además el nombre del parámetro convierte el número de la llamada en información: buscarConDisponibilidad(3) se lee «estaciones con al menos tres bicicletas».
Cuatro reglas prácticas: nada de abreviaturas salvo las universalmente conocidas (id, url, http); nada de números en los nombres, porque proceso1 y proceso2 significan que no sabes qué los distingue; el nombre indica el tipo de retorno —lo que empieza por es o puede devuelve boolean, lo que empieza por buscar puede no encontrar y devuelve Optional o lista—; y el mismo concepto, siempre la misma palabra: si es buscar, no es obtener en la clase de al lado.
- Funciones y clases
3.1. Un solo nivel de abstracción
La regla más útil sobre el tamaño de una función no es «menos de veinte líneas», sino: todas las sentencias de un método deben estar al mismo nivel de detalle.
// ❌ Mezcla "qué se hace" con "cómo se hace"
public AlquilerResponse iniciar(IniciarAlquilerRequest peticion) {
Bicicleta bicicleta = bicicletaRepositorio.bloquearMejorDisponible(
peticion.estacionOrigenId(), red.umbralBateria())
.orElseThrow(() -> new BicicletaNoDisponibleException(peticion.estacionOrigenId()));
if (alquilerRepositorio.countByUsuarioIdAndFinIsNull(peticion.usuarioId()) >= 1) {
throw new ReglaNegocioException("ALQUILER_EN_CURSO", "Ya tienes un alquiler abierto");
}
bicicleta.setEstado(EstadoBicicleta.EN_USO);
// ... veinte líneas más al mismo nivel de detalle
}
// ✅ El método público cuenta la historia; los privados la detallan
public AlquilerResponse iniciar(IniciarAlquilerRequest peticion) {
validarQueNoTieneAlquilerAbierto(peticion.usuarioId());
Bicicleta bicicleta = reservarMejorBicicleta(peticion.estacionOrigenId());
Alquiler alquiler = registrarAlquiler(peticion, bicicleta);
eventos.publishEvent(new AlquilerIniciado(alquiler.getId(), instanteActual()));
return alquilerMapper.aRespuesta(alquiler);
}El método público se lee como un párrafo y se entiende en cinco segundos. Quien necesite el detalle baja un nivel; quien solo quiera saber qué ocurre, no baja.
3.2. Argumentos, y por qué las banderas booleanas son un problema
Cero, uno o dos argumentos es lo ideal; tres es aceptable si pertenecen al mismo concepto; con cuatro o más casi siempre falta un objeto que los agrupe.
Una bandera booleana es un argumento que hace que el método haga dos cosas distintas, y tiene tres defectos: en la llamada no se lee nada (finalizar(9L, true) no dice qué es true), obliga a un if dentro que separa dos flujos que ya son dos métodos, y crece: mañana hay dos banderas y cuatro combinaciones, de las cuales dos no tienen sentido.
// ❌ ¿Qué significa ese true en la llamada?
public AlquilerResponse finalizar(Long id, boolean aplicarPenalizacion) { ... }
finalizar(9L, true);
// ✅ Dos métodos con nombre
public AlquilerResponse finalizar(Long id) { ... }
public AlquilerResponse finalizarPorCaducidad(Long id) { ... } // aplica la penalización3.3. Responsabilidad única, aplicada de verdad
«Una clase, una responsabilidad» se cita mucho y se aplica mal, porque «responsabilidad» es vago. La formulación operativa es mejor: una clase debe tener una sola razón para cambiar, es decir, un solo interlocutor que pueda pedir modificarla. Si AlquilerService cambia cuando el ayuntamiento revisa las tarifas, cuando facturación cambia el formato de la factura y cuando el proveedor de correo cambia su API, tiene tres razones para cambiar y tres personas distintas pidiéndolas. El apartado siguiente lo arregla.
- Una refactorización completa:
AlquilerService
AlquilerServicePartimos de una versión real y verosímil de AlquilerService.finalizar: funciona, pasa las pruebas y hace demasiado.
// ❌ Antes: 5 responsabilidades en un método
@Transactional
public AlquilerResponse finalizar(Long id, FinalizarAlquilerRequest peticion) {
Alquiler alquiler = alquilerRepositorio.findById(id).orElseThrow();
if (alquiler.getFin() != null) throw new ConflictoRecursoException("Ya finalizó");
Estacion destino = estacionRepositorio.findById(peticion.estacionDestinoId()).orElseThrow();
if (bicicletaRepositorio.countByEstacionId(destino.getId()) >= destino.getCapacidad()) {
throw new EstacionLlenaException(destino.getId());
}
// Cálculo del importe, reimplementado a mano dentro del servicio
long minutos = Duration.between(alquiler.getInicio(), Instant.now()).toMinutes();
BigDecimal importe;
if (alquiler.getUsuario().getTipoTarifa() == TipoTarifa.ESTUDIANTE) {
importe = new BigDecimal("0.08").multiply(BigDecimal.valueOf(Math.max(0, minutos - 15)));
} else if (alquiler.getUsuario().getTipoTarifa() == TipoTarifa.JUBILADO) {
importe = new BigDecimal("0.05").multiply(BigDecimal.valueOf(Math.max(0, minutos - 30)));
} else {
importe = new BigDecimal("0.50")
.add(new BigDecimal("0.12").multiply(BigDecimal.valueOf(Math.max(1, minutos))));
}
importe = importe.setScale(2, RoundingMode.HALF_UP);
if (minutos > 120) importe = importe.add(new BigDecimal("5.00")); // recargo por exceso
alquiler.setFin(Instant.now()); // mutación a base de setters
alquiler.setEstacionDestino(destino);
alquiler.setImporte(importe);
alquiler.setEstado(EstadoAlquiler.FINALIZADO);
alquiler.getBicicleta().setEstado(EstadoBicicleta.DISPONIBLE);
alquiler.getBicicleta().setEstacion(destino);
correo.enviarResumen(alquiler.getUsuario().getCorreo(), importe); // dentro de la transacción
metricas.registrarFinalizacion(alquiler);
return new AlquilerResponse(alquiler.getId(), /* ... 8 campos más ... */);
}Las cinco responsabilidades son cinco razones distintas para cambiar esta clase, y cada una tiene un interlocutor diferente: validar las reglas de finalización (el ayuntamiento), calcular el importe (el departamento de tarifas), aplicar el recargo por exceso (el ayuntamiento, en otra reunión), mutar el estado del alquiler y la bicicleta (el equipo de dominio) y notificar y medir (marketing y operaciones).
Paso 1: extraer el cálculo de tarifa a donde ya existía
El if/else if/else reimplementa lo que SelectorTarifa y las tres CalculadoraTarifa de 02-02 ya hacían. Es duplicación pura, y peor: una segunda fuente de verdad que puede divergir de la primera. Las quince líneas se convierten en selectorTarifa.paraUsuario(alquiler.getUsuario()).calcular(duracion).
Paso 2: el recargo es una tarifa, no un if
El if (minutos > 120) es una regla de negocio escondida en un servicio. Como el sistema de tarifas ya es extensible por diseño, el recargo encaja como decorador:
package com.ciclourbana.alquileres;
/** Recargo por exceso de duración sobre cualquier tarifa base. Decorador: solo conoce
* el contrato, no la tarifa concreta. */
public record TarifaConRecargoPorExceso(CalculadoraTarifa base, Duration duracionMaxima,
BigDecimal recargo) implements CalculadoraTarifa {
@Override
public BigDecimal calcular(Duration duracion) {
BigDecimal importe = base.calcular(duracion);
return duracion.compareTo(duracionMaxima) > 0 ? importe.add(recargo) : importe;
}
@Override
public String nombre() { return base.nombre() + "-con-recargo"; }
}Ahora el recargo se prueba solo, se combina con cualquier tarifa presente o futura y se configura por propiedades. AlquilerService deja de saber que existe.
Paso 3: el comportamiento, al dominio
Los seis setters consecutivos son el síntoma del modelo anémico del apartado 10. La operación «finalizar un alquiler» pertenece a Alquiler, que es quien conoce sus invariantes:
// En la entidad Alquiler:
/** Cierra el alquiler. Invariante: un alquiler finalizado no vuelve a finalizarse. */
public void finalizar(Estacion destino, Instant fin, BigDecimal importe) {
if (this.estado == EstadoAlquiler.FINALIZADO) {
throw new ConflictoRecursoException("El alquiler " + id + " ya finalizó");
}
this.fin = fin;
this.estacionDestino = destino;
this.importe = importe;
this.estado = EstadoAlquiler.FINALIZADO;
this.bicicleta.anclarEn(destino); // la bicicleta gestiona su propio estado
}Nótese lo que se gana: la comprobación de «ya finalizó» deja de depender de que el servicio se acuerde. Cualquier camino que llame a finalizar la aplica.
Paso 4: los efectos secundarios, fuera de la transacción
El correo y la métrica se sacan a un evento en AFTER_COMMIT, como ya establecimos en Transacciones y Tareas Programadas y Asincronía.
El resultado
// ✅ Después: una responsabilidad y cero reglas de negocio escondidas
@Transactional
public AlquilerResponse finalizar(Long id, FinalizarAlquilerRequest peticion) {
Alquiler alquiler = buscarEnCurso(id);
Estacion destino = buscarEstacionConHueco(peticion.estacionDestinoId());
Instant fin = Instant.now(reloj);
BigDecimal importe = selectorTarifa.paraUsuario(alquiler.getUsuario())
.calcular(Duration.between(alquiler.getInicio(), fin));
alquiler.finalizar(destino, fin, importe); // el dominio protege sus reglas
eventos.publishEvent(new AlquilerFinalizado(alquiler.getId(), importe, fin));
return alquilerMapper.aRespuesta(alquiler);
}| Antes | Después | |
|---|---|---|
| Líneas del método / razones para cambiar | 38 / 5 | 11 / 1 |
| Reglas de negocio en el servicio | 3 | 0 |
| Prueba del recargo | Con contexto y datos | Unitaria de 1 ms |
| Añadir una tarifa nueva | Tocar este método | Una clase nueva |
Y la condición sin la cual nada de esto se hace: las pruebas del módulo 6 estaban en verde antes de empezar y siguieron en verde después de cada paso. Sin ellas, esta refactorización es una reescritura a ciegas.
- SOLID en Spring, con ejemplos de Ribalta
| Principio | Qué dice | Dónde se ve en CicloUrbana |
|---|---|---|
| SRP — responsabilidad única | Una sola razón para cambiar | La refactorización del apartado 4; SeguridadAlquileres separado de AlquilerService |
| OCP — abierto/cerrado | Abierto a extensión, cerrado a modificación | CalculadoraTarifa: añadir TarifaJubilado no toca SelectorTarifa, que las inyecta en una List |
| LSP — sustitución de Liskov | Un subtipo debe poder sustituir al tipo base sin sorpresas | Cualquier CalculadoraTarifa debe devolver un importe no negativo; una que lanzara excepción con duración cero rompería a todos sus consumidores |
| ISP — segregación de interfaces | Mejor varias interfaces pequeñas que una grande | Los repositorios: EstacionRepositorio expone lo de estaciones y nada más; y Pageable/Sort en lugar de un método con ocho parámetros |
| DIP — inversión de dependencias | Depender de abstracciones, no de implementaciones | Es literalmente lo que hace el contenedor: AlquilerService declara CalculadoraTarifa y Spring decide cuál inyectar |
Dos malentendidos que conviene despejar. DIP no significa «una interfaz por clase»: significa que la dependencia apunta hacia la abstracción cuando hay una frontera real. AlquilerService depende de CalculadoraTarifa porque hay tres implementaciones y habrá más; una IAlquilerService con una sola implementación es ceremonia sin beneficio. Y LSP es el más ignorado y el que produce fallos más raros: su incumplimiento típico no es de herencia, sino de contrato —una implementación que devuelve null donde las demás devuelven lista vacía, o que lanza excepción donde las demás devuelven cero—, y el consumidor, escrito contra el comportamiento de la primera, se rompe con la segunda.
- Comentarios
Un comentario es una deuda de mantenimiento: no lo comprueba el compilador, no lo ejecuta ninguna prueba y envejece en silencio. Por eso el criterio es exigente.
| Comentario | ¿Merece la pena? |
|---|---|
| Repite lo que el código dice | No. // incrementa i sobre i++ |
| Explica qué hace un método largo | No. Extrae un método con nombre |
| Explica por qué se tomó una decisión no obvia | Sí. El más valioso |
| Documenta una restricción externa | Sí. «El proveedor limita a 100 peticiones/min» |
| Advierte de una trampa | Sí. «No convertir en private: se pierde el proxy» |
Código comentado, o // TODO sin fecha ni responsable |
No. Para eso está el control de versiones y el gestor de tareas |
// ❌ Ruido: dice lo que ya se lee
// Comprobamos si la estación está llena
if (bicicletas.size() >= estacion.getCapacidad()) { ... }
// ✅ Explica lo que el código no puede decir
// El ayuntamiento exige dejar siempre un anclaje libre para el furgón de
// mantenimiento (convenio de 2026), de ahí el -1 y no la capacidad completa.
if (bicicletas.size() >= estacion.getCapacidad() - 1) { ... }El javadoc que merece la pena es el de las fronteras: interfaces públicas, contratos de dominio y cualquier método cuyo comportamiento no sea evidente en los casos límite —CalculadoraTarifa.calcular documenta qué ocurre con duración cero, porque eso no está en la firma—. En cambio, un /** Devuelve el nombre. @return el nombre */ sobre getNombre() solo añade líneas.
- Manejo de errores
Excepciones específicas del dominio, no genéricas. throw new RuntimeException("Error") obliga a quien la captura a leer el mensaje para saber qué pasó. La jerarquía de 03-06 —CicloUrbanaException con RecursoNoEncontradoException, ConflictoRecursoException, EstacionLlenaException y BicicletaNoDisponibleException— permite que el @RestControllerAdvice traduzca cada una a su código HTTP sin un solo if sobre cadenas.
No uses excepciones para el flujo normal. «No hay bicicletas disponibles» en una estación concurrida no es excepcional: pasa cien veces al día. Una excepción cuesta construir la traza de pila y, sobre todo, comunica algo equivocado al lector; si el caso es esperable, devuelve Optional o un tipo resultado. Y no captures Exception: atrapa también lo que no sabes manejar y lo convierte en un log. Captura lo que puedes tratar, y deja subir lo demás.
// ❌ Traga todo, incluidos los fallos que deberían llegar al manejador global
try { ... } catch (Exception e) { log.error("Error", e); }
// ✅ Trata lo previsto, relanza como excepción del dominio
try {
pasarelaPago.cobrar(importe);
} catch (PagoRechazadoException e) {
throw new ReglaNegocioException("PAGO_RECHAZADO", "El pago ha sido rechazado", e);
}Mensajes útiles. "Error" no sirve para nada; "La estación 3 está llena: 18 bicicletas para 18 anclajes" dice qué pasó, con qué datos, y permite reproducirlo. Con una condición que no se puede olvidar: el mensaje que va al cliente y el que va al log no son el mismo. Al log, el detalle; al cliente, la versión sin información interna.
- Inmutabilidad
Un objeto inmutable no puede estar en un estado inválido después de construirse, es seguro entre hilos sin razonar nada y no puede cambiar entre el momento en que se valida y el momento en que se usa —una fuente real de vulnerabilidades—.
record para DTOs y objetos de valor. Todos los DTOs de CicloUrbana lo son, con el constructor compacto como sitio natural para normalizar; y también los objetos de valor del dominio, como UbicacionResponse. Colecciones inmutables en los retornos, porque devolver la lista interna permite que el llamante la modifique por la espalda:
// ❌ El llamante puede hacer estacion.getBicicletas().clear()
public List<Bicicleta> getBicicletas() { return bicicletas; }
// ✅ Vista de solo lectura; las modificaciones pasan por métodos con nombre
public List<Bicicleta> getBicicletas() { return List.copyOf(bicicletas); }
public void anclar(Bicicleta bicicleta) { /* valida capacidad y añade */ }Y el límite honesto: las entidades JPA no pueden ser inmutables. Hibernate necesita un constructor sin argumentos y modifica el estado al cargar y al aplicar cambios. La respuesta no es forzar la inmutabilidad, sino controlar la mutación: campos privados, sin setters públicos indiscriminados, y métodos con nombre de negocio —finalizar, anclarEn, marcarAveriada— que son los únicos que cambian el estado y que pueden proteger los invariantes.
Optional bien usado
Optional bien usadoOptional se introdujo para un caso concreto —el retorno de un método que puede no encontrar nada— y se usa con frecuencia para tres más, donde estorba.
| Uso | ¿Correcto? | Por qué |
|---|---|---|
| Tipo de retorno | Sí | Es su propósito: obliga al llamante a decidir qué hace si no hay valor |
| Parámetro de método | No | El llamante tiene que envolver; y hay tres estados posibles: null, vacío y presente |
| Campo de una clase | No | No es serializable, ocupa memoria extra y complica el enlace de Jackson y JPA |
| Colección vacía | No | Una lista vacía ya expresa «no hay nada»; Optional<List<T>> es redundante |
Y el antipatrón más frecuente de todos: .get() sin comprobar, que convierte un caso previsto en una NoSuchElementException con 500 y traza en el log.
// ❌ Un 500 en lugar de un 404
Estacion estacion = estacionRepositorio.findById(id).get();
// ✅ La ausencia se traduce a una excepción del dominio
Estacion estacion = estacionRepositorio.findById(id)
.orElseThrow(() -> new RecursoNoEncontradoException("Estación", id));orElseThrow, orElseGet, map, filter e ifPresent cubren prácticamente todo. Un detalle que se pasa por alto: orElse evalúa siempre su argumento, incluso cuando hay valor, así que si construir la alternativa es caro debe ser orElseGet con un proveedor perezoso.
- El modelo de dominio anémico
Un modelo anémico es aquel en el que las entidades solo tienen datos —campos, getters y setters— y toda la lógica vive en los servicios. Es el estilo por defecto de la mayoría de los proyectos Spring, y merece una discusión honesta porque no siempre está mal.
El caso a favor de poner comportamiento en el dominio. Compara las dos formas de finalizar un alquiler:
// ❌ El servicio manipula el estado con setters: los invariantes viven en el servicio
alquiler.setFin(fin);
alquiler.setEstacionDestino(destino);
alquiler.setImporte(importe);
alquiler.setEstado(EstadoAlquiler.FINALIZADO);
// ✅ La entidad protege sus propias reglas
alquiler.finalizar(destino, fin, importe);Cuatro diferencias que importan. El invariante viaja con el dato: la comprobación de «ya finalizó» se aplica por cualquier camino, no solo por el que se acordó de escribirla. La entidad no puede quedar a medias: con setters públicos, un método puede fijar el importe y olvidar el estado, y nada lo impide. El nombre está en el lenguaje del ayuntamiento, no en el de la base de datos. Y se puede probar sin nada: Alquiler.finalizar es una prueba unitaria de un milisegundo.
El caso a favor del modelo anémico existe y no conviene despreciarlo:
| Situación | Por qué el anémico es aceptable |
|---|---|
| CRUD sin reglas | Si «actualizar la dirección de una estación» es asignar un campo, envolverlo no añade nada |
| La lógica necesita colaboradores | Calcular el importe requiere SelectorTarifa; una entidad JPA no debería inyectar servicios |
| La regla cruza varios agregados | «El usuario no puede tener dos alquileres abiertos» no cabe dentro de Alquiler |
| Equipo sin experiencia en DDD | Un dominio rico mal hecho —entidades con repositorios dentro— es peor que uno anémico |
El criterio práctico de CicloUrbana, que es un punto intermedio defendible: las reglas que dependen solo del estado del propio agregado van en la entidad —finalizar, puedeAlquilarse, estaLlena, anclarEn—; las que necesitan colaboradores o cruzan agregados van en el servicio —seleccionar la tarifa, verificar que no hay otro alquiler abierto, comprobar permisos—. Con esa división, Alquiler no conoce ningún repositorio y AlquilerService no manipula estado a base de setters.
- Arquitectura verificable con ArchUnit
Las reglas de dependencia de 10-01 —el controlador no toca el repositorio, el dominio no importa el framework— son acuerdos que se incumplen tarde o temprano si dependen de que alguien lo note en una revisión. ArchUnit las convierte en pruebas.
Con la dependencia com.tngtech.archunit:archunit-junit5:1.3.0 en ámbito test, las reglas se escriben como campos estáticos anotados con @ArchTest:
package com.ciclourbana;
import com.tngtech.archunit.junit.*;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
@AnalyzeClasses(packages = "com.ciclourbana",
importOptions = ImportOption.DoNotIncludeTests.class)
class ReglasArquitecturaTest {
/** Regla 1: los controladores hablan con servicios, nunca con repositorios. */
@ArchTest
static final ArchRule losControladoresNoAccedenARepositorios =
noClasses().that().haveSimpleNameEndingWith("Controller")
.should().dependOnClassesThat().haveSimpleNameEndingWith("Repositorio")
.because("el controlador traduce HTTP; las reglas viven en el servicio");
/** Regla 2: las entidades no salen del paquete de su agregado. */
@ArchTest
static final ArchRule lasEntidadesNoSalenDeSuPaquete =
classes().that().areAnnotatedWith(jakarta.persistence.Entity.class)
.should().onlyBeAccessed().byClassesThat()
.resideInAnyPackage("com.ciclourbana.(**)", "com.ciclourbana.comun..")
.because("el contrato público se expresa con DTOs, no con entidades");
/** Regla 3: nada del dominio conoce la capa web. */
@ArchTest
static final ArchRule elDominioNoConoceElServlet =
noClasses().that().resideInAPackage("..alquileres..")
.and().haveSimpleNameNotEndingWith("Controller")
.should().dependOnClassesThat().resideInAPackage("jakarta.servlet..")
.because("la lógica de alquileres no depende de que la entrada sea HTTP");
/** Regla 4: sin ciclos entre los módulos de negocio. */
@ArchTest
static final ArchRule sinCiclosEntreModulos =
slices().matching("com.ciclourbana.(*)..").should().beFreeOfCycles();
/** Regla 5: la trampa del proxy de 10-02, convertida en prueba. */
@ArchTest
static final ArchRule lasAnotacionesDeProxyVanEnMetodosPublicos =
methods().that().areAnnotatedWith(Transactional.class).should().bePublic()
.because("CGLIB no intercepta métodos no públicos");
}Cinco reglas, un fichero, y a partir de ese momento ./mvnw test se pone en rojo el día que alguien las incumpla, con un mensaje que dice qué clase, qué dependencia y —gracias al because— por qué está prohibida. Es la diferencia entre una convención documentada y una convención que se cumple. Tres consejos para que la inversión no se vuelva en contra: empieza por tres o cuatro reglas, las que de verdad duelen si se rompen; escribe siempre el because, porque el mensaje de fallo es lo único que verá quien la incumpla dentro de un año; y si una regla necesita excepciones, decláralas explícitamente en lugar de borrar la regla entera.
- Estilo y automatización
El estilo —dónde van las llaves, cuántos espacios, el orden de los import, la longitud de línea— es la discusión con peor relación entre tiempo invertido y valor obtenido de toda la ingeniería de software. La solución no es acordar un estilo: es delegarlo en una herramienta.
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.44.0</version>
<configuration>
<java>
<palantirJavaFormat/> <!-- formateo determinista -->
<removeUnusedImports/>
<importOrder><order>java,javax,jakarta,org,com,</order></importOrder>
<trimTrailingWhitespace/>
<endWithNewline/>
</java>
</configuration>
<!-- Enganchado a la fase validate: falla si algo no está formateado -->
<executions><execution><phase>validate</phase>
<goals><goal>check</goal></goals></execution></executions>
</plugin>./mvnw spotless:apply formatea todo el proyecto y ./mvnw spotless:check falla si algo no lo está, lo que ejecuta la canalización de 08-05 en cada cambio.
| Herramienta | Qué comprueba | Cuándo se ejecuta |
|---|---|---|
| Spotless | Formato: espacios, import, saltos |
En validate, y con apply en local |
| Checkstyle | Convenciones: nombres, tamaño de método, javadoc | En CI |
| SpotBugs / Error Prone | Errores probables: equals incoherente, comparaciones sospechosas |
En CI |
| SonarQube | Análisis global, deuda, duplicación, cobertura | Nocturno o por pull request |
| ArchUnit | Reglas de arquitectura (apartado 11) | Con las pruebas |
Por qué el estilo no se discute en las revisiones. Un comentario que dice «faltan dos espacios» consume la atención que debería ir a la lógica, genera fricción y no aporta nada que una herramienta no haga gratis. Con Spotless en validate, el código llega ya formateado y la revisión se ocupa de lo que ninguna herramienta detecta: si el diseño es correcto, si el nombre revela la intención y si falta un caso límite. Y el orden importa al adoptarlo: primero se aplica el formateador a todo el proyecto en un commit dedicado, sin ningún cambio funcional, y solo después se activa la comprobación; mezclar reformateo y lógica produce diferencias irrevisables.
- Refactorización segura
Refactorizar es cambiar la estructura interna sin cambiar el comportamiento observable, y la definición contiene su propio requisito: para saber que el comportamiento no ha cambiado, hay que poder comprobarlo.
flowchart LR
A["Pruebas en verde<br/>ANTES de tocar nada"] --> B["Un cambio pequeño<br/>y reversible"]
B --> C["Pruebas en verde"]
C -->|"Sí"| D["Commit"]
C -->|"No"| E["Deshacer y<br/>hacerlo más pequeño"]
D --> B
Las cuatro reglas, en orden de importancia:
- Si no hay pruebas, la primera tarea es escribirlas. No las de todo el sistema: las del comportamiento que vas a mover. Se llaman pruebas de caracterización y afirman lo que el código hace hoy, incluidas sus rarezas. Sin ellas no estás refactorizando, estás reescribiendo.
- Un paso cada vez, con las pruebas entre medias. La refactorización del apartado 4 fueron cuatro pasos, cada uno con la suite en verde. Si el paso 3 rompiera algo, sabrías exactamente cuál fue.
- No mezcles refactorización y funcionalidad en el mismo commit. Un cambio que mueve doscientas líneas y además arregla un fallo es irrevisable: nadie puede distinguir el movimiento del cambio.
- Confía en el IDE para lo mecánico. Renombrar, extraer método o clase, cambiar firma e introducir parámetro son transformaciones que el IDE hace sin equivocarse; hacerlas con buscar-y-reemplazar es donde aparecen los errores.
Cuándo refactorizar. La respuesta sostenible no es «reservamos un sprint»: es la regla del campamento, dejar el código un poco mejor de como lo encontraste cada vez que lo tocas por otro motivo. Un sprint de refactorización compite con funcionalidad y siempre pierde; una mejora de diez minutos dentro de una tarea que ya estabas haciendo, no compite con nada.
- Deuda técnica
La metáfora es exacta y por eso funciona: tomas prestado tiempo hoy y pagas intereses cada vez que tocas ese código. Como la financiera, hay deuda razonable y deuda ruinosa.
| Tipo | Ejemplo en CicloUrbana | ¿Aceptable? |
|---|---|---|
| Deliberada y prudente | «Salimos con mapeo manual; migraremos a MapStruct al llegar a 20 DTOs» | Sí, si está registrada |
| Deliberada e imprudente | «No hay tiempo para pruebas» | No: los intereses son inmediatos |
| Accidental y prudente | «Ahora entendemos que el mapeador no debería consultar repositorios» | Inevitable y sana: es aprendizaje |
| Accidental e imprudente | Nadie sabía que @Transactional no funciona en autoinvocación |
Formación, revisión y las reglas del apartado 11 |
Cómo se reconoce. Cuatro señales: una estimación que crece sin que el requisito crezca; un fichero que aparece en el 80 % de los commits; una parte del código que «solo toca fulano»; y la frase «no lo toques, que funciona», reconocimiento explícito de que nadie lo entiende.
Cómo se registra. Un // TODO sin fecha ni responsable es decoración: a los seis meses hay ciento cuarenta y nadie los lee. Lo que sí funciona:
// DEUDA-2026-03: el mapeador consulta repositorios y provoca N+1 en listados.
// Impacto: p95 de GET /api/v1/alquileres. Salida: que el servicio devuelva
// un agregado con los nombres ya resueltos. Estimado: 1 día. Ficha: CU-412.La diferencia con un TODO es que tiene destinatario, coste e impacto, que es la información necesaria para priorizarla frente a una funcionalidad; y va acompañada de una ficha en el mismo sistema donde vive el resto del trabajo, porque una deuda que solo existe en el código nunca se planifica.
Cuándo se paga. Tres momentos, y ninguno es «cuando haya tiempo»: cuando vas a tocar esa zona por otro motivo —el interés se paga solo—; cuando bloquea algo que el ayuntamiento sí quiere; y cuando el coste de los intereses supera el de la amortización, lo que se detecta midiendo, no discutiendo. La deuda que no molesta a nadie puede quedarse: no todo lo mejorable merece ser mejorado.
Errores Comunes y Consejos
Confundir código limpio con código «elegante». Una cadena de cinco stream() anidados puede ser muy ingeniosa e ilegible. Si hay que leerla tres veces, un bucle con nombres claros es mejor código.
Refactorizar sin pruebas. Es reescribir con otro nombre. Si no hay pruebas, escríbelas antes: primero las de caracterización, después el cambio. Y aplicar SOLID como ritual —una interfaz por clase, un mapeador por DTO, una fábrica por servicio— no es diseño: es ceremonia. Cada abstracción tiene que estar pagando algo concreto.
Comentar lo que se puede nombrar. Si necesitas un comentario para explicar qué hace un bloque, casi siempre lo que necesitas es extraer ese bloque a un método con ese nombre. Y dejar código comentado «por si acaso» solo genera dudas sobre si debería estar activo: está en el control de versiones.
Confundir el modelo rico con meter repositorios en las entidades. Una entidad que inyecta un repositorio es peor que el modelo anémico: mezcla persistencia y dominio y hace imposible probarla aisladamente.
Consejo: la mejor prueba de legibilidad es leerlo en voz alta. Si alquiler.finalizar(destino, fin, importe) se lee como una frase del ayuntamiento y procesar(a, true, 2) no, ya tienes la respuesta sin discutir de estilo.
Consejo: escribe el código pensando en quien lo borrará. El código fácil de borrar —fronteras claras, pocas dependencias entrantes— es el mismo que es fácil de cambiar: si eliminar TarifaJubilado es borrar una clase, el diseño es bueno.
Consejo: la revisión de código se ocupa de lo que ninguna herramienta ve. Spotless mira el formato, ArchUnit las dependencias, SpotBugs los errores probables y JaCoCo la cobertura. Lo que queda para las personas es si el nombre revela la intención, si la abstracción es la correcta y si falta un caso límite. Eso es exactamente donde una revisión aporta valor.
Ejercicios
Ejercicio 1: limpiar un servicio de incidencias
Refactoriza esta clase aplicando lo visto: nombres, nivel de abstracción, banderas booleanas, Optional, manejo de errores y modelo de dominio. Justifica cada cambio.
@Service
public class IncService {
@Transactional
public Object proc(Long id, boolean cerrar, boolean notificar) throws Exception {
Incidencia i = repo.findById(id).get();
if (cerrar) {
i.setEstado("CERRADA");
i.setFechaCierre(new Date());
if (i.getTipo().equals("BATERIA")) {
i.getBicicleta().setEstado("DISPONIBLE");
i.getBicicleta().setNivelBateria(100);
}
} else {
i.setEstado("ABIERTA");
}
repo.save(i);
if (notificar) {
try { correo.enviar(i.getAutor().getCorreo(), "Incidencia " + id); }
catch (Exception e) { }
}
return i;
}
}Ejercicio 2: reglas de ArchUnit para Ribalta
Escribe cuatro reglas de ArchUnit que protejan decisiones tomadas a lo largo del curso, distintas de las cinco del apartado 11. Para cada una, indica qué decisión protege, en qué lección se tomó y qué mensaje daría al fallar.
Ejercicio 3: ¿anémico o rico?
Para cada una de estas seis reglas de negocio de CicloUrbana, decide si debe vivir en la entidad o en el servicio, y justifícalo con el criterio del apartado 10: (1) una bicicleta con menos del 20 % de batería no puede alquilarse; (2) un usuario no puede tener dos alquileres abiertos a la vez; (3) una estación está llena cuando sus bicicletas ancladas igualan su capacidad; (4) el importe depende del tipo de tarifa del usuario; (5) un alquiler finalizado no puede volver a finalizarse; (6) solo un operario puede marcar una bicicleta como averiada.
Soluciones
Solución 1
@Service
@Transactional(readOnly = true)
public class IncidenciaService { // constructor omitido: repositorio, eventos, mapper, reloj
/** Cierra la incidencia y devuelve la bicicleta al servicio si procede. */
@Transactional
public IncidenciaResponse cerrar(Long idIncidencia, String resolucion) {
Incidencia incidencia = buscar(idIncidencia);
incidencia.cerrar(resolucion, Instant.now(reloj)); // el dominio decide
eventos.publishEvent(new IncidenciaCerrada(idIncidencia,
incidencia.getAutor().getCorreo()));
return incidenciaMapper.aRespuesta(incidencia);
}
/** Reabre una incidencia cerrada por error. Método aparte, no una bandera. */
@Transactional
public IncidenciaResponse reabrir(Long idIncidencia, String motivo) { ... }
private Incidencia buscar(Long id) {
return incidenciaRepositorio.findById(id)
.orElseThrow(() -> new RecursoNoEncontradoException("Incidencia", id));
}
}
// En la entidad Incidencia:
/** Cierra la incidencia. Si era de batería, la bicicleta vuelve al servicio. */
public void cerrar(String resolucion, Instant momento) {
if (this.estado == EstadoIncidencia.CERRADA) {
throw new ConflictoRecursoException("La incidencia " + id + " ya estaba cerrada");
}
this.estado = EstadoIncidencia.CERRADA;
this.fechaCierre = momento;
this.resolucion = resolucion;
if (this instanceof IncidenciaBateria) {
bicicleta.devolverAlServicio(); // la bicicleta gestiona su estado
}
}Los diez cambios, justificados.
| # | Cambio | Motivo |
|---|---|---|
| 1 | IncService → IncidenciaService, proc → cerrar/reabrir |
Un nombre abreviado no ahorra nada y cuesta una consulta al lector |
| 2 | Las dos banderas booleanas pasan a ser dos métodos | proc(9L, true, false) no se lee; y de las cuatro combinaciones, dos no tenían sentido |
| 3 | Object → IncidenciaResponse |
Object renuncia al tipado; devolver la entidad sería la fuga de 03-05 |
| 4 | throws Exception desaparece; .get() → orElseThrow |
La jerarquía del dominio hereda de RuntimeException y provoca rollback; y la ausencia da un 404, no un 500 |
| 5 | String → enum en estado y tipo |
equals("BATERIA") falla en silencio con un error tipográfico; el enum no compila |
| 6 | new Date() → Instant.now(reloj) |
Determinismo: la prueba controla el tiempo con Clock.fixed |
| 7 | repo.save(i) desaparece |
La entidad está gestionada; el dirty checking genera el UPDATE |
| 8 | El correo pasa a un evento en AFTER_COMMIT |
No retiene la conexión durante la llamada de red y no anuncia lo que puede deshacerse |
| 9 | El catch (Exception e) { } vacío desaparece |
Es el peor fragmento de la clase: descarta el fallo sin dejar rastro |
Y el cambio de fondo: la lógica de cierre se ha movido a Incidencia, donde el invariante «no se cierra dos veces» se aplica por cualquier camino.
Solución 2
/** Decisión (03-05, 04-07): el servicio devuelve DTOs, nunca entidades. */
@ArchTest
static final ArchRule losServiciosNoDevuelvenEntidades =
noMethods().that().areDeclaredInClassesThat().haveSimpleNameEndingWith("Service")
.and().arePublic()
.should().haveRawReturnType(describe("una entidad JPA",
c -> c.isAnnotatedWith(jakarta.persistence.Entity.class)))
.because("con open-in-view: false provoca LazyInitializationException");
/** Decisión (03-05): los DTOs son record inmutables. */
@ArchTest
static final ArchRule losDtosSonRecord =
classes().that().resideInAPackage("..dto..").should().beRecords()
.because("un DTO mutable puede cambiar entre validarse y usarse");
/** Decisión (09-05): el log lo gobierna Logback, nunca System.out. */
@ArchTest
static final ArchRule sinSalidaEstandarDirecta =
noClasses().should().accessField(System.class, "out")
.because("un println no lleva traceId, ni nivel, ni formato JSON");
/** Decisión (02-02): inyección por constructor con campos final. */
@ArchTest
static final ArchRule sinInyeccionPorCampo =
noFields().should().beAnnotatedWith(Autowired.class)
.because("impide final y obliga a reflexión para construir en una prueba");Por qué estas cuatro y no otras: las cuatro protegen decisiones que se incumplen por descuido y no producen ningún error inmediato. Una regla de ArchUnit sobre algo que ya falla al compilar no aporta nada; su valor está exactamente en los acuerdos silenciosos. Y el because no es decorativo: es lo único que verá dentro de dos años quien la incumpla, así que conviene incluir la razón concreta o la lección, no un «está prohibido».
Solución 3
| # | Regla | Dónde vive | Justificación |
|---|---|---|---|
| 1 | Batería mínima para alquilar | Entidad Bicicleta.puedeAlquilarse(umbral) |
Depende solo del estado propio. El umbral entra como parámetro desde RedProperties, así que la entidad no necesita conocer la configuración |
| 2 | Un solo alquiler abierto por usuario | Servicio | Cruza agregados y necesita consultar el repositorio: Alquiler no puede saber cuántos otros alquileres existen |
| 3 | Estación llena | Entidad Estacion.estaLlena() |
Comparación entre dos datos del propio agregado. Es el caso más claro de los seis |
| 4 | Importe según tarifa | Servicio | Requiere el colaborador SelectorTarifa. Meterlo en Alquiler obligaría a inyectar un servicio en una entidad, que es peor que el modelo anémico |
| 5 | No finalizar dos veces | Entidad Alquiler.finalizar(...) |
Es un invariante del agregado, y ponerlo en la entidad garantiza que ningún camino pueda saltárselo |
| 6 | Solo un operario marca averiada | Servicio, con @PreAuthorize |
Es autorización, no dominio. Depende del usuario autenticado, un concepto ajeno a Bicicleta |
El patrón que emerge, y que es la respuesta del ejercicio: la regla vive en la entidad cuando se puede evaluar con lo que la entidad ya tiene delante. En cuanto hace falta consultar otra cosa —otro agregado, un servicio, el usuario autenticado— sube al servicio. El caso 1 suele generar discusión, porque podría argumentarse que el umbral es configuración y por tanto la regla es del servicio; pasarlo como argumento mantiene la entidad limpia y la regla junto al dato. Cuando dudes, la pregunta útil es: ¿puedo probar esta regla con un new y nada más? Si la respuesta es sí, puede vivir en la entidad.
Conclusión
El código de CicloUrbana ya no solo funciona y evita los errores conocidos: se puede leer. Y el criterio que lo gobierna todo es uno solo, el que abría la lección: el destinatario no es el compilador, es el próximo que lo lea, con la propiedad más objetiva —la comprobabilidad— como termómetro: si probar algo exige acrobacias, el problema es el diseño.
Sabes nombrar con la convención del curso —dominio en español, framework en inglés, sin híbridos— y distinguir getData() de buscarConDisponibilidad(int bicicletasMinimas), que responde a qué, de dónde y según qué criterio. Escribes funciones con un solo nivel de abstracción, donde el método público cuenta la historia y los privados la detallan, y sabes por qué una bandera booleana es en realidad dos métodos que aún no se han separado. Has visto la refactorización completa de AlquilerService.finalizar en cuatro pasos —extraer el cálculo a SelectorTarifa, convertir el recargo en el decorador TarifaConRecargoPorExceso, mover el comportamiento a Alquiler.finalizar(...) y sacar los efectos secundarios al AFTER_COMMIT—, de treinta y ocho líneas y cinco razones para cambiar a once líneas y una, con la condición innegociable de tener las pruebas en verde antes y después de cada paso.
Tienes SOLID aterrizado en Ribalta con sus dos malentendidos aclarados —DIP no es una interfaz por clase, y LSP se incumple casi siempre por contrato y no por herencia—; el criterio para los comentarios, que documentan el porqué y nunca el qué; el manejo de errores con excepciones del dominio, sin usarlas para el flujo normal, sin capturar Exception y con mensajes distintos para el log y para el cliente; la inmutabilidad con record y colecciones de solo lectura, y su límite honesto en las entidades JPA, donde la respuesta no es la inmutabilidad sino la mutación controlada por métodos con nombre de negocio; y Optional en su único sitio correcto, el retorno, con orElseThrow sustituyendo al .get() que convierte un 404 en un 500. Y tienes la discusión honesta sobre el modelo anémico, con el criterio de CicloUrbana como punto intermedio defendible: las reglas que dependen solo del estado del propio agregado van en la entidad, las que necesitan colaboradores o cruzan agregados van en el servicio.
Cierran la lección las tres piezas que convierten todo lo anterior en algo que se sostiene solo: ArchUnit, que transforma las reglas de dependencia en pruebas que se ponen en rojo con un mensaje que explica el porqué; Spotless, Checkstyle y el análisis estático, que sacan el estilo de las revisiones para que estas se ocupen de lo que ninguna herramienta ve; y la refactorización segura apoyada en el módulo 6, con la regla del campamento como forma sostenible de mejorar y con la deuda técnica registrada con destinatario, coste e impacto en lugar de con TODO decorativos —y con el permiso explícito de dejar sin pagar la deuda que no molesta a nadie—.
Con esto se cierra la reflexión sobre cómo está hecha CicloUrbana. Durante diez módulos hemos construido la red de Ribalta capa por capa y nunca la hemos mirado entera. La lección Proyecto Final: Recorrido Completo de CicloUrbana hace exactamente eso: la arquitectura final en un diagrama, la estructura completa del repositorio, el recorrido de un POST /api/v1/alquileres paso a paso desde el balanceador hasta la métrica —citando en cada paso la lección donde 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 el application.yml finales comentados, cómo poner en marcha el proyecto entero desde cero, y hacia dónde seguir ampliándolo.
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
