Desarrollo Dirigido por Especificaciones (Spec-Driven Development)

La especificación como fuente de verdad: qué es el Spec-Driven Development, en qué se diferencia de TDD y BDD, cómo escribir una spec efectiva y qué herramientas lo soportan.

Introducción

Durante años, el flujo de trabajo dominante en el desarrollo de software ha sido "código primero, documentación después" (si es que llega a existir). Con la llegada de los asistentes de IA capaces de generar código a partir de lenguaje natural, ese flujo se ha vuelto insuficiente: un prompt ambiguo produce código ambiguo, y un desarrollador que no sabe exactamente qué quiere construir termina iterando a ciegas con el modelo.

El Spec-Driven Development (SDD), o desarrollo dirigido por especificaciones, propone invertir el orden: la especificación —no el código— es el artefacto central del proceso. El código se convierte en una derivación de la especificación, ya sea escrito por un humano o generado por IA.

En este artículo se cubre:

  • Qué es exactamente SDD y en qué se diferencia de TDD, BDD y el desarrollo tradicional
  • Por qué se ha vuelto relevante en la era de los agentes de IA
  • Cómo se estructura una especificación efectiva
  • Un ejemplo práctico completo
  • Herramientas y frameworks que lo soportan (Spec Kit, OpenSpec, entre otros)
  • Errores comunes y buenas prácticas

¿Qué es el Spec-Driven Development?

SDD es una metodología en la que la especificación funcional y técnica de un sistema se trata como la fuente de verdad, y a partir de ella se derivan de forma sistemática: el plan técnico, las tareas de implementación y finalmente el código.

La idea no es nueva —la ingeniería de software formal lleva décadas hablando de especificaciones formales (Z notation, TLA+, etc.)— pero el SDD moderno es más pragmático: no busca demostrar matemáticamente la corrección de un sistema, sino reducir la ambigüedad antes de escribir código, de forma que tanto humanos como modelos de IA trabajen sobre una base compartida y verificable.

La diferencia clave con otros enfoques

Enfoque Artefacto central Cuándo se define el "qué" Rol de las pruebas
Desarrollo tradicional (código primero) Código fuente Implícito, en la cabeza del developer Se escriben después, si acaso
TDD (Test-Driven Development) Tests unitarios A nivel de función/unidad, justo antes de codear Definen el comportamiento línea a línea
BDD (Behavior-Driven Development) Escenarios Given/When/Then A nivel de comportamiento observable Ejecutables, pero centradas en UX/negocio
SDD (Spec-Driven Development) Documento de especificación Antes de cualquier diseño técnico Se derivan de la spec, no la reemplazan

SDD no compite con TDD/BDD: los complementa. Una buena especificación en SDD suele generar los escenarios BDD y los casos de prueba TDD como una de sus salidas, no como el punto de partida.


¿Por qué SDD se ha vuelto relevante ahora?

Tres factores han empujado esta metodología al primer plano:

  1. Los agentes de IA generan código rápido, pero no adivinan intención. Un LLM puede escribir un endpoint FastAPI en segundos, pero si no sabe qué códigos de error debe devolver, qué validaciones aplicar o qué reglas de negocio existen, el resultado es plausible pero incorrecto.
  2. El costo de la ambigüedad se multiplica con la velocidad. Cuando generar código era lento, la ambigüedad se corregía en el camino. Cuando generar código es casi instantáneo, la ambigüedad se amplifica: puedes terminar con miles de líneas construidas sobre un malentendido.
  3. Necesitamos un contrato verificable entre humano y máquina. La especificación actúa como un contrato: el humano valida la especificación (que puede leer y entender), y la IA valida el código contra esa especificación (que puede ejecutar y probar).

Esto ha dado lugar a herramientas específicas como GitHub Spec Kit, OpenSpec o flujos integrados en Claude Code y otros agentes, que estructuran el proceso en fases explícitas.


Las fases del Spec-Driven Development

Aunque cada herramienta lo llama distinto, el flujo típico tiene cuatro fases:

1. Especificación (spec)

Se define qué debe hacer el sistema, no cómo. Incluye:

  • Objetivo y alcance de la funcionalidad
  • Requisitos funcionales, numerados y verificables
  • Casos de uso / historias de usuario
  • Criterios de aceptación
  • Restricciones no funcionales (rendimiento, seguridad, compatibilidad)
  • Fuera de alcance (explícitamente lo que NO se va a construir)

2. Plan técnico (plan)

