Evolucionar una API sin romper compatibilidad
Metadatos de la lección
- Autoría:
- Ignacio Slater-Muñoz
- Última actualización:
- 8 de abril de 2026
Abstract
En la lección anterior vimos qué vuelve buena a una API pública. Pero diseñarla bien una vez no basta. Las bibliotecas evolucionan: agregan capacidades, corrigen errores y revisan decisiones pasadas. La pregunta ahora es otra: cómo cambiar una API publicada sin convertir esa evolución en una fuente de ruptura.
Evolucionar una API no consiste solo en seguir agregando cosas. Una vez publicada, cada cambio debe medirse también por su impacto en quienes ya dependen de esa interfaz. Esta lección propone un criterio para enfrentar ese problema: distinguir qué puede cambiarse con libertad, qué debe preservarse como parte del contrato público y cómo introducir mejoras sin convertir la evolución en una fuente de ruptura innecesaria.
Publicación y ciclo de vida de una API
Publicar crea compromiso
Antes de publicar puedes rediseñar con libertad razonable. Después de publicar, la API adquiere compromisos con quienes dependen de ella. Cada elemento público se convierte en una promesa.
Una API pública tiene un ciclo de vida que determina qué cambios son aceptables en cada momento. En la etapa prerelease todavía es posible rediseñar nombres, firmas y estructuras sin costo externo. Una vez publicada, cada cambio debe evaluarse no solo por su calidad interna, sino también por su impacto en compatibilidad, migración, documentación y flujos de trabajo ya establecidos sobre la interfaz pública.
Cuatro momentos del ciclo de vida
- Prerelease: el diseño todavía está en exploración. Cambios internos y de firma pueden ser sanos porque no hay un contrato público estable.
- Mantenimiento: la API ya fue publicada y ahora debe crecer con cuidado, minimizando ruptura para quien la consume. Conviene preferir adición a modificación destructiva.
- Madurez: la API se considera estable. Los cambios son principalmente correctivos o extensiones menores, siempre respetando compatibilidad.
- Deprecación: la API sigue disponible para no romper dependencias existentes, pero ya no se recomienda para trabajo nuevo. Debe incluir reemplazos claros, advertencias y ruta de migración.
Después de publicar, cada cambio tiene costo externo
Desde el paso a prerelease, el criterio de diseño cambia. Cada modificación debe evaluarse también por su impacto en compatibilidad, migración, documentación y automatizaciones construidas sobre la interfaz pública. Una mejora internamente elegante que rompe código dependiente es un cambio que daña, no que mejora, la biblioteca.
Compatibilidad como contrato observable
La compatibilidad no se limita a la firma
Una API es compatible cuando quien ya la integró puede seguir trabajando sin reescribir su integración ni enfrentar cambios en el comportamiento observable que la API prometía.
Hablar de compatibilidad no es hablar solo de firmas y nombres públicos. El contrato de una API incluye también lo que quien la consume puede observar de forma estable: errores esperados, orden de resultados, valores por defecto, semántica de retorno y momento en que ocurren las validaciones. No toda conducta visible forma parte del contrato: la compatibilidad protege aquello que la API documenta, garantiza o sostiene de manera consistente para sus consumidores.
No toda compatibilidad protege lo mismo
- Compatibilidad de fuente: el código cliente sigue compilando sin cambios.
- Compatibilidad de comportamiento: el comportamiento observable sigue siendo el esperado para los casos ya soportados.
- Compatibilidad binaria: en algunos ecosistemas, reemplazar la biblioteca sin recompilar también importa. No profundizaremos en ABI, pero conviene recordar que no toda ruptura se ve al nivel del código fuente.
Pitfall: pensar que un cambio interno es invisible por definición
Si una función validaba antes de escribir estado y ahora escribe estado antes de fallar, la firma sigue intacta, pero el contrato observable cambió y puede romper integraciones. Lo mismo ocurre si cambia el orden de resultados o el tipo de error ante una condición específica. Interno no siempre significa inobservable. Mientras más pública y madura es una API, mayor cuidado requiere distinguir entre cambios genuinamente invisibles y cambios que alteran el contrato.
Estrategias para agregar sin romper
Ampliar sin reemplazar
Cuando necesitas ampliar una API publicada, suele ser más seguro agregar capacidades nuevas que reemplazar de forma destructiva lo que ya existe.
En evolución de APIs, una regla práctica útil es preferir cambios por adición. Agregar una nueva función, una sobrecarga o un nuevo tipo suele generar menos fricción que modificar una firma ya establecida, porque conserva la ruta existente mientras introduce una alternativa mejor.
fun parseConfig(raw: String, strict: Boolean = false): Config
// Cambio destructivo:
fun parseConfig(raw: String, mode: ParseMode): Config Opciones más seguras para evolucionar
- Agregar una nueva operación: por ejemplo,
cuando la nueva variante expresa un caso de uso distinto.parseConfigWithMode(...)parseConfigWithMode(...) - Agregar una sobrecarga: mantener la forma anterior y sumar una variante más expresiva.
- Introducir una capacidad opt-in: parámetros, opciones o tipos nuevos que amplíen la API sin alterar silenciosamente los casos ya existentes.
- Deprecar con guía: si la nueva ruta reemplaza a otra, conviene marcar la anterior como deprecada y acompañarla con una migración clara.
Agregar también exige criterio
Agregar no es automáticamente bueno. Una API puede volverse inconsistente, redundante o difícil de aprender si acumula variantes sin un diseño claro. Evolucionar de forma compatible no consiste en «no tocar nada» , sino en ampliar la capacidad de la biblioteca sin volver confuso su contrato.
Deprecación, reemplazo y retiro
Deprecación como transición guiada
Deprecar no es abandonar ni borrar. Es declarar que una parte de la API sigue existiendo para no romper dependencias, pero ya no es el camino recomendado para trabajo nuevo. Una deprecación bien diseñada comunica qué se desaconseja, por qué, con qué se reemplaza y cuánto tiempo existe la transición antes de retiro.
La deprecación permite evolucionar una API sin forzar una ruptura inmediata. Su función es dar tiempo para migrar de forma planificada, comunicando con claridad qué deja de recomendarse, por qué existe una alternativa mejor y cuánto durará la transición antes del retiro.
El retiro viene después, no en lugar de la deprecación: es el paso final de una transición manejada. Mientras que deprecar avisa y guía, retirar remover la API después de una ventana suficiente de adaptación. Una buena política de retiro incluye aviso visible en documentación y tooling, reemplazo claro, tiempo razonable entre aviso y retiro, y guía de migración cuando el cambio no es trivial.
Retirar sin transición no solo rompe código: rompe la capacidad de planificar
Si una función desaparece de una versión a la siguiente sin aviso claro, la biblioteca obliga a reaccionar en vez de permitir planificar. Una API confiable no oculta su evolución: la comunica y ofrece una ventana razonable de adaptación. La ruta ordenada es advertir, guiar, esperar y finalmente retirar; no pasar directamente de disponible a removido.
Ejemplo: sustituir una función por otra más precisa
Supón que una biblioteca publicó
y más adelante necesita distinguir distintas políticas de validación. En vez de
sustituirla de inmediato por parse(raw: String): Configparse(raw: String): Config, una evolución más cuidadosa sería:
parse(raw: String, mode: ParseMode): Configparse(raw: String, mode: ParseMode): Config
- Introducir la nueva operación o una nueva sobrecarga.
- Mantener la forma anterior y marcarla como deprecada.
- Explicar con qué se reemplaza, cuándo conviene usarlo y qué cambia en la práctica.
- Retirar la versión anterior solo después de una transición visible, documentada y razonable.
@Deprecated@Deprecated con
alternativa clara
@Deprecated(
message = "Use parse(raw, mode) instead. " +
"The strict validation is now explicit via ParseMode.STRICT.",
replaceWith = ReplaceWith("parse(raw, ParseMode.LENIENT)")
)
fun parse(raw: String): Config = parse(raw, ParseMode.LENIENT)
fun parse(raw: String, mode: ParseMode): Config { /* ... */ }Durante la transición, la forma antigua incluso puede delegar en la nueva para centralizar la lógica sin romper a quienes todavía dependen de la API antigua. Así se evita mantener dos implementaciones distintas mientras se guía a las personas usuarias hacia el camino recomendado.
Versionado como comunicación de cambio
La versión comunica riesgo y cautela
El número de versión no es solo un rótulo. Es una forma de comunicar qué tipo de cambio ocurrió y cuánta cautela conviene tener al actualizar.
En muchos ecosistemas, el esquema de Versionado Semántico (major.minor.patch) funciona como convención:
- Major para cambios incompatibles — por ejemplo, una funcionalidad pasa de deprecada a eliminada, o una función cambia su contrato de forma que rompe expectativas previas.
- Minor para cambios compatibles que agregan capacidad sin romper lo existente — por ejemplo, agregar una función nueva o una sobrecarga.
- Patch para correcciones acotadas sin cambios de API — por ejemplo, corregir un error en la implementación sin alterar la firma ni el contrato observable.
La versión comunica el tamaño esperado del cambio; el changelog y la guía de migración explican cómo actuar frente a él. Una versión bien usada no oculta rupturas ni erosiona confianza, incluso cuando introduce cambios mayores.
La excepción de las versiones tempranas (0.x)
En etapas de exploración, muchas bibliotecas usan 0.major.minor para
comunicar que la API todavía puede cambiar con mayor libertad. Aun así, esa
inestabilidad debe comunicarse con claridad: incluso en 0.x, quien depende de la
biblioteca merece saber cuándo esperar cambios incompatibles y cuándo contar con
estabilidad relativa.
Criterio práctico para quien actualiza
No pienses el versionado solo desde quien publica la biblioteca. Piénsalo desde la persona que actualiza una dependencia en un proyecto real. Lo que esa persona necesita saber no es solo «qué cambió» , sino qué riesgo asumo actualizando, qué acciones debo tomar y cuánto tiempo me exige la migración. Una versión mayor, un changelog claro y una guía de migración juntos responden esa pregunta. Separados, no bastan.
Tests de regresión como red de seguridad
El contrato también se fija con tests
Si la API pública es un contrato, entonces los tests de regresión son una forma de volver ese contrato ejecutable y verificable cada vez que la biblioteca cambia.
La evolución compatible no se sostiene solo con criterio de diseño ni con buena comunicación. También necesita verificación. El versionado puede anticipar riesgo y la deprecación puede guiar la transición, pero los tests de regresión son los que permiten comprobar que una mejora no alteró silenciosamente el contrato observable de la API.
Una lección habitual en evolución de APIs es que no basta con confiar en memoria o intuición. Los tests de regresión permiten verificar que una nueva versión sigue respetando expectativas ya públicas: resultados prometidos, errores documentados, secuencias de uso válidas y casos límite que otras personas ya integraron en su trabajo.
En este contexto, los tests más valiosos no son los que describen detalles internos de la implementación, sino los que fijan comportamientos públicos que otras personas pueden observar y esperar de forma estable. Un buen test de regresión no protege cualquier detalle: protege aquello que la API hizo razonable depender.
test("parseConfig should use default timeout of 5 seconds") {
val config = parseConfig("timeout=5")
config.timeout shouldBe 5.seconds
}En general, conviene escribir estos tests desde la perspectiva de quien consume la API, no desde la estructura interna de su implementación. Si los cambios internos no alteran lo que esta persona observa, los tests deberían seguir pasando; si pasan pero el API se vuelve más confusa o difícil de migrar, entonces los tests no fueron suficientemente exhaustivos.
Qué conviene fijar con tests de regresión
- Casos de uso centrales ya publicados.
- Errores o validaciones que quien consume probablemente maneja de forma explícita.
- Comportamientos que deben mantenerse al agregar nuevas rutas compatibles.
Conclusiones
Evolucionar una API sin romper compatibilidad implica reconocer que publicar crea compromisos. Desde ese momento, cada cambio debe evaluarse también por su impacto en quienes ya dependen de la interfaz pública.
Cambiar bien no significa dejar todo inmóvil, sino ampliar con criterio, guiar las transiciones cuando haga falta y verificar con tests que el contrato observable sigue cumpliéndose. Una buena API puede evolucionar sin volverse una fuente de ruptura innecesaria.
Puntos clave
- Publicar una API crea compromisos: después de ese punto, cada cambio tiene costo externo.
- La compatibilidad protege el contrato observable, no solo firmas y nombres.
- Cuando sea posible, conviene ampliar por adición antes que reemplazar de forma destructiva.
- Deprecar bien implica advertir, justificar, reemplazar y dar tiempo antes de retirar.
- La versión comunica el riesgo esperado de actualizar; el changelog y la migración explican cómo actuar.
- Los tests de regresión fijan expectativas públicas y ayudan a evitar rupturas silenciosas.
Reflexión de cierre
Hemos visto que evolucionar una API bien requiere decisiones: qué cambios son seguros, cómo marcar lo que se depreca, cómo comunicar riesgo mediante versiones, y cómo fijar expectativas con tests. Pero estas decisiones de ingeniería todavía son insuficientes. El versionado comunica un número; los tests ejecutables verifican expectativas; pero para que otras personas comprendan qué cambió, por qué, qué alternativas existen y cómo migrar, necesitamos comunicación clara.
Por eso la documentación no es un adorno que se agrega después. Cuando la API evolucionó bien, la documentación se convierte en parte del contrato: no es un manual secundario, sino el lugar donde se articula el cambio, se justifican las rupturas, se guían las migraciones y se sostiene la confianza en el criterio de quien mantiene la biblioteca. Eso es lo que estudiaremos a continuación.
¿Con ganas de más?
Referencias recomendadas
- Recomendado si quieres profundizar en la parte más operativa de esta lección: distingue con más detalle los tipos de compatibilidad, desarrolla estrategias de versionado, deprecación y retiro, y muestra por qué mantener una API exige revisar cambios con el mismo cuidado con que se diseña.
Referencias adicionales
- Recomendado si quieres profundizar en ejemplos concretos de evolución de APIs: muestra con bastante detalle qué tipos de cambios introducen breaking changes, cómo evaluar distintas estrategias de versionado y por qué diseñar con extensibilidad en mente reduce el costo de cambiar sin romper.
- Recomendado si quieres ampliar la lección hacia el trabajo que rodea la evolución de una API: conecta compatibilidad y cambio con ciclo de vida, guías de diseño, revisiones y comunicación, mostrando que mantener una biblioteca no es solo una cuestión de código, sino también de coordinación y criterio compartido.
- Útil para profundizar en tests de regresión y en otras estrategias de testing para APIs, especialmente si quieres pasar de la idea general de «verificar el contrato» a prácticas más concretas.