De una SPEC a tickets, tests y criterios de aceptación: flujo real de SDD

Cómo transformar una spec en trabajo ejecutable: tickets, criterios de aceptación, tests y prompts para agentes de IA.

Cover for De una SPEC a tickets, tests y criterios de aceptación: flujo real de SDD

Escribir una buena spec es la mitad del trabajo. La otra mitad es convertirla en algo que alguien (persona o agente) pueda ejecutar. Y es justo ahí donde la mayoría de equipos fallan.

En los artículos anteriores sobre SDD y cómo escribir una spec hablaba de por qué las specs importan y cómo escribirlas bien. Pero me quedaba el paso más práctico: cómo pasar de una spec aprobada a tickets concretos, criterios de aceptación verificables, tests derivados de esos criterios y prompts que un agente de IA pueda usar para implementar cada ticket.

Este artículo es ese puente. Lo que uso cuando tengo una spec firmada y necesito que el equipo (humano y agentes) empiece a trabajar.


El problema: specs que no se ejecutan

He visto specs técnicamente perfectas que se quedan en un Google Doc y nunca se convierten en trabajo real. El equipo lee la spec, asiente en la reunión de planning, y luego cada uno interpreta por su cuenta qué hay que hacer. Se crean tickets vagos tipo “Implementar sistema de notificaciones” y el desarrollador (o el agente) tiene que adivinar el alcance.

El resultado: tickets que se reabren porque “faltaba esto”, PRs que no pasan review porque “eso no era lo que se pedía”, y una spec que nadie vuelve a consultar después del primer día.

Una spec que no se descompone en trabajo ejecutable es un documento de intenciones, no una herramienta de trabajo.


Ejemplo concreto: spec de un sistema de notificaciones

Voy a usar un ejemplo real (simplificado) para mostrar todo el flujo. Imagina que la spec describe un sistema de notificaciones por email para una plataforma de pedidos.

Spec resumida

# SPEC: Sistema de notificaciones por email

## Objetivo
Permitir que los usuarios reciban notificaciones por email cuando cambia el
estado de sus pedidos (confirmado, enviado, entregado, cancelado).

## Alcance
- Solo email (no push ni SMS en esta fase).
- Solo cambios de estado de pedidos.
- El usuario puede desactivar notificaciones desde su perfil.

## Requisitos funcionales
1. Cuando un pedido cambia de estado, se envía un email al usuario.
2. El contenido del email depende del nuevo estado (plantilla diferente por estado).
3. Los emails se envían de forma asíncrona (no bloquean el flujo del pedido).
4. Si el envío falla, se reintenta hasta 3 veces con backoff exponencial.
5. El usuario puede activar/desactivar notificaciones por email.
6. Se registra un log de cada notificación enviada (estado, timestamp, intentos).

## Requisitos no funcionales
- Latencia máxima entre cambio de estado y envío del email: 5 minutos.
- El sistema debe soportar 1000 notificaciones/hora sin degradación.
- Los emails deben cumplir con las políticas anti-spam (SPF, DKIM).

## Fuera de alcance
- Notificaciones push o SMS.
- Personalización de frecuencia (digest diario, etc.).
- Notificaciones de otros eventos que no sean cambio de estado de pedido.

Esta spec es clara, tiene alcance definido y especifica qué queda fuera. Ahora viene la pregunta: cómo la convierto en trabajo.


Paso 1: Identificar los bloques funcionales

Antes de crear tickets, identifico los bloques funcionales principales. No son tickets todavía, son agrupaciones lógicas de trabajo:

  1. Infraestructura de eventos: publicar y consumir eventos de cambio de estado.
  2. Servicio de notificaciones: decidir qué notificar, a quién, y gestionar preferencias.
  3. Envío de email: integración con proveedor de email, plantillas, reintentos.
  4. Preferencias de usuario: API y persistencia para activar/desactivar.
  5. Logging y observabilidad: registro de notificaciones enviadas.

Cada bloque se puede desarrollar de forma relativamente independiente, y eso es clave para la descomposición en tickets.


Paso 2: Descomponer en tickets

