← Proyecto del primer trimestre

Sesión 27 · Semana 14

Cerrar la primera versión del proyecto elegido

Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado la defensa del proceso. En Servidor continúas la implementación del mismo producto.

Se explica

25 minutos · explicación y demostración

El primer trimestre termina con el mismo proyecto que elegiste al comienzo. Hoy usarás una matriz de aceptación: cada requisito debe tener una comprobación que demuestre si se cumple. El taller se dedica a cerrar carencias de esa versión, sin iniciar otro CRUD.

De requisitos informales a criterios de aceptación (Gherkin)

Para que una tarea esté «terminada» (Definition of Done), su comportamiento debe especificarse mediante criterios de aceptación objetivos. Utilizaremos el formato Dado / Cuando / Entonces (Given / When / Then):

Escenario: Intento de asociar tarea a un proyecto inexistente
  Dado que no existe ningún proyecto con id 999 en PostgreSQL
  Cuando el cliente envía una petición POST /tareas con cuerpo {"titulo": "Fix", "prioridad": "alta", "proyectoId": 999}
  Entonces el servidor responde con código de estado HTTP 404 Not Found
  Y el cuerpo JSON contiene {"title": "Not Found", "status": 404, "detail": "No existe proyecto con id 999"}
  Y la consola SQL demuestra que no se ejecutó ninguna sentencia INSERT en la tabla tareas
Escenario: Alta de proyecto con nombre duplicado
  Dado que ya existe un proyecto registrado con nombre "Portal Web"
  Cuando el cliente envía POST /proyectos con nombre "Portal Web"
  Entonces el servidor responde con código HTTP 409 Conflict
  Y el cuerpo JSON detalla la regla de negocio violada

La trampa del corte horizontal frente al corte vertical

El error más destructivo cuando el plazo es corto es organizar el trabajo por «capas horizontales»:

  • Primer tramo: escribir todas las clases @Entity.
  • Segundo tramo: escribir todas las interfaces JpaRepository.
  • Tercer tramo: escribir todos los servicios.
  • Último tramo: escribir los controladores e intentar arrancar por primera vez.

¿Qué ocurre el jueves por la tarde? La aplicación arroja 35 errores en cascada: tipos de datos incompatibles, dependencias circulares, mapeos erróneos de Hibernate y excepciones PropertyReferenceException. Al haberse modificado 40 archivos en una sola operación, resulta imposible determinar qué línea originó el fallo.

En ingeniería de software profesional utilizamos cortes verticales (Vertical Slices o Tracer Bullets):

Desarrollo por corte vertical (un caso de uso completo cada vez)
  1. 1. Entidad y Repositorio
  2. 2. Test de persistencia (@DataJpaTest)
  3. 3. Servicio y Reglas de Negocio
  4. 4. DTOs y Controlador REST
  5. 5. Validación en Bruno/Postman
  1. Coges la primera historia del backlog: «Alta de Proyecto con validación de nombre único».
  2. Creas su entidad, su repositorio y su test @DataJpaTest. Ejecutas ./mvnw testVERDE.
  3. Creas su servicio con la regla de negocio y su excepción.
  4. Creas sus DTOs, su controlador y su manejador de errores.
  5. Ejecutas la petición en Bruno/Postman → 201 Created.
  6. Haces commit: git commit -m "feat(proyectos): implementar alta con validacion de unicidad".

Ahora tu aplicación ya hace algo real, está probada, compila y no se romperá al avanzar.

La regla del semáforo en verde

Bajo ningún concepto se empieza un nuevo caso de uso si la suite de tests existente no está completamente en verde.

Si introduces un cambio y un test previo falla, detente inmediatamente. Arregla el fallo antes de escribir una sola línea de la siguiente funcionalidad. Arrastrar errores multiplica el coste de solución por diez.

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre la propuesta del README, las pruebas, la colección y la versión desplegada. Anota el commit que estás revisando.
  2. Relaciona cada criterio del trimestre con una ruta, test o consulta que lo demuestre. Marca cumple, falla o pendiente con una evidencia concreta.
  3. Ordena los fallos por impacto y elige primero los que impiden ejecutar el flujo principal, conservar datos o respetar reglas.

Paso 2 · El coste del código sin contrato previo

El impulso más común del programador inexperto cuando recibe un proyecto es abrir el IDE y empezar a picar clases Java: un controlador por aquí, una entidad por allá, un método que se le acaba de ocurrir sobre la marcha.