Se traduce el "qué" en un "cómo" a alto nivel: arquitectura, stack tecnológico, modelos de datos, contratos de API, decisiones de diseño y sus justificaciones (ADRs).

3. Desglose de tareas (tasks)

El plan se descompone en tareas atómicas, ordenadas y con dependencias claras, cada una idealmente pequeña y verificable de forma independiente.

4. Implementación (implement)

Se ejecutan las tareas —por un humano, un agente de IA, o ambos— siempre validando el resultado contra la especificación original, no contra la interpretación del momento.

spec.md  →  plan.md  →  tasks.md  →  código + tests
   ↑______________________________________|
        (la spec sigue siendo la fuente de verdad;
         cualquier cambio de comportamiento se
         actualiza primero aquí)

Ejemplo práctico: especificando un endpoint de autenticación

Vamos a ver cómo se ve una especificación real, y cómo se traduce en código. Supongamos que queremos construir un endpoint de login para una API con FastAPI.

Paso 1 — Especificación

# Spec: Endpoint de autenticación de usuarios

## Objetivo

Permitir que un usuario registrado obtenga un token de acceso
JWT mediante email y contraseña.

## Requisitos funcionales

- RF1: El sistema debe exponer POST /auth/login
- RF2: El request debe aceptar `email` (string, formato email) y
  `password` (string, mínimo 8 caracteres)
- RF3: Si las credenciales son válidas, debe devolver un JWT con
  expiración de 30 minutos y un refresh token con expiración de 7 días
- RF4: Si el email no existe, debe devolver 401 con mensaje genérico
  "Credenciales inválidas" (NO debe revelar si el email existe o no)
- RF5: Si la contraseña es incorrecta, debe devolver el mismo 401
  genérico que RF4
- RF6: Tras 5 intentos fallidos en 15 minutos para el mismo email,
  debe devolver 429 (rate limiting)

## Requisitos no funcionales

- RNF1: Las contraseñas se comparan con verificación de hash bcrypt,
  nunca en texto plano
- RNF2: El endpoint debe responder en menos de 300ms (p95) bajo carga
  normal
- RNF3: Todos los intentos fallidos deben quedar registrados en logs
  de auditoría (sin loggear la contraseña)

## Fuera de alcance

- Login con proveedores externos (OAuth/Google/GitHub) — se
  especificará en un documento aparte
- Recuperación de contraseña

## Criterios de aceptación

- [ ] Dado un usuario válido con credenciales correctas, cuando hace
      POST /auth/login, entonces recibe 200 con access_token y
      refresh_token
- [ ] Dado un email inexistente, cuando hace POST /auth/login,
      entonces recibe 401 con mensaje genérico
- [ ] Dado 6 intentos fallidos consecutivos, cuando hace el sexto
      intento, entonces recibe 429

Nótese algo clave: la especificación ya contiene las decisiones de seguridad (mensaje genérico en 401, rate limiting, bcrypt). Esto no es un detalle menor — es precisamente el tipo de decisión que un LLM no va a inventar por sí solo, y que un desarrollador junior podría pasar por alto.

Paso 2 — Plan técnico (extracto)

# Plan: Endpoint de autenticación

## Stack

- FastAPI + Pydantic para validación de entrada
- passlib[bcrypt] para hashing
- python-jose para JWT
- Redis para rate limiting (contador con TTL)

## Modelo de datos

LoginRequest(email: EmailStr, password: str = Field(min_length=8))
LoginResponse(access_token: str, refresh_token: str, token_type: str)

## Decisiones (ADR)

- Se usa Redis en vez de contador en PostgreSQL para el rate
  limiting por su soporte nativo de TTL y menor latencia
- El mensaje de error es idéntico para "usuario no existe" y
  "password incorrecta" para prevenir enumeración de usuarios

Paso 3 — Tareas derivadas

# Tasks

- [ ] T1: Crear schema Pydantic LoginRequest/LoginResponse
- [ ] T2: Implementar servicio de verificación de credenciales
- [ ] T3: Implementar generación de JWT (access + refresh)
- [ ] T4: Implementar rate limiter con Redis
- [ ] T5: Implementar endpoint POST /auth/login integrando lo anterior
- [ ] T6: Tests: credenciales válidas, email inexistente, password
      incorrecta, rate limiting
- [ ] T7: Logging de auditoría sin exponer contraseñas

Paso 4 — Implementación (fragmento)

from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, EmailStr, Field

router = APIRouter()