Cada ticket debe cumplir tres criterios:

  • Autocontenido: se puede implementar y testear sin depender de otros tickets en progreso (aunque sí puede depender de tickets ya completados).
  • Verificable: tiene criterios de aceptación concretos que se pueden comprobar.
  • Dimensionado: un desarrollador (o un agente) puede completarlo en 1-3 días.

Para la spec de notificaciones, la descomposición queda así:

NOTIF-1: Publicación de eventos de cambio de estado de pedido

Descripción: Cuando un pedido cambia de estado, el servicio de pedidos publica un evento OrderStatusChanged en el bus de eventos interno.

Criterios de aceptación:

  • Se publica un evento OrderStatusChanged cada vez que Order.status cambia.
  • El evento contiene: orderId, userId, previousStatus, newStatus, timestamp.
  • El evento se publica de forma asíncrona (no bloquea la transacción del pedido).
  • Si la publicación falla, se loguea el error pero no se revierte el cambio de estado.

Dependencias: ninguna.


NOTIF-2: Consumidor de eventos y servicio de decisión de notificaciones

Descripción: Un consumidor escucha eventos OrderStatusChanged, consulta las preferencias del usuario y decide si debe enviarse una notificación.

Criterios de aceptación:

  • El consumidor procesa eventos OrderStatusChanged.
  • Antes de notificar, verifica que el usuario tiene las notificaciones por email activadas.
  • Si el usuario tiene notificaciones desactivadas, el evento se descarta y se loguea.
  • Si el usuario tiene notificaciones activadas, se crea un registro Notification en estado PENDING.

Dependencias: NOTIF-1.


NOTIF-3: Servicio de envío de email con plantillas

Descripción: Servicio que recibe una notificación pendiente, selecciona la plantilla correspondiente al tipo de evento, renderiza el email y lo envía a través del proveedor de email.

Criterios de aceptación:

  • Existe una plantilla de email para cada estado: CONFIRMED, SHIPPED, DELIVERED, CANCELLED.
  • Cada plantilla incluye: nombre del usuario, número de pedido, estado nuevo, enlace al detalle.
  • El email se envía a la dirección registrada del usuario.
  • Si el envío es exitoso, la notificación pasa a estado SENT.
  • Si el envío falla, la notificación pasa a estado FAILED y se registra el error.

Dependencias: NOTIF-2.


NOTIF-4: Sistema de reintentos para emails fallidos

Descripción: Las notificaciones en estado FAILED se reintentan automáticamente con backoff exponencial, hasta un máximo de 3 intentos.

Criterios de aceptación:

  • Las notificaciones FAILED se reintentan automáticamente.
  • Backoff: 1 minuto, 5 minutos, 15 minutos.
  • Máximo 3 reintentos. Después pasa a estado PERMANENTLY_FAILED.
  • Cada reintento se registra con timestamp y resultado.

Dependencias: NOTIF-3.


NOTIF-5: API de preferencias de notificación del usuario

Descripción: Endpoints para que el usuario pueda consultar y modificar sus preferencias de notificación por email.

Criterios de aceptación:

  • GET /api/v1/users/{userId}/notification-preferences: devuelve las preferencias actuales.
  • PUT /api/v1/users/{userId}/notification-preferences: actualiza las preferencias.
  • Por defecto, las notificaciones por email están activadas para usuarios nuevos.
  • Solo el propio usuario puede modificar sus preferencias (validación de auth).
  • Los cambios son efectivos inmediatamente.

Dependencias: ninguna (se puede desarrollar en paralelo con NOTIF-1).


NOTIF-6: Logging y métricas de notificaciones

Descripción: Registro de cada notificación enviada y métricas para monitorización.

Criterios de aceptación:

  • Cada notificación tiene un registro con: notificationId, userId, orderId, type, status, attempts, timestamps.
  • Métricas expuestas: notificaciones enviadas/minuto, tasa de error, latencia media entre evento y envío.
  • Los logs incluyen suficiente información para debugging sin exponer datos personales del usuario.

Dependencias: NOTIF-3.


Paso 3: Tests derivados de los criterios de aceptación

Aquí está la parte donde la mayoría de equipos hacen trampa. Dicen “sí, luego escribimos los tests” y los tests acaban siendo genéricos o insuficientes. Mi enfoque: cada criterio de aceptación genera al menos un test.