Al cabo de cuatro horas, la catástrofe es inevitable:

  • El endpoint POST /tareas espera un JSON con nombres de campos distintos a los que diseñó el compañero que hace las pruebas.
  • La base de datos tiene una columna que no admite nulos, pero el DTO no lleva @NotNull, provocando errores 500 incomprensibles.
  • Las rutas no siguen una convención REST consistente: unas usan plural (/proyectos), otras singular (/tarea) y otras verbos (/crear-usuario).
  • La mitad del código escrito hay que refactorizarlo o tirarlo a la basura.

La ley de la especificación previa

En ingeniería de software profesional, el contrato de la API y el modelo relacional se pactan y documentan antes de abrir el IDE.

El código es barato de cambiar antes de escribirlo; reescribir entidades JPA, migraciones de PostgreSQL y controladores en marcha cuesta diez veces más tiempo.

Paso 3 · Ejemplo de matriz de aceptación: gestor de proyectos e incidencias

El siguiente ejemplo muestra cómo comprobar el alcance de un gestor. Usa los mismos criterios técnicos para revisar el CRUD de tu dominio, construido desde la UD1.

Arquitectura del entregable del primer trimestre
  1. API REST (Spring Boot, DTOs, @Valid)
  2. Arquitectura en 3 capas desacopladas
  3. Persistencia JPA (Hibernate, proxies LAZY)
  4. PostgreSQL (tablas, FKs, secuencias, índices)

En el gestor de referencia, los criterios se concretan así. En tu proyecto, escribe las operaciones equivalentes y conserva sus reglas de negocio:

  • POST /proyectos: Alta de proyecto con nombre (único, no en blanco, máx. 80 caracteres) y descripcion opcional. Devuelve 201 Created con cabecera Location.

  • GET /proyectos: Listado paginado con Pageable o filtrado por estado activo.

  • GET /proyectos/{id}: Detalle del proyecto. Si no existe, 404 Not Found.

  • PUT /proyectos/{id}: Modificación completa validando unicidad de nombre.

  • DELETE /proyectos/{id}: Borrado físico o desactivación lógica, controlando restricciones de integridad referencial (204 No Content).

  • POST /tareas: Alta de tarea asociada obligatoriamente a un proyecto existente (@ManyToOne, FetchType.LAZY). Campos: titulo, prioridad (ALTA, MEDIA, BAJA) y proyectoId. Nace con completada = false.

  • GET /proyectos/{id}/tareas: Subrecurso que devuelve todas las tareas de un proyecto específico.

  • PATCH /tareas/{id}/completar: Modificación parcial para marcar tarea como resuelta.

  • PUT /tareas/{id} y DELETE /tareas/{id}: Actualización y borrado con validación de existencia previa (existsById).

  • Cada tarea puede tener opcionalmente un usuario responsable asignado (@ManyToOne nullable).

  • Endpoint de consulta: GET /tareas?responsableId={id} para ver la carga de trabajo de un desarrollador.

  • Catálogo de etiquetas independientes (Etiqueta: id, nombre único, colorHex).

  • Tabla puente en PostgreSQL: tareas_etiquetas.

  • Endpoints para asociar y desasociar: POST /tareas/{id}/etiquetas/{etiquetaId} y DELETE /tareas/{id}/etiquetas/{etiquetaId}.

  • Restricción estricta: borrar una tarea jamás debe borrar la etiqueta del catálogo maestro.

  • Caso de uso atómico multioperación: POST /proyectos/{id}/clonar?nuevoNombre=… protegido con @Transactional(rollbackFor = Exception.class). Si una tarea falla, el proyecto clonado se revierte por completo.

  • Cero consultas N+1: el listado de tareas con proyecto y etiquetas debe resolverse mediante JOIN FETCH en una sola sentencia SQL verificable en consola.

Paso 4 · Los tres artefactos de partida del proyecto

Antes de cerrar la versión del trimestre, revisa estos tres elementos en tu proyecto existente:

En lugar de redactar un documento de texto que nadie mantiene, el contrato de la API se define directamente en la colección de peticiones HTTP:

Método Endpoint Descripción Body Request Códigos HTTP esperados
POST /proyectos Crea un proyecto nuevo ProyectoRequest 201, 400, 409
GET /proyectos Lista proyectos con filtro Ninguno 200
GET /proyectos/{id} Detalle de proyecto Ninguno 200, 404
POST /tareas Crea tarea en proyecto TareaRequest 201, 400, 404
GET /proyectos/{id}/tareas Tareas de un proyecto Ninguno 200, 404
POST /proyectos/{id}/clonar Clona proyecto y tareas Query param 201, 400, 404, 409

