Decidir con lo que mostró el sprint
Un backlog no se escribe una vez y se obedece: se corrige con lo que el equipo aprende en cada sprint. Esta guía recorre dos tipos de decisión que aparecen justo después de una Review y una Retro. Primero, cómo reordenar el backlog y cuánto comprometer usando datos en vez de impresiones. Después, una decisión técnica que sale de esos mismos datos: cuándo vale la pena un Repository y la inyección de dependencias, y cuándo solo suman archivos.
👁 — vistas · Ing. Roy Carrasco, Facultad de Ingeniería de Sistemas · UAB
Terminó el Sprint 1 de EventoUAB. Estás con Camila (Product Owner), Valentina y Mateo en la Retro. En cada decisión podés probar primero un camino equivocado, ver su consecuencia sin ninguna penalidad, y recién después se desbloquea el correcto.
Lo que mostró el piloto
En la Review del Sprint 1, tres clubes (Robótica, Teatro y Debate) usaron la app durante una semana. Camila volvió a la Retro con esto anotado, y con el backlog tal como estaba antes del piloto: 1) Notificar a Bienestar de cada reserva (8 puntos), 2) Calendario visual con colores (8 puntos), 3) Reservas semanales (5 puntos).
| Qué apareció en la Review y la Retro | De dónde salió |
|---|---|
| Robótica y Debate crearon a mano la misma reserva del mismo salón, mismo horario, cuatro semanas seguidas. | Registro de reservas del piloto |
| Ningún club abrió ni mencionó las notificaciones a Bienestar. | Observación durante la Review |
| Un docente dijo: "yo les pondría un calendario más lindo, con colores". | Comentario suelto |
Las pruebas de Valentina y Mateo fallaron tres veces porque ambos usaban el mismo archivo reservas.db. | Retro del equipo |
¿Qué hace Camila con el backlog después de la Review?
El backlog es una lista de hipótesis sobre qué le sirve al usuario, y el piloto acaba de probar una. Si la primera evidencia real no puede mover nada, el Planning dejó de ser una apuesta y pasó a ser un dogma. Mantener el orden por "seriedad" tiene un costo concreto: el equipo construiría primero lo que nadie pidió. Probá con otra opción.
Anotar todo con la misma prioridad trata un comentario suelto igual que un patrón repetido cuatro semanas seguidas. El backlog crece y el equipo vuelve a decidir por quién habló más fuerte, no por lo que pasó. Probá con otra opción.
Exacto. Cambiar el orden con una razón visible no rompe el plan: es el plan funcionando. Seguí leyendo ↓
Backlog vivo: evidencia vs. opinión
Un ítem del backlog es una apuesta: "creemos que esto le sirve a este usuario". Cada sprint trae datos que confirman o desmienten esa apuesta. La disciplina es distinguir lo que alguien observó de lo que alguien supone, y dejar que lo primero pese más al ordenar.
| Fuente | Ejemplo en EventoUAB | Qué ayuda a decidir |
|---|---|---|
| Uso real del producto | La misma reserva repetida a mano cuatro semanas | Qué historia falta o está mal resuelta |
| Observación en la Review | Nadie tocó las notificaciones | Qué valor se había sobreestimado |
| Retrospectiva | Pruebas que se pisan por compartir un archivo | Qué deuda técnica frena al equipo |
| Números del sprint | Puntos terminados, defectos que volvieron | Cuánto se puede comprometer |
| Opinión o pedido | "Yo pondría un calendario más lindo" | Una pista para investigar, no una orden |
Con esa lectura, el backlog de Camila queda así. Nada se borró: lo que bajó sigue visible, con la razón anotada, por si más adelante llega evidencia a favor.
| Antes del piloto | Después del piloto | Razón anotada |
|---|---|---|
| 1. Notificar a Bienestar (8) | 3. Notificar a Bienestar (8) | Ningún club lo usó; validar antes de construir |
| 2. Calendario con colores (8) | 4. Calendario con colores (8) | Solo una opinión, sin dato de uso |
| 3. Reservas semanales (5) | 1. Reservas semanales (5) | Repetición manual observada en 2 de 3 clubes |
| — | 2. Pruebas que no compartan archivo (3) | Falló tres veces en el sprint; frena el desarrollo |
Que Bienestar quiera notificaciones es un dato valioso sobre el contexto. Lo que no es, todavía, es evidencia de que los clubes las necesiten. Por eso se baja la prioridad y se agrega una pregunta concreta para el próximo piloto, en vez de descartarlo.
¿Es evidencia observada o una opinión?
¿Cuántos puntos entran al Sprint 2?
El backlog ya está reordenado. Ahora toca el Planning del Sprint 2. En el Sprint 1 el equipo planificó 24 puntos (seis historias) y terminó 15 (cuatro historias que cumplen la Definition of Done). Las otras dos, de 5 y 4 puntos, quedaron "casi listas": con el código escrito, pero sin pruebas ni revisión. Bienestar espera la app completa dentro de cuatro sprints más.
¿Cuántos puntos comprometen para el Sprint 2?
Repetir el plan que ya falló una vez no es optimismo, es ignorar el dato. Y "casi hecho" no es un estado: una historia sin pruebas ni revisión puede esconder días de trabajo. Si el Sprint 2 arranca con la misma meta, probablemente termine con el mismo resultado. Probá con otra opción.
Comprometer más para compensar lo que no salió es el camino al apuro: se recortan pruebas y revisión, aparecen defectos, y el Sprint 3 se llena de arreglos. Una fecha que no coincide con la capacidad real se negocia con datos; no se tapa con horas. Probá con otra opción.
Exacto. Lo que el equipo terminó de verdad es el mejor predictor de lo que va a terminar. Seguí leyendo ↓
Velocidad y pronóstico
La velocidad (velocity) es la cantidad de puntos de historias terminadas en un sprint. Solo cuenta lo que cumple la Definition of Done: una historia con la mitad del trabajo hecho suma cero. Con la velocidad de los últimos sprints, el equipo pronostica cuánto comprometer y cuándo llegaría al final del backlog.
| Sprint | Puntos planificados | Puntos terminados (DoD) |
|---|---|---|
| Sprint 1 | 24 | 15 |
| Sprint 2 | 15 (compromiso) | — |
Un único dato todavía es ruido, pero sigue siendo mejor que una esperanza. Cuando haya tres sprints se usa el promedio, y se desconfía de cualquier número que haga felices a todos antes de mirar los datos. Y la velocidad no es una meta ni se compara entre equipos: se usa solo para planificar.
Para saber si la fecha es realista se divide lo que falta por la velocidad. Con 60 puntos restantes en el backlog y 15 por sprint, el equipo necesita 4 sprints. Si Bienestar quiere la app en 3, la conversación correcta es de alcance ("¿qué sale de la lista?"), no de horas extra.
Un equipo terminó 12, 15 y 18 puntos en sus tres primeros sprints.
1. Su velocidad promedio es de puntos por sprint.
2. Si el backlog tiene 60 puntos pendientes, necesita aproximadamente sprints más.
3. Una historia de 5 puntos con el código escrito pero sin pruebas ni revisión suma a la velocidad.
"¿Y si lo ordenamos con un Repository?"
La historia que encabeza el Sprint 2 es "reservas semanales": un club pide el mismo salón y horario durante varias semanas. Si alguna de esas semanas choca con otra reserva, no se crea ninguna. Hoy la lógica de reservas arma sus consultas SQL directo, en seis lugares de reservas.js. Valentina propone un Repository. Mateo duda: "¿no es meterle ingeniería de más a algo que funciona?"
import { db } from "./db.js";
export function reservarSemanal({ club, espacioId, inicio, fin, semanas }) {
const existentes = db
.prepare("SELECT inicio, fin FROM reservas WHERE espacio_id = ?")
.all(espacioId); // ← la consulta vive dentro de la regla
// ...regla: revisar choques semana por semana...
db.prepare("INSERT INTO reservas ...").run(/* ... */);
}
¿Qué hace el equipo con la propuesta de Valentina?
Refactorizar tres tablas que nunca dieron problemas consume parte del sprint sin que ningún usuario lo note, y sin ningún dato que diga que valía la pena. Es hacer el trabajo por si acaso. Probá con otra opción.
Las pruebas ya fallaron tres veces por esto. "Limpiar al terminar" depende de que nadie se olvide, y de que las pruebas no corran al mismo tiempo: el ejecutor de pruebas de Node corre los archivos en paralelo, así que dos archivos pueden pisarse igual. La Retro ya mostró que el dolor es real. Probá con otra opción.
Exacto. El patrón entra donde hay dolor medido y no en el resto del proyecto. Seguí leyendo ↓
Repository: el bibliotecario de tus datos
Pensalo como una biblioteca. Vos le pedís al bibliotecario "dame los libros del estante 3" y "guardá este libro". No te importa si el catálogo es un fichero de papel o una computadora: él sabe cómo, y vos solo hacés el pedido. Un Repository es ese bibliotecario: el resto del programa le pide reservas y él se ocupa de dónde y cómo se guardan.
| Sin Repository | Con Repository | |
|---|---|---|
| Quién sabe de SQL | La regla de reservas, mezclada con su lógica | Solo el repositorio |
| Para probar la regla | Hace falta una base de datos real | Alcanza con una lista en memoria |
| Lo que la regla pide | Escribe una consulta SQL | Dice delEspacio(1) o guardar(r) |
El "contrato" son solo dos pedidos. JavaScript no tiene interfaces: el equipo lo escribe y lo respeta.
| Pedido | Qué hace |
|---|---|
delEspacio(espacioId) | Devuelve las reservas de ese espacio. |
guardar(reserva) | Guarda una reserva nueva. |
La versión más simple posible del bibliotecario es una lista dentro de la memoria. Son ocho líneas y sirve para las pruebas:
export class ReservasEnMemoria {
#datos = []; // una lista, nada más
delEspacio(espacioId) {
return this.#datos.filter((r) => r.espacioId === espacioId);
}
guardar(reserva) {
this.#datos.push(reserva);
}
}
Ver el otro bibliotecario: el de SQLite (la app real)
export class ReservasSqlite {
constructor(db) {
this.db = db;
db.exec(`CREATE TABLE IF NOT EXISTS reservas (
id INTEGER PRIMARY KEY, club TEXT, espacio_id INTEGER, inicio INTEGER, fin INTEGER)`);
}
delEspacio(espacioId) {
return this.db
.prepare("SELECT club, espacio_id AS espacioId, inicio, fin FROM reservas WHERE espacio_id = ?")
.all(espacioId);
}
guardar(r) {
this.db
.prepare("INSERT INTO reservas (club, espacio_id, inicio, fin) VALUES (?, ?, ?, ?)")
.run(r.club, r.espacioId, r.inicio, r.fin);
}
}
Tiene exactamente los mismos dos pedidos. Solo cambia lo que hay adentro.
Dos bibliotecarios distintos (memoria y SQLite), el mismo mostrador (delEspacio y guardar). Quien hace el pedido no se entera de cuál lo atendió.
¿De dónde saca el servicio su repositorio?
Valentina ya tiene los dos repositorios. Falta escribir ReservasService, la clase con la regla de "reservas semanales". Ahora hay que decidir cómo consigue el repositorio que va a usar.
¿Cómo obtiene ReservasService su repositorio?
Con esa línea adentro, el servicio queda soldado a SQLite: no hay forma de probarlo sin una base de datos, y volvemos exactamente al problema que el Repository venía a resolver. La comodidad de "no armar nada" se paga en cada prueba. Probá con otra opción.
Funciona, pero el estado compartido es frágil: si una prueba se olvida de restaurar la variable, la siguiente corre con un repositorio ajeno y falla (o peor, pasa por casualidad). Con dos pruebas corriendo a la vez, se pisan entre sí. Probá con otra opción.
Exacto. Cada instancia trae su propio repositorio, y nada global puede interferir entre pruebas. Seguí leyendo ↓
Inyección de dependencias: que se lo pasen
Es una idea sencilla: en vez de que un objeto cree lo que necesita, se lo pasan desde afuera. Como cuando el cajero no fabrica su propia caja registradora: se la dan al empezar el turno. No requiere ningún framework; alcanza con un parámetro del constructor.
| Sin inyección | Con inyección |
|---|---|
this.repo = new ReservasSqlite(db)El servicio se arma su propio bibliotecario, y siempre es el de SQLite. | this.repo = repoEl servicio usa el bibliotecario que le entreguen. |
Así queda el servicio completo para la regla "no reservar un horario ocupado". Son pocas líneas y no menciona SQLite en ningún lado:
export class ReservasService {
constructor(repo) {
this.repo = repo; // ← llega de afuera
}
reservar(nueva) {
const ocupado = this.repo
.delEspacio(nueva.espacioId)
.some((r) => hayChoque(r, nueva));
if (ocupado) return false;
this.repo.guardar(nueva);
return true;
}
}
Y quien decide cuál bibliotecario usar es un solo lugar, el main.js (la raíz de composición). Las pruebas hacen la misma elección, pero con el de memoria:
// main.js — la app real
const servicio = new ReservasService(new ReservasSqlite(db));
// en una prueba
const servicio = new ReservasService(new ReservasEnMemoria());
test("no deja reservar un horario ocupado", () => {
const servicio = new ReservasService(new ReservasEnMemoria());
assert.equal(servicio.reservar({ espacioId: 1, inicio: 10, fin: 12 }), true);
assert.equal(servicio.reservar({ espacioId: 1, inicio: 11, fin: 13 }), false); // choca
assert.equal(servicio.reservar({ espacioId: 1, inicio: 12, fin: 14 }), true); // contigua: no choca
});
La prueba corre en milisegundos, sin archivo de base de datos que otro pueda pisar. Y la misma regla funciona con SQLite en la app real sin cambiar una sola línea del servicio.
Ver la regla completa de "reservas semanales" (la historia del Sprint 2)
const SEMANA = 7 * 24 * 60 * 60 * 1000;
reservarSemanal({ club, espacioId, inicio, fin, semanas }) {
const nuevas = [];
for (let i = 0; i < semanas; i++) {
const r = { club, espacioId, inicio: inicio + i * SEMANA, fin: fin + i * SEMANA };
const choque = this.repo.delEspacio(espacioId).some((x) => hayChoque(x, r));
if (choque) return { ok: false, semana: i + 1 };
nuevas.push(r);
}
nuevas.forEach((r) => this.repo.guardar(r)); // recién acá se guarda: todo o nada
return { ok: true, creadas: nuevas.length };
}
Si alguien cambiara esto para guardar cada semana sobre la marcha, una prueba con el repositorio en memoria lo detectaría enseguida: quedarían guardadas las semanas anteriores al choque.
Une cada concepto con su descripción.
"Hagamos lo mismo con los clubes"
Mateo, ya convencido, quiere aplicar el patrón a listarClubes(), la función que llena el desplegable de clubes. Es un único SELECT, se llama desde un solo lugar, no tiene ninguna regla de negocio y nunca causó una prueba fallida.
¿Qué hace el equipo con listarClubes()?
Para una consulta sin lógica, el patrón agrega tres archivos y un salto más para leer el código, a cambio de nada que alguien vaya a notar o probar. La consistencia por sí sola no justifica el costo; el patrón se paga cuando resuelve un problema que el equipo tiene. Probá con otra opción.
Un repositorio genérico se escribe antes de conocer los casos reales, y cada tabla termina necesitando algo distinto (reservas con rangos de fechas, usuarios con credenciales). El resultado es una abstracción que no encaja con ninguna y que todos tienen que esquivar. Probá con otra opción.
Exacto. No es "nunca"; es "todavía no". Si la señal aparece, el cambio se hace en ese momento, con evidencia. Seguí leyendo ↓
Cuándo ayuda y cuándo estorba
Un patrón de diseño no es un objetivo: es una herramienta para un dolor concreto. Un Repository con inyección de dependencias cuesta algo (más archivos, dos implementaciones que mantener, un salto más para leer el código), y ese costo tiene que quedar compensado por algo visible. La misma lógica de la primera mitad de la guía: decidir con evidencia, no con costumbre.
| Ayuda cuando... | Estorba cuando... |
|---|---|
| Hay reglas de negocio que querés probar sin una base de datos. | Es un CRUD sin lógica: leer y guardar sin decidir nada. |
| El mismo acceso a datos lo usan varios lugares. | Hay un solo lugar que lo usa y no se prevé otro. |
| Hay una razón real para cambiar la fuente (otro motor, una API). | Nadie va a cambiar la fuente en el horizonte del proyecto. |
| Las pruebas actuales ya duelen: lentas, frágiles o que se pisan. | No hay pruebas que se beneficien, o ya son rápidas y estables. |
Decidir "no por ahora" es parte del diseño, no una omisión. Lo que lo vuelve una decisión de equipo es dejarlo escrito en el backlog o en un ADR corto: qué se decidió, por qué, y qué cambio en el proyecto haría volver a pensarlo. Así, quien llegue después sabe que fue elegido y no olvidado.
¿Un Repository con inyección ayuda o estorba en este caso?
Actividades de repaso
Un repaso integrador de lo que acabás de recorrer. Cada actividad se corrige al instante; tu progreso se guarda automáticamente en este navegador.
Al reordenar el backlog después de una Review, ¿qué debería pesar más?
Para decidir si conviene un Repository, hay que mirar primero qué problema concreto tiene el proyecto, y no si el patrón "se usa en la industria".
¿Qué se gana al pasar el repositorio por el constructor en lugar de crearlo dentro del servicio?
🎯 Reto Final: tu backlog y tu patrón
Ahora con el proyecto real de tu equipo. Escribí, en no más de un párrafo por punto: 1) una evidencia concreta del último sprint (de uso, de la Retro o de tus números) que justifique mover un ítem de lugar en tu backlog, y qué ítem bajaría por falta de evidencia, 2) tu velocidad de los sprints que llevás y cuántos puntos vas a comprometer en el próximo, 3) un lugar de tu código donde un Repository con inyección ayudaría y otro donde estorbaría, con la señal que usaste para decidir.
Reordenar con evidencia y decidir un patrón en tu proyecto real.
- La evidencia es algo observado (un dato, un registro, una falla repetida), no una opinión ni un "creo que".
- Se nombra qué ítem baja o sale y la razón queda anotada, en lugar de descartarlo sin explicación.
- La velocidad se calcula con puntos de historias terminadas según la Definition of Done, no con las planificadas.
- El compromiso del próximo sprint parte de esa velocidad y no de la fecha deseada.
- El lugar donde el patrón ayuda se justifica con un dolor concreto (pruebas que se pisan, lógica sin poder probar), y el lugar donde estorba, con la ausencia de ese dolor.
- Para el caso donde estorba, se anota la señal que haría reconsiderarlo.
Cheat Sheet
| Término | Idea clave |
|---|---|
| Backlog vivo | Lista de apuestas que se corrige con lo que muestra cada sprint. Lo que baja no se borra: queda con la razón anotada. |
| Evidencia vs. opinión | Evidencia es lo observado (uso, registros, fallas repetidas). Un pedido o un "creo que" es una pista para investigar. |
| Velocidad | Puntos de historias terminadas (con DoD) por sprint. Sirve para planificar, no es una meta ni se compara entre equipos. |
| Pronóstico | Puntos pendientes divididos por la velocidad. Si la fecha no da, se negocia el alcance, no las horas. |
| Repository | Objeto que se usa como una colección del dominio y esconde cómo se guarda. El contrato lo escribe el equipo. |
| Inyección de dependencias | Pasarle a un objeto lo que necesita en vez de crearlo adentro. Alcanza con un parámetro del constructor. |
| Raíz de composición | El único lugar (por ejemplo, main.js) donde se arman y conectan las piezas de la app. |
| Ayuda cuando... | Hay reglas que probar sin base de datos, varios usos del mismo acceso, o un cambio real de fuente a la vista. |
| Estorba cuando... | Es un CRUD sin lógica, con un solo uso y sin dolor medido. Se anota la señal para reconsiderarlo. |
¿Sabías que...?
🌦 "El clima de ayer"
Extreme Programming (XP) llama "el clima de ayer" a la regla de planificar con lo que el equipo terminó en el sprint anterior. Igual que el pronóstico del tiempo, la mejor predicción de mañana es lo que pasó hoy.
📚 Un patrón con más de veinte años
El Repository se describe en Patterns of Enterprise Application Architecture (Martin Fowler, 2002) y en Domain-Driven Design (Eric Evans, 2003). Casi todos los lenguajes y frameworks lo tienen hoy en alguna forma.
🧭 Lo que viene en la próxima guía
El repositorio en memoria que usaste es un "doble de prueba" simple. En la próxima guía profundizamos en pruebas unitarias, mocks, cobertura y pruebas de regresión, y hacemos un recorrido breve por los microservicios.