Para NOTIF-2 (el servicio de decisión), los tests serían:

class NotificationDecisionServiceTest {

    private val eventConsumer = NotificationDecisionService(
        notificationRepository = mockk(),
        userPreferencesRepository = mockk(),
        eventPublisher = mockk()
    )

    @Test
    fun `should create pending notification when user has email enabled`() {
        // Given
        val event = OrderStatusChangedEvent(
            orderId = 1L,
            userId = 42L,
            previousStatus = OrderStatus.CONFIRMED,
            newStatus = OrderStatus.SHIPPED,
            timestamp = Instant.now()
        )
        every { userPreferencesRepository.findByUserId(42L) } returns
            UserPreferences(userId = 42L, emailNotificationsEnabled = true)
        every { notificationRepository.save(any()) } answers { firstArg() }

        // When
        eventConsumer.handleOrderStatusChanged(event)

        // Then
        verify {
            notificationRepository.save(match { notification ->
                notification.userId == 42L &&
                notification.orderId == 1L &&
                notification.status == NotificationStatus.PENDING &&
                notification.type == "ORDER_SHIPPED"
            })
        }
    }

    @Test
    fun `should skip notification when user has email disabled`() {
        // Given
        val event = OrderStatusChangedEvent(
            orderId = 1L,
            userId = 42L,
            previousStatus = OrderStatus.CONFIRMED,
            newStatus = OrderStatus.SHIPPED,
            timestamp = Instant.now()
        )
        every { userPreferencesRepository.findByUserId(42L) } returns
            UserPreferences(userId = 42L, emailNotificationsEnabled = false)

        // When
        eventConsumer.handleOrderStatusChanged(event)

        // Then
        verify(exactly = 0) { notificationRepository.save(any()) }
    }

    @Test
    fun `should log discarded notification when user preferences disabled`() {
        // Given
        val event = orderStatusChangedEvent(userId = 42L, newStatus = OrderStatus.SHIPPED)
        every { userPreferencesRepository.findByUserId(42L) } returns
            UserPreferences(userId = 42L, emailNotificationsEnabled = false)

        // When
        eventConsumer.handleOrderStatusChanged(event)

        // Then
        verify { logger.info(match { it.contains("discarded") && it.contains("42") }) }
    }
}

Fíjate en la correspondencia directa:

Criterio de aceptaciónTest
”verifica que el usuario tiene notificaciones activadas”should create pending notification when user has email enabled
”si el usuario tiene notificaciones desactivadas, se descarta”should skip notification when user has email disabled
”se loguea el descarte”should log discarded notification when user preferences disabled

Cada criterio tiene su test. No hay ambigüedad sobre qué se verifica ni cómo.


Paso 4: Prompts para agentes de IA

Cuando tengo los tickets con criterios de aceptación y tests definidos, preparar un prompt para un agente es casi mecánico. El truco está en darle toda la información necesaria sin ruido.

Estructura del prompt que uso:

## Contexto
Proyecto: [nombre] - Spring Boot 3.x con Kotlin
Skills cargadas: backend-conventions.md, testing-patterns.md

## Ticket: NOTIF-3 - Servicio de envío de email con plantillas

### Descripción
Implementar el servicio que toma una notificación en estado PENDING, selecciona
la plantilla de email correspondiente, renderiza el contenido y lo envía a
través del proveedor de email configurado.

### Criterios de aceptación
1. Existe una plantilla para cada estado: CONFIRMED, SHIPPED, DELIVERED, CANCELLED
2. Cada plantilla incluye: nombre del usuario, número de pedido, estado, enlace
3. El email se envía a la dirección registrada del usuario
4. Si el envío es exitoso: notificación pasa a SENT
5. Si el envío falla: notificación pasa a FAILED con error registrado

### Interfaces existentes
- `NotificationRepository`: ya implementado en NOTIF-2
- `EmailProvider`: interfaz a implementar (abstracción sobre el proveedor)
- `Notification`: entidad con campos orderId, userId, type, status, attempts

### Tests esperados
- Unit test: renderizado correcto de cada plantilla
- Unit test: actualización de estado a SENT en caso exitoso
- Unit test: actualización de estado a FAILED en caso de error
- Integration test: envío real con proveedor mock (Testcontainers no aplica aquí)