El script DDL de referencia con los tipos exactos de PostgreSQL, secuencias, claves primarias, claves foráneas e índices:

-- Tablas principales
CREATE TABLE proyectos (
    id BIGSERIAL PRIMARY KEY,
    nombre VARCHAR(80) NOT NULL UNIQUE,
    descripcion TEXT,
    activo BOOLEAN NOT NULL DEFAULT TRUE,
    creado_en TIMESTAMP NOT NULL DEFAULT NOW()
);

CREATE TABLE usuarios (
    id BIGSERIAL PRIMARY KEY,
    nombre VARCHAR(60) NOT NULL,
    email VARCHAR(100) NOT NULL UNIQUE
);

CREATE TABLE etiquetas (
    id BIGSERIAL PRIMARY KEY,
    nombre VARCHAR(40) NOT NULL UNIQUE,
    color_hex VARCHAR(7) NOT NULL
);

CREATE TABLE tareas (
    id BIGSERIAL PRIMARY KEY,
    titulo VARCHAR(120) NOT NULL,
    prioridad VARCHAR(20) NOT NULL,
    completada BOOLEAN NOT NULL DEFAULT FALSE,
    proyecto_id BIGINT NOT NULL REFERENCES proyectos(id) ON DELETE CASCADE,
    responsable_id BIGINT REFERENCES usuarios(id) ON DELETE SET NULL
);

CREATE TABLE tareas_etiquetas (
    tarea_id BIGINT NOT NULL REFERENCES tareas(id) ON DELETE CASCADE,
    etiqueta_id BIGINT NOT NULL REFERENCES etiquetas(id) ON DELETE RESTRICT,
    PRIMARY KEY (tarea_id, etiqueta_id)
);

-- Índices para optimizar búsquedas frecuentes
CREATE INDEX idx_tareas_proyecto ON tareas(proyecto_id);
CREATE INDEX idx_tareas_prioridad ON tareas(prioridad);

El orden de trabajo del sprint organizado por dependencias técnicas:

  1. Fase 1 (Fundamentos): Entidades base, DDL en schema.sql, Repositorios JPA y tests @DataJpaTest.
  2. Fase 2 (Casos de Uso Core): Servicios y endpoints CRUD de Proyecto y Tarea con DTOs y validación @Valid.
  3. Fase 3 (Relaciones N:M y Subrecursos): Catálogo de etiquetas, tabla puente y endpoints de asignación.
  4. Fase 4 (Integridad y Transacciones): Caso de uso multioperación clonarProyecto con reversión atómica ante fallos.
  5. Fase 5 (Calidad y Rendimiento): Erradicación de N+1 con JOIN FETCH, paginación y suite de tests en verde.

Paso 5 · Preparar la suite de pruebas y el esquema

  1. Abre las entidades y el esquema de PostgreSQL. Compara nombres de tablas, tipos, claves y restricciones. Guarda el esquema revisado en docs/esquema.sql como documentación; no actives su ejecución automática sobre tablas existentes.
  2. En la colección del proyecto añade una carpeta «Aceptación trimestre 1» y conserva la variable baseUrl. Ordena los casos: crear padre → crear recursos relacionados → consultar → modificar → comprobar rechazos → borrar datos de prueba.
  3. En cada petición utiliza el id devuelto por el alta anterior. Añade una aserción de estado y otra sobre el dato importante; reutiliza los casos que ya funcionan.
  4. Ejecuta la carpeta completa y revisa las discrepancias entre colección, modelo y README. Corrige primero una discrepancia, repite su caso y después vuelve a ejecutar la carpeta.
  5. Registra qué requisitos están comprobados y cuál falta por resolver. Esta es la misma aplicación que se pondrá en producción con Intermodular.

Paso 6 · Guía de diagnóstico rápido ante bloqueos típicos

Durante el sprint te toparás con errores reales de integración. Esta tabla resume la causa raíz de los cuatro bloqueos más frecuentes y su solución inmediata:

Síntoma en la consola Causa raíz Solución de ingeniería
LazyInitializationException: could not initialize proxy - no Session Se intentó acceder a una relación perezosa (FetchType.LAZY) fuera de la frontera transaccional (ej: en el controlador o Jackson). Mapea la entidad a DTO dentro del servicio bajo @Transactional(readOnly = true), o añade JOIN FETCH a la consulta del repositorio.
DataIntegrityViolationException: null value in column violates not-null constraint El DTO aceptó un campo nulo que la tabla de PostgreSQL prohíbe, o la entidad se persistió sin asignar una clave foránea obligatoria. Añade @NotNull / @NotBlank en el DTO con @Valid en el controlador, y valida en el servicio antes de llamar a save().
PropertyReferenceException: No property 'xyz' found for type 'Entidad' El nombre de un método en JpaRepository tiene una errata o hace referencia a un atributo inexistente. Revisa el nombre exacto de la propiedad Java en la entidad (respetando mayúsculas y minúsculas).
MultipleBagFetchException: cannot simultaneously fetch multiple bags Se intentó hacer JOIN FETCH simultáneo sobre dos colecciones de tipo List en la misma consulta JPQL. Cambia las colecciones a Set o divide la carga en dos consultas dirigidas dentro de la misma transacción.

