D'una SPEC a tickets, tests i criteris d'acceptació: flux real de SDD
Com transformar una spec en treball executable: tickets, criteris d'acceptació, tests i prompts per a agents d'IA.

Escriure una bona spec és la meitat de la feina. L’altra meitat és convertir-la en alguna cosa que algú (persona o agent) pugui executar. I és just aquí on la majoria d’equips fallen.
Als articles anteriors sobre SDD i com escriure una spec parlava de per què les specs importen i com escriure-les bé. Però em quedava el pas més pràctic: com passar d’una spec aprovada a tickets concrets, criteris d’acceptació verificables, tests derivats d’aquests criteris i prompts que un agent d’IA pugui fer servir per implementar cada ticket.
Aquest article és aquell pont. El que faig servir quan tinc una spec signada i necessito que l’equip (humà i agents) comenci a treballar.
El problema: specs que no s’executen
He vist specs tècnicament perfectes que es queden en un Google Doc i mai es converteixen en treball real. L’equip llegeix la spec, assenteix a la reunió de planning, i després cadascú interpreta pel seu compte què cal fer. Es creen tickets vagues tipus “Implementar sistema de notificacions” i el desenvolupador (o l’agent) ha d’endevinar l’abast.
El resultat: tickets que es reobren perquè “faltava això”, PRs que no passen review perquè “això no era el que es demanava”, i una spec que ningú torna a consultar després del primer dia.
Una spec que no es descompon en treball executable és un document d’intencions, no una eina de treball.
Exemple concret: spec d’un sistema de notificacions
Faré servir un exemple real (simplificat) per mostrar tot el flux. Imagina que la spec descriu un sistema de notificacions per email per a una plataforma de comandes.
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.Aquesta spec és clara, té abast definit i especifica què queda fora. Ara ve la pregunta: com la converteixo en treball.
Pas 1: Identificar els blocs funcionals
Abans de crear tickets, identifico els blocs funcionals principals. No són tickets encara, són agrupacions lògiques de treball:
- Infraestructura d’esdeveniments: publicar i consumir esdeveniments de canvi d’estat.
- Servei de notificacions: decidir què notificar, a qui, i gestionar preferències.
- Enviament d’email: integració amb proveïdor d’email, plantilles, reintents.
- Preferències d’usuari: API i persistència per activar/desactivar.
- Logging i observabilitat: registre de notificacions enviades.
Cada bloc es pot desenvolupar de forma relativament independent, i això és clau per a la descomposició en tickets.
Pas 2: Descompondre en tickets
Cada ticket ha de complir tres criteris:
- Autocontingut: es pot implementar i testejar sense dependre d’altres tickets en progrés (encara que sí pot dependre de tickets ja completats).
- Verificable: té criteris d’acceptació concrets que es poden comprovar.
- Dimensionat: un desenvolupador (o un agent) pot completar-lo en 1-3 dies.
Per a la spec de notificacions, la descomposició queda així:
NOTIF-1: Publicació d’esdeveniments de canvi d’estat de comanda
Descripció: Quan una comanda canvia d’estat, el servei de comandes publica un esdeveniment OrderStatusChanged al bus d’esdeveniments intern.
Criteris d’acceptació:
- Es publica un esdeveniment
OrderStatusChangedcada vegada queOrder.statuscanvia. - L’esdeveniment conté:
orderId,userId,previousStatus,newStatus,timestamp. - L’esdeveniment es publica de forma asíncrona (no bloqueja la transacció de la comanda).
- Si la publicació falla, es registra l’error però no es reverteix el canvi d’estat.
Dependències: cap.
NOTIF-2: Consumidor d’esdeveniments i servei de decisió de notificacions
Descripció: Un consumidor escolta esdeveniments OrderStatusChanged, consulta les preferències de l’usuari i decideix si s’ha d’enviar una notificació.
Criteris d’acceptació:
- El consumidor processa esdeveniments
OrderStatusChanged. - Abans de notificar, verifica que l’usuari té les notificacions per email activades.
- Si l’usuari té les notificacions desactivades, l’esdeveniment es descarta i es registra.
- Si l’usuari té les notificacions activades, es crea un registre
Notificationen estatPENDING.
Dependències: NOTIF-1.
NOTIF-3: Servei d’enviament d’email amb plantilles
Descripció: Servei que rep una notificació pendent, selecciona la plantilla corresponent al tipus d’esdeveniment, renderitza l’email i l’envia a través del proveïdor d’email.
Criteris d’acceptació:
- Existeix una plantilla d’email per a cada estat:
CONFIRMED,SHIPPED,DELIVERED,CANCELLED. - Cada plantilla inclou: nom de l’usuari, número de comanda, estat nou, enllaç al detall.
- L’email s’envia a l’adreça registrada de l’usuari.
- Si l’enviament és exitós, la notificació passa a estat
SENT. - Si l’enviament falla, la notificació passa a estat
FAILEDi es registra l’error.
Dependències: NOTIF-2.
NOTIF-4: Sistema de reintents per a emails fallits
Descripció: Les notificacions en estat FAILED es reintenten automàticament amb backoff exponencial, fins a un màxim de 3 intents.
Criteris d’acceptació:
- Les notificacions
FAILEDes reintenten automàticament. - Backoff: 1 minut, 5 minuts, 15 minuts.
- Màxim 3 reintents. Després passa a estat
PERMANENTLY_FAILED. - Cada reintent es registra amb timestamp i resultat.
Dependències: NOTIF-3.
NOTIF-5: API de preferències de notificació de l’usuari
Descripció: Endpoints perquè l’usuari pugui consultar i modificar les seves preferències de notificació per email.
Criteris d’acceptació:
GET /api/v1/users/{userId}/notification-preferences: retorna les preferències actuals.PUT /api/v1/users/{userId}/notification-preferences: actualitza les preferències.- Per defecte, les notificacions per email estan activades per a usuaris nous.
- Només el propi usuari pot modificar les seves preferències (validació d’auth).
- Els canvis són efectius immediatament.
Dependències: cap (es pot desenvolupar en paral·lel amb NOTIF-1).
NOTIF-6: Logging i mètriques de notificacions
Descripció: Registre de cada notificació enviada i mètriques per a monitorització.
Criteris d’acceptació:
- Cada notificació té un registre amb:
notificationId,userId,orderId,type,status,attempts,timestamps. - Mètriques exposades: notificacions enviades/minut, taxa d’error, latència mitjana entre esdeveniment i enviament.
- Els logs inclouen prou informació per a debugging sense exposar dades personals de l’usuari.
Dependències: NOTIF-3.
Pas 3: Tests derivats dels criteris d’acceptació
Aquí és on la majoria d’equips fan trampa. Diuen “sí, després escriurem els tests” i els tests acaben sent genèrics o insuficients. El meu enfocament: cada criteri d’acceptació genera almenys un test.
Per a NOTIF-2 (el servei de decisió), els tests serien:
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") }) }
}
}Fixa’t en la correspondència directa:
| Criteri d’acceptació | Test |
|---|---|
| ”verifica que l’usuari té notificacions activades” | should create pending notification when user has email enabled |
| ”si l’usuari té les notificacions desactivades, es descarta” | should skip notification when user has email disabled |
| ”es registra el descart” | should log discarded notification when user preferences disabled |
Cada criteri té el seu test. No hi ha ambigüitat sobre què es verifica ni com.
Pas 4: Prompts per a agents d’IA
Quan tinc els tickets amb criteris d’acceptació i tests definits, preparar un prompt per a un agent és gairebé mecànic. El truc és donar-li tota la informació necessària sense soroll.
Estructura del prompt que faig servir:
## 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 clau del prompt no és ser llarg. És ser precís. L’agent necessita saber: què construir, quins criteris complir, quines interfícies existeixen, quins tests escriure i quines restriccions respectar. Amb això i les skills del projecte, el resultat acostuma a ser força bo.
Errors comuns en descompondre specs
Després de fer això moltes vegades, aquests són els errors que més he vist (i comès):
1. Tickets massa grans
“Implementar el sistema de notificacions” no és un ticket. És un èpic. Si un ticket no es pot completar en 1-3 dies, necessita més descomposició. Un ticket gran genera PRs enormes, code reviews superficials i una falsa sensació de progrés.
2. Tickets sense criteris d’acceptació verificables
“El sistema ha de funcionar correctament” no és un criteri d’acceptació. “Quan una comanda canvia a estat SHIPPED, s’envia un email amb el número de comanda i l’enllaç de seguiment a l’email registrat de l’usuari” sí que ho és. La diferència és que el segon es pot convertir en un test automatitzat.
3. Ignorar les dependències entre tickets
Si NOTIF-3 necessita que NOTIF-2 estigui complet per funcionar, això ha d’estar explícit. Si no, algú comença NOTIF-3 sense tenir la interfície que necessita i perd mig dia esbrinant què li falta.
4. No separar infraestructura de lògica de negoci
Barrejar “configurar el servei d’email” amb “implementar la lògica de decisió de notificacions” al mateix ticket és una recepta per al desastre. Són responsabilitats diferents, amb expertise diferent, i probablement les fa gent (o agents) diferent.
5. Oblidar-se del ticket d’observabilitat
Sempre hi ha un ticket per a logs, mètriques i alertes. Sempre. Si no està explícit, no es fa. I quan alguna cosa falla a producció, et penediràs de no haver-lo inclòs.
6. Criteris d’acceptació ambigus que generen interpretació
“L’email s’envia ràpidament” és ambigu. Ràpidament per a qui, en quines condicions, mesurat com. “La latència entre el canvi d’estat i l’enviament de l’email és menor a 5 minuts al percentil 95” és verificable. Si no pots escriure un test per al criteri, el criteri no està ben escrit.
El flux complet en una imatge
SPEC (aprovada)
│
├── Identificar blocs funcionals
│
├── Descompondre en tickets
│ ├── Descripció concreta
│ ├── Criteris d'acceptació verificables
│ └── Dependències explícites
│
├── Derivar tests dels criteris
│ ├── Un test per criteri (mínim)
│ ├── Unit tests per a lògica
│ └── Integration tests per a flux
│
└── Preparar prompts per a agents
├── Context + skills
├── Ticket + criteris
├── Interfícies existents
├── Tests esperats
└── RestriccionsCada pas alimenta el següent. La spec defineix els requisits, els tickets els fan executables, els criteris els fan verificables, els tests els fan automatitzables, i els prompts els fan delegables a un agent.
No és un procés burocràtic. És un procés que redueix ambigüitat a cada pas. I en un món on part del teu equip són agents d’IA que interpreten instruccions de forma literal, reduir ambigüitat és la diferència entre una feature ben implementada i un desastre tècnic.
El que va canviar quan vaig començar a fer-ho bé
Abans d’aplicar aquest flux, els sprints acabaven amb tickets reoberts, PRs que necessitaven tres rondes de review i una sensació constant que “la spec deia una cosa però es va implementar una altra”.
Ara el flux és més previsible. No perfecte, però previsible. Els tickets es tanquen a la primera més vegades. Els agents generen codi que passa els criteris d’acceptació perquè els criteris estan escrits de manera que un sistema literal els pot verificar. I els tests existeixen abans que el codi existeixi, no després.
No és màgia. És estructura. I l’estructura funciona independentment de si qui executa el ticket és una persona o un agent.