class LoginRequest(BaseModel):
    email: EmailStr
    password: str = Field(min_length=8)

class LoginResponse(BaseModel):
    access_token: str
    refresh_token: str
    token_type: str = "bearer"

@router.post("/auth/login", response_model=LoginResponse)
async def login(payload: LoginRequest):
    # RF6: rate limiting antes de cualquier verificación
    if await is_rate_limited(payload.email):
        raise HTTPException(
            status_code=status.HTTP_429_TOO_MANY_REQUESTS,
            detail="Demasiados intentos, intenta más tarde",
        )

    user = await get_user_by_email(payload.email)

    # RF4 + RF5: mismo mensaje genérico en ambos casos
    if not user or not verify_password(payload.password, user.hashed_password):
        await register_failed_attempt(payload.email)
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Credenciales inválidas",
        )

    access_token = create_access_token(user.id, expires_minutes=30)
    refresh_token = create_refresh_token(user.id, expires_days=7)

    return LoginResponse(access_token=access_token, refresh_token=refresh_token)

Cada línea de este código es trazable a un requisito específico de la spec (los comentarios # RF4 + RF5 y # RF6 no son casualidad: son la forma de mantener esa trazabilidad visible).


Herramientas del ecosistema

Herramienta Enfoque Integración con IA
GitHub Spec Kit Framework CLI open source con comandos /specify, /plan, /tasks Diseñado específicamente para agentes como Claude Code, Copilot
OpenSpec Especificaciones como changesets versionados junto al código Agnóstico de modelo, integrable en cualquier flujo Git
Claude Code / Agentic workflows Uso de archivos spec.md como contexto persistente para el agente Nativo, la spec vive en el repo y se referencia en cada sesión
ADRs (Architecture Decision Records) No es SDD puro, pero complementa el "plan" documentando decisiones Se pueden generar/mantener con ayuda de IA

No es imprescindible adoptar una herramienta específica: los tres primeros pasos (spec → plan → tasks) se pueden hacer perfectamente con archivos Markdown en una carpeta /specs dentro del repo, versionados junto al código.


Errores comunes al adoptar SDD

  1. Escribir specs demasiado vagas. "El sistema debe ser rápido y seguro" no es una especificación, es una aspiración. Los requisitos deben ser verificables (RNF2 del ejemplo: "menos de 300ms p95" sí lo es).
  2. Tratar la spec como documentación desechable. Si el código cambia y la spec no se actualiza, esta pierde su valor como fuente de verdad y el equipo vuelve al caos de siempre.
  3. Sobre-especificar el "cómo" en la fase de spec. La especificación describe comportamiento, no implementación. Decidir "usaremos Redis" pertenece al plan técnico, no a la spec funcional.
  4. Saltarse la fase de tareas. Pasar directo de plan a código con un agente de IA suele producir PRs enormes y difíciles de revisar. Descomponer en tareas pequeñas mejora la trazabilidad y la calidad del review.
  5. No versionar las specs junto al código. Si la especificación vive en una herramienta externa (Notion, Confluence) desconectada del repositorio, se desincroniza rápidamente.

Buenas prácticas

  • Una spec por feature o slice vertical, no una spec monolítica para todo el sistema.
  • Numerar los requisitos (RF1, RF2...) para poder referenciarlos en commits, PRs y comentarios de código.
  • Incluir explícitamente qué queda fuera de alcance: previene el "scope creep" tanto en humanos como en agentes de IA.
  • Definir criterios de aceptación verificables, idealmente en formato Given/When/Then, que luego se convierten directamente en tests.
  • Versionar las specs en el mismo repositorio, típicamente en una carpeta /specs, para que evolucionen junto al código.
  • Usar la spec como contexto para el agente de IA en cada sesión de trabajo, no solo al inicio del proyecto.

Conclusión

El Spec-Driven Development no es una moda pasajera ligada a la IA generativa, sino una respuesta lógica a un problema real: cuando la velocidad de generación de código deja de ser el cuello de botella, la claridad de intención pasa a serlo. Invertir tiempo en especificar bien —de forma verificable, versionada y trazable— paga dividendos tanto si el código lo escribe un humano como si lo escribe un agente.

La recomendación práctica para empezar: la próxima vez que vayas a construir una feature no trivial, antes de abrir el editor, escribe primero un spec.md con requisitos numerados y criterios de aceptación. Notarás que muchas decisiones que normalmente se toman "sobre la marcha" —y que generan bugs sutiles— se hacen explícitas desde el principio.