Paso 7 · El registro de incidencias técnicas

En ingeniería de software no se esconden los problemas: se diagnostican y se resuelven con método. Registra las incidencias técnicas resueltas en las comprobaciones de la sesión, dentro del mismo repositorio, con este esquema:

1. Incidencia: LazyInitializationException al listar proyectos con tareas
   - Síntoma: Al llamar a GET /proyectos/1/detalle, Jackson lanzaba error 500 por sesión cerrada.
   - Causa raíz: El mapper se ejecutaba en el controlador después de que la transacción del servicio hubiera cerrado la conexión con PostgreSQL.
   - Solución de ingeniería: Añadimos @Query("SELECT p FROM Proyecto p LEFT JOIN FETCH p.tareas WHERE p.id = :id") en ProyectoRepository para traer las tareas en la misma sentencia SQL.

Tener identificados y resueltos estos casos te servirá además como evidencia directa para la defensa técnica oral de la sesión 28.

Cuánto trabajo cabe aquí, dicho sin rodeos

Esta unidad ocupa dos sesiones de tres horas. El proyecto se lleva construyendo desde la primera sesión del trimestre. Las iteraciones de este ejemplo sirven para revisar e integrar lo que ya existe antes de la defensa; no son el comienzo de otra aplicación.

Trabaja siempre en este orden: termina una iteración entera antes de empezar la siguiente. Es preferible entregar dos iteraciones que funcionan de punta a punta que cuatro a medias. La rúbrica valora lo que funciona, no lo que está empezado.

Al final de cada sesión, haz un commit de lo que funcione, aunque esté incompleto. Un repositorio con historial es también una evidencia de cómo trabajas.

Paso 8 · Ejecutar el sprint de desarrollo

Elige el primer requisito pendiente de la matriz y reproduce su fallo. Localiza la capa responsable, realiza un cambio pequeño y repite esa comprobación; después ejecuta las pruebas relacionadas para detectar regresiones. Actualiza la fila con archivo, commit y resultado. Continúa con el siguiente pendiente hasta que el recorrido principal, las relaciones y las reglas puedan demostrarse en la versión publicada mediante Intermodular.

  1. Desarrolla Proyecto y Tarea con la relación @ManyToOne(fetch = FetchType.LAZY).

  2. Implementa los repositorios con sus tests @DataJpaTest.

  3. Conecta los servicios y controladores para altas y consultas.

  4. Valida en Bruno/Postman que las peticiones devuelven 201 Created y 404 Not Found.

  5. Añade anotaciones de Bean Validation en todos los DTOs de petición (@NotBlank, @Size, @Pattern).

  6. Configura @RestControllerAdvice para capturar errores de validación y de dominio, emitiendo respuestas con formato estándar RFC 7807 (Problem Details).

  7. Prueba en Bruno casos de fallo con cuerpos JSON inválidos y verifica que la API devuelve 400 Bad Request con mensajes detallados por campo.

  8. Implementa la entidad Etiqueta y la relación @ManyToMany con @JoinTable en Tarea utilizando Set.

  9. Añade los endpoints de asignación y desasignación.

  10. Comprueba en PostgreSQL que la tabla tareas_etiquetas se puebla y que borrar una tarea no elimina la etiqueta maestra.

  11. Desarrolla el caso de uso clonarProyecto protegido con @Transactional(rollbackFor = Exception.class).

  12. Audita la consola de Spring Boot con spring.jpa.show-sql=true:

    • Identifica cualquier consulta N+1 en los listados.
    • Sustitúyela por una consulta con JOIN FETCH o paginación con Pageable.
  13. Ejecuta la suite completa: ./mvnw test debe pasar al 100 % en verde.

Paso 9 · Comprobar y registrar el resultado del proyecto

  1. Ejecuta de nuevo cada comprobación que antes fallaba y registra qué cambio la ha corregido.
  2. Repite el recorrido completo del producto y confirma que el repositorio contiene instrucciones suficientes para arrancar esa versión con su base de datos.