### Restricciones
- No usar string concatenation para las plantillas. Usar Thymeleaf o similar.
- El EmailProvider debe ser una interfaz (no acoplar a un proveedor concreto).
- Los datos personales del usuario no se loguean en texto plano.

La clave del prompt no es ser largo. Es ser preciso. El agente necesita saber: qué construir, qué criterios cumplir, qué interfaces existen, qué tests escribir y qué restricciones respetar. Con eso y las skills del proyecto, el resultado suele ser bastante bueno.


Errores comunes al descomponer specs

Después de hacer esto muchas veces, estos son los errores que más he visto (y cometido):

1. Tickets demasiado grandes

“Implementar el sistema de notificaciones” no es un ticket. Es un épico. Si un ticket no se puede completar en 1-3 días, necesita más descomposición. Un ticket grande genera PRs enormes, code reviews superficiales y una falsa sensación de progreso.

2. Tickets sin criterios de aceptación verificables

“El sistema debe funcionar correctamente” no es un criterio de aceptación. “Cuando un pedido cambia a estado SHIPPED, se envía un email con el número de pedido y el enlace de seguimiento al email registrado del usuario” sí lo es. La diferencia es que el segundo se puede convertir en un test automatizado.

3. Ignorar las dependencias entre tickets

Si NOTIF-3 necesita que NOTIF-2 esté completo para funcionar, eso tiene que estar explícito. Si no, alguien empieza NOTIF-3 sin tener la interfaz que necesita y pierde medio día averiguando qué le falta.

4. No separar infraestructura de lógica de negocio

Mezclar “configurar el servicio de email” con “implementar la lógica de decisión de notificaciones” en el mismo ticket es una receta para el desastre. Son responsabilidades diferentes, con expertise diferente, y probablemente las hace gente (o agentes) diferentes.

5. Olvidarse del ticket de observabilidad

Siempre hay un ticket para logs, métricas y alertas. Siempre. Si no está explícito, no se hace. Y cuando algo falla en producción, te arrepientes de no haberlo incluido.

6. Criterios de aceptación ambiguos que generan interpretación

“El email se envía rápidamente” es ambiguo. Rápidamente para quién, en qué condiciones, medido cómo. “La latencia entre el cambio de estado y el envío del email es menor a 5 minutos en el percentil 95” es verificable. Si no puedes escribir un test para el criterio, el criterio no está bien escrito.


El flujo completo en una imagen

SPEC (aprobada)

  ├── Identificar bloques funcionales

  ├── Descomponer en tickets
  │     ├── Descripción concreta
  │     ├── Criterios de aceptación verificables
  │     └── Dependencias explícitas

  ├── Derivar tests de los criterios
  │     ├── Un test por criterio (mínimo)
  │     ├── Unit tests para lógica
  │     └── Integration tests para flujo

  └── Preparar prompts para agentes
        ├── Contexto + skills
        ├── Ticket + criterios
        ├── Interfaces existentes
        ├── Tests esperados
        └── Restricciones

Cada paso alimenta al siguiente. La spec define los requisitos, los tickets los hacen ejecutables, los criterios los hacen verificables, los tests los hacen automatizables, y los prompts los hacen delegables a un agente.

No es un proceso burocrático. Es un proceso que reduce ambigüedad en cada paso. Y en un mundo donde parte de tu equipo son agentes de IA que interpretan instrucciones de forma literal, reducir ambigüedad es la diferencia entre un feature bien implementado y un desastre técnico.


Lo que cambió cuando empecé a hacer esto bien

Antes de aplicar este flujo, los sprints terminaban con tickets reabiertos, PRs que necesitaban tres rondas de review y una sensación constante de que “la spec decía una cosa pero se implementó otra”.

Ahora el flujo es más predecible. No perfecto, pero predecible. Los tickets se cierran a la primera más veces. Los agentes generan código que pasa los criterios de aceptación porque los criterios están escritos de forma que un sistema literal los puede verificar. Y los tests existen antes de que el código exista, no después.

No es magia. Es estructura. Y la estructura funciona independientemente de si quien ejecuta el ticket es una persona o un agente.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados