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):
- 1. Entidad y Repositorio
- 2. Test de persistencia (@DataJpaTest)
- 3. Servicio y Reglas de Negocio
- 4. DTOs y Controlador REST
- 5. Validación en Bruno/Postman
- Coges la primera historia del backlog: «Alta de Proyecto con validación de nombre único».
- Creas su entidad, su repositorio y su test
@DataJpaTest. Ejecutas./mvnw test→ VERDE. - Creas su servicio con la regla de negocio y su excepción.
- Creas sus DTOs, su controlador y su manejador de errores.
- Ejecutas la petición en Bruno/Postman →
201 Created. - 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
- Abre la propuesta del README, las pruebas, la colección y la versión desplegada. Anota el commit que estás revisando.
- Relaciona cada criterio del trimestre con una ruta, test o consulta que lo demuestre. Marca cumple, falla o pendiente con una evidencia concreta.
- 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 /tareasespera 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 errores500incomprensibles. - 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.
- API REST (Spring Boot, DTOs, @Valid)
- Arquitectura en 3 capas desacopladas
- Persistencia JPA (Hibernate, proxies LAZY)
- 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 connombre(único, no en blanco, máx. 80 caracteres) ydescripcionopcional. Devuelve201 Createdcon cabeceraLocation. -
GET /proyectos: Listado paginado conPageableo filtrado por estadoactivo. -
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) yproyectoId. Nace concompletada = 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}yDELETE /tareas/{id}: Actualización y borrado con validación de existencia previa (existsById). -
Cada tarea puede tener opcionalmente un usuario responsable asignado (
@ManyToOnenullable). -
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}yDELETE /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 FETCHen 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:
- Fase 1 (Fundamentos): Entidades base, DDL en
schema.sql, Repositorios JPA y tests@DataJpaTest. - Fase 2 (Casos de Uso Core): Servicios y endpoints CRUD de Proyecto y Tarea con DTOs y validación
@Valid. - Fase 3 (Relaciones N:M y Subrecursos): Catálogo de etiquetas, tabla puente y endpoints de asignación.
- Fase 4 (Integridad y Transacciones): Caso de uso multioperación
clonarProyectocon reversión atómica ante fallos. - 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
- Abre las entidades y el esquema de PostgreSQL. Compara nombres de tablas, tipos, claves y restricciones. Guarda el esquema revisado en
docs/esquema.sqlcomo documentación; no actives su ejecución automática sobre tablas existentes. - 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. - 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.
- 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.
- 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.
-
Desarrolla
ProyectoyTareacon la relación@ManyToOne(fetch = FetchType.LAZY). -
Implementa los repositorios con sus tests
@DataJpaTest. -
Conecta los servicios y controladores para altas y consultas.
-
Valida en Bruno/Postman que las peticiones devuelven
201 Createdy404 Not Found. -
Añade anotaciones de Bean Validation en todos los DTOs de petición (
@NotBlank,@Size,@Pattern). -
Configura
@RestControllerAdvicepara capturar errores de validación y de dominio, emitiendo respuestas con formato estándar RFC 7807 (Problem Details). -
Prueba en Bruno casos de fallo con cuerpos JSON inválidos y verifica que la API devuelve
400 Bad Requestcon mensajes detallados por campo. -
Implementa la entidad
Etiquetay la relación@ManyToManycon@JoinTableenTareautilizandoSet. -
Añade los endpoints de asignación y desasignación.
-
Comprueba en PostgreSQL que la tabla
tareas_etiquetasse puebla y que borrar una tarea no elimina la etiqueta maestra. -
Desarrolla el caso de uso
clonarProyectoprotegido con@Transactional(rollbackFor = Exception.class). -
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 FETCHo paginación conPageable.
-
Ejecuta la suite completa:
./mvnw testdebe pasar al 100 % en verde.
Paso 9 · Comprobar y registrar el resultado del proyecto
- Ejecuta de nuevo cada comprobación que antes fallaba y registra qué cambio la ha corregido.
- 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?
schema.sql y tabla de correspondencia de endpoints definidos sin ambigüedades de nombres ni tipos.README.md.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.
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.