Ampliación si has completado el trabajo

Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.

Reto · Detección de ambigüedades en pliegos técnicos

Analiza estas tres frases extraídas de pliegos de clientes reales y detecta sus trampas de ingeniería:

  • Frase del cliente: «La consulta de proyectos y tareas debe cargar rápido».

  • Explica por qué «rápido» no es un criterio de aceptación verificable.

  • Reescribe esa frase como un criterio de ingeniería preciso y medible (definiendo percentiles de latencia, volumen de registros y número máximo de consultas SQL permitidas).

  • Frase del cliente: «Los usuarios pueden borrar proyectos cuando ya no los necesiten».

  • ¿Qué ambigüedad mortal esconde esa frase respecto a las tareas que contiene el proyecto?

  • ¿Qué tres alternativas técnicas existen y qué consecuencias tiene cada una para la base de datos?

  • Frase del cliente: «No puede haber dos proyectos con el mismo nombre».

  • ¿Qué ocurre si un usuario intenta crear “Mi Proyecto” y otro intenta crear “mi proyecto” o “Mi Proyecto “ (con espacio al final)?

  • ¿Cómo debe formularse este criterio a nivel de DTO, de Servicio y de base de datos relacional para que sea invulnerable?

Objetivo mínimoEsquema schema.sql y tabla de correspondencia de endpoints definidos sin ambigüedades de nombres ni tipos.
Si lo tienesColección completa en Bruno/Postman preparada con variables de entorno y fases ordenadas en README.md.
RetoAnálisis de ambigüedades técnicas completado con criterios Gherkin rigurosos y gestión de borrados definida.
Ver respuestas

1 · Porque actúa como especificación ejecutable y contrato compartido: permite validar de inmediato cada endpoint conforme se construye sin tener que inventar peticiones sobre la marcha.

2 · Dado (el contexto o estado inicial del sistema), Cuando (la acción o petición que realiza el cliente) y Entonces (el resultado observable, código de respuesta y cambios en base de datos).

3 · El servicio permite emitir un error de negocio limpio (409 Conflict) con un mensaje comprensible, mientras que la restricción UNIQUE física de PostgreSQL garantiza la integridad ante condiciones de carrera concurrentes que el servicio no pueda prever.

4 · Es la secuencia de tareas que determina la duración mínima del proyecto porque cada una depende estrictamente de que la anterior esté terminada (ej: no se pueden mapear relaciones JPA sin haber creado primero las entidades base).

Reto · Diagnóstico forense de pruebas intermitentes (Flaky Tests)

Analiza este escenario crítico de integración continua (CI):

Un compañero de equipo sube un cambio y el pipeline de GitHub Actions se pone en rojo de forma intermitente: unas veces los tests pasan y otras fallan sin tocar una sola línea de código:

  • ¿Por qué los tests que dependen de secuencias autoincrementales (assertThat(tarea.getId()).isEqualTo(1L)) son la causa número uno de tests intermitentes (Flaky Tests) en persistencia?
  • ¿Por qué un test que no limpia su base de datos o que olvida la transacción con rollback puede romper el test de otra clase que se ejecuta a continuación?
  • Escribe tres reglas de diseño que garanticen que una batería de pruebas de persistencia sea 100 % determinista, independiente del orden de ejecución e inmune a las secuencias de PostgreSQL.
Objetivo mínimoIteraciones 1 y 2 completadas: CRUD de Proyectos y Tareas funcionando con validación y DTOs limpios.
Si lo tienesIteraciones 3 y 4 completadas: Etiquetas N:M, clonación transaccional y cero consultas N+1 con tests en verde.
RetoRegistro de incidencias resueltas preparado para la defensa técnica y suite de tests 100 % determinista.
Ver respuestas

1 · Porque valida la integración completa de cada pieza en pequeños incrementos funcionales, detectando inconsistencias entre capas de inmediato en lugar de acumularlas para el final.

2 · Porque Spring Boot por defecto solo revierte la transacción ante excepciones no comprobadas (RuntimeException o Error), a menos que se especifique rollbackFor = Exception.class.

3 · Las propiedades de logging de SQL en application.properties (spring.jpa.show-sql=true y el formateador de Hibernate).

4 · Porque el valor exacto de la secuencia depende del orden de ejecución de los tests y de inserciones previas; se debe comprobar que no sea nulo (assertThat(id).isNotNull() o isPositive()).

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

El mismo commit identifica la API evaluable y su despliegue; el CRUD, las relaciones y las operaciones complejas tienen evidencias.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.