Aprende los principios de abstracción de datos de Barbara Liskov para diseñar interfaces estables, reducir roturas y construir sistemas mantenibles con APIs claras y fiables.

Barbara Liskov es una científica de la computación cuyo trabajo modeló de forma silenciosa cómo los equipos de software modernos construyen cosas que no se desmoronan. Su investigación sobre la abstracción de datos, el ocultamiento de información y, más tarde, el Principio de Sustitución de Liskov (LSP) influyó desde los lenguajes de programación hasta la forma cotidiana en que pensamos sobre las APIs: definir un comportamiento claro, proteger los internos y hacer que sea seguro que otros dependan de tu interfaz.
Una API fiable no es solo “correcta” en sentido teórico. Es una interfaz que hace que un producto avance más rápido:
Esa fiabilidad es una experiencia: para el desarrollador que llama a tu API, para el equipo que la mantiene y para los usuarios que dependen indirectamente de ella.
La abstracción de datos es la idea de que los llamadores deben interactuar con un concepto (una cuenta, una cola, una suscripción) mediante un conjunto pequeño de operaciones, no mediante los desordenados detalles de cómo se almacena o calcula.
Cuando ocultas los detalles de representación, eliminas categorías enteras de errores: nadie puede “depender accidentalmente” de un campo de la base de datos que no debía ser público, ni mutar un estado compartido de una forma que el sistema no pueda manejar. Igual de importante, la abstracción reduce la sobrecarga de coordinación: los equipos no necesitan permiso para refactorizar internals mientras el comportamiento público siga igual.
Al final de este artículo tendrás formas prácticas de:
Si quieres un resumen rápido más tarde, salta a /blog/a-practical-checklist-for-designing-reliable-apis.
La abstracción de datos es una idea simple: interactúas con algo por lo que hace, no por cómo está construido.
Piensa en una máquina expendedora. No necesitas saber cómo giran los motores o cómo se cuentan las monedas. Solo necesitas los controles (“seleccionar artículo”, “pagar”, “recibir artículo”) y las reglas (“si pagas suficiente, obtienes el artículo; si está agotado, te devuelven el dinero”). Eso es abstracción.
En software, la interfaz es el “lo que hace”: los nombres de operaciones, qué entradas aceptan, qué salidas producen y qué errores esperar. La implementación es el “cómo funciona”: tablas de base de datos, estrategia de caché, clases internas y trucos de rendimiento.
Mantener estas capas separadas es como obtienes APIs que permanecen estables aunque el sistema evolucione. Puedes reescribir internals, cambiar librerías u optimizar almacenamiento—mientras la interfaz sigue igual para los usuarios.
Un tipo de dato abstracto es un “contenedor + operaciones permitidas + reglas”, descrito sin comprometerse con una estructura interna específica.
Ejemplo: una Stack (último en entrar, primero en salir).
push(item): añadir un elementopop(): quitar y devolver el elemento añadido más recientementepeek(): ver el elemento superior sin quitarloLa clave es la promesa: pop() devuelve el último push(). Si la pila usa un array, una lista enlazada u otra cosa es privado.
La misma separación aplica en todas partes:
POST /payments es la interfaz; los controles de fraude, reintentos y escrituras en la base son la implementación.client.upload(file) es la interfaz; fragmentación, compresión y solicitudes paralelas son la implementación.Cuando diseñas con abstracción, te centras en el contrato del que dependen los usuarios, y te compras la libertad de cambiar todo detrás del telón sin romperlos.
Un invariante es una regla que siempre debe ser cierta dentro de una abstracción. Si diseñas una API, los invariantes son las guías que impiden que tus datos deriven a estados imposibles—como una cuenta bancaria con dos monedas a la vez, o un pedido “completado” sin artículos.
Piensa en un invariante como “la forma de la realidad” para tu tipo:
Cart no puede contener cantidades negativas.UserEmail siempre es una dirección de correo válida (no “validada más tarde”).Reservation tiene start < end, y ambos tiempos están en la misma zona horaria.Si esas afirmaciones dejan de ser ciertas, tu sistema se vuelve impredecible, porque cada función ahora tiene que adivinar qué significa un dato “roto”.
Las buenas APIs hacen cumplir invariantes en los límites:
Esto mejora el manejo de errores: en lugar de fallos vagos más adelante (“algo salió mal”), la API puede explicar qué regla se violó (“end debe ser posterior a start”).
Los llamadores no deberían memorizar reglas internas como “este método solo funciona después de llamar a normalize().” Si un invariante depende de un ritual especial, no es un invariante—es una trampa.
Diseña la interfaz de modo que:
Al documentar un tipo de API, escribe:
Una buena API no es solo un conjunto de funciones: es una promesa. Los contratos hacen explícita esa promesa, para que los llamadores puedan fiarse del comportamiento y los mantenedores puedan cambiar internals sin sorprender a nadie.
Como mínimo, documenta:
Esta claridad hace el comportamiento predecible: los llamadores saben qué entradas son seguras y qué resultados deberán manejar, y las pruebas pueden verificar la promesa en lugar de adivinar la intención.
Sin contratos, los equipos dependen de memoria y normas informales: “No pases null ahí”, “esa llamada a veces reintenta”, “devuelve vacío en caso de error”. Esas reglas se pierden en la incorporación, refactors o incidentes.
Un contrato escrito convierte esas reglas ocultas en conocimiento compartido. Además crea un objetivo estable para las revisiones de código: las discusiones pasan a ser “¿sigue esta modificación satisfaciendo el contrato?” en vez de “a mí me funcionó”.
Vago: “Crea un usuario.”
Mejor: “Crea un usuario con email único.
email debe ser una dirección válida; el llamador debe tener permiso users:create.userId; el usuario está persistido y recuperable de inmediato.409 si el email ya existe; devuelve 400 por campos inválidos; no se crea un usuario parcial.”Vago: “Obtiene ítems rápidamente.”
Mejor: “Devuelve hasta limit ítems ordenados por createdAt descendente.
nextCursor para la siguiente página; los cursores expiran tras 15 minutos.”El ocultamiento de información es el lado práctico de la abstracción de datos: los llamadores deben depender de qué hace la API, no de cómo lo hace. Si los usuarios no pueden ver tus internos, puedes cambiarlos sin convertir cada release en un cambio rompedor.
Una buena interfaz publica un conjunto pequeño de operaciones (create, fetch, update, list, validate) y mantiene la representación—tablas, cachés, colas, layout de ficheros, límites de servicio—privada.
Por ejemplo, “añadir ítem al carrito” es una operación. “CartRowId” de tu base de datos es un detalle de implementación. Si expones ese detalle, invitas a los usuarios a construir lógica propia alrededor, lo que congela tu capacidad de cambio.
Cuando los clientes solo dependen de comportamiento estable, puedes:
…y la API sigue siendo compatible porque el contrato no se movió. Ese es el verdadero beneficio: estabilidad para usuarios, libertad para mantenedores.
Algunas formas en que los internos se filtran por accidente:
status=3 en lugar de un nombre claro u operación dedicada.Prefiere respuestas que describan significado, no mecánica:
"userId": "usr_…") en lugar de números de fila.Si un detalle puede cambiar, no lo publiques. Si los usuarios lo necesitan, promuévelo a parte deliberada del contrato y documéntalo.
El Principio de Sustitución de Liskov (LSP) en una frase: si un fragmento de código funciona con una interfaz, debería seguir funcionando cuando sustituyas cualquier implementación válida de esa interfaz—sin necesidad de casos especiales.
LSP trata menos de herencia y más de confianza. Cuando publicas una interfaz, estás haciendo una promesa sobre el comportamiento. LSP dice que cada implementación debe mantener esa promesa, aunque use un enfoque interno muy diferente.
Los llamadores confían en lo que tu API declara—no en lo que hace hoy. Si una interfaz dice “puedes llamar a save() con cualquier registro válido”, entonces todas las implementaciones deben aceptar esos registros válidos. Si la interfaz dice “get() devuelve un valor o un resultado claro de ‘no encontrado’”, entonces las implementaciones no pueden lanzar errores nuevos aleatoriamente o devolver datos parciales.
La extensión segura significa que puedes añadir implementaciones (o cambiar proveedores) sin obligar a los usuarios a reescribir código. Ese es el beneficio práctico de LSP: mantiene las interfaces sustituibles.
Dos formas comunes en que las APIs rompen la promesa son:
Entradas más estrictas (precondiciones más estrechas): una nueva implementación rechaza entradas que la definición de la interfaz permitía. Ejemplo: la interfaz acepta cualquier string UTF‑8 como ID, pero una implementación solo acepta IDs numéricos o rechaza campos vacíos válidos.
Salidas más débiles (postcondiciones menos garantizadas): una nueva implementación devuelve menos de lo prometido. Ejemplo: la interfaz dice que los resultados están ordenados, son únicos o completos—pero una implementación devuelve datos desordenados, duplicados o que omite elementos silenciosamente.
Una tercera violación sutil es cambiar el comportamiento de fallo: si una implementación devuelve “no encontrado” mientras otra lanza una excepción para la misma situación, los llamadores no pueden sustituir una por otra con seguridad.
Para soportar “plug-ins” (múltiples implementaciones), escribe la interfaz como un contrato:
Si una implementación realmente necesita reglas más estrictas, no lo ocultes bajo la misma interfaz. O (1) define una interfaz separada, o (2) haz la restricción explícita como una capacidad (por ejemplo, supportsNumericIds() o un requisito de configuración documentado). Así los clientes optan conscientemente—en lugar de ser sorprendidos por un “sustituto” que en realidad no es sustituible.
Una interfaz bien diseñada se siente “obvia” de usar porque expone solo lo que el llamador necesita—y nada más. La visión de Liskov sobre la abstracción de datos te empuja a interfaces estrechas, estables y legibles, para que los usuarios puedan depender de ellas sin aprender detalles internos.
Las APIs grandes suelen mezclar responsabilidades no relacionadas: configuración, cambios de estado, reporting y troubleshooting en un mismo sitio. Eso dificulta entender qué es seguro llamar y cuándo.
Una interfaz cohesiva agrupa operaciones que pertenecen a la misma abstracción. Si tu API representa una cola, céntrate en comportamientos de cola (enqueue/dequeue/peek/size), no en utilidades generales. Menos conceptos significan menos caminos de uso indebido accidental.
“Flexible” a menudo significa “poco claro”. Parámetros como options: any, mode: string o múltiples booleanos (p. ej., force, skipCache, silent) generan combinaciones mal definidas.
Prefiere:
publish() vs publishDraft()), oSi un parámetro obliga a los llamadores a leer el código fuente para saber qué pasa, no es parte de una buena abstracción.
Los nombres comunican el contrato. Elige verbos que describan comportamiento observable: reserve, release, validate, list, get. Evita metáforas ingeniosas y términos sobrecargados. Si dos métodos suenan similares, los llamadores asumirán que se comportan de forma similar—haz que eso sea verdad.
Divide una API cuando notes:
Los módulos separados te permiten evolucionar internals manteniendo la promesa central. Si planeas crecimiento, considera un paquete “core” ligero más add-ons; ver también /blog/evolving-apis-without-breaking-users.
Las APIs rara vez se quedan quietas. Llegan nuevas funciones, se descubren casos límite y “pequeñas mejoras” pueden romper aplicaciones reales. La meta no es congelar una interfaz, sino hacerla evolucionar sin violar las promesas que ya usan los clientes.
El versionado semántico es una herramienta de comunicación:
Su límite: aún hace falta juicio. Si una “corrección” cambia un comportamiento del que los clientes dependían, es rompedor en la práctica—aunque el comportamiento anterior fuera accidental.
Muchos cambios rompedoras no aparecen en un compilador:
Piensa en términos de precondiciones y postcondiciones: qué deben proporcionar los llamadores y qué pueden esperar recibir.
La deprecación funciona cuando es explícita y con plazos:
La abstracción al estilo Liskov ayuda porque estrecha lo que los usuarios pueden depender. Si los llamadores solo dependen del contrato—no de la estructura interna—puedes cambiar formatos de almacenamiento, algoritmos y optimizaciones libremente.
En la práctica, también ayuda el tooling. Por ejemplo, si iteras rápido en una API interna mientras construyes una app React o un backend Go + PostgreSQL, un flujo de trabajo de tipo "vibe-coding" como Koder.ai puede acelerar la implementación sin cambiar la disciplina central: sigues queriendo contratos nítidos, identificadores estables y evolución compatible hacia atrás. La velocidad es un multiplicador—vale la pena multiplicar buenos hábitos de interfaz.
Una API fiable no es la que nunca falla—es la que falla de maneras que los llamadores pueden entender, manejar y probar. El manejo de errores es parte de la abstracción: define qué significa “uso correcto” y qué pasa cuando el mundo (redes, discos, permisos, tiempo) discrepa.
Empieza separando dos categorías:
Esta distinción mantiene honesta la interfaz: los llamadores aprenden qué pueden arreglar en código y qué deben manejar en tiempo de ejecución.
Tu contrato debería implicar el mecanismo:
Ok | Error) cuando los fallos son esperados y quieres que los llamadores los manejen explícitamente.Lo que elijas, sé consistente en toda la API para que los usuarios no tengan que adivinar.
Lista posibles fallos por operación en términos de significado, no detalles de implementación: “conflicto porque la versión está obsoleta”, “no encontrado”, “permiso denegado”, “limitado por tasa”. Proporciona códigos de error estables y campos estructurados para que las pruebas puedan afirmar el comportamiento sin depender de coincidencias de texto.
Documenta si una operación es segura para reintentar, en qué condiciones y cómo lograr idempotencia (claves de idempotencia, IDs naturales de petición). Si el éxito parcial es posible (operaciones por lotes), define cómo se reportan éxitos y fallos, y qué estado deben suponer los llamadores tras un timeout.
Una abstracción es una promesa: “Si llamas a estas operaciones con entradas válidas, obtendrás estos resultados, y estas reglas siempre se mantendrán.” Las pruebas son cómo mantener esa promesa honesta a medida que el código cambia.
Empieza traduciendo el contrato en comprobaciones ejecutables automáticamente.
Las pruebas unitarias deben verificar las postcondiciones y casos límite de cada operación: valores de retorno, cambios de estado y comportamiento de error. Si tu interfaz dice “quitar un ítem inexistente devuelve false y no cambia nada”, escribe exactamente eso.
Las pruebas de integración deben validar el contrato a través de límites reales: base de datos, red, serialización y autenticación. Muchos “incumplimientos de contrato” aparecen solo cuando los tipos se codifican/decodifican o cuando reintentos/timeouts entran en juego.
Los invariantes son reglas que deben mantenerse a través de cualquier secuencia de operaciones válidas (p. ej., “el balance nunca es negativo”, “IDs son únicos”, “ítems devueltos por list() pueden obtenerse con get(id)).
El testing basado en propiedades comprueba estas reglas generando muchas entradas y secuencias de operaciones aleatorias pero válidas, buscando contraejemplos. Conceptualmente, estás diciendo: “No importa en qué orden los usuarios llamen estos métodos, el invariante se mantiene.” Esto es especialmente bueno para encontrar esquinas raras que los humanos no piensan documentar.
Para APIs públicas o compartidas, deja que los consumidores publiquen ejemplos de las requests que hacen y las respuestas de las que dependen. Los proveedores ejecutan estos contratos en CI para confirmar que los cambios no romperán usos reales—aun cuando el equipo proveedor no anticipó ese uso.
Las pruebas no cubren todo, así que monitoriza señales que sugieran que el contrato está cambiando: cambios en la forma de respuesta, aumentos en tasas 4xx/5xx, nuevos códigos de error, picos de latencia y fallos de deserialización por “campo desconocido”. Rastrear esto por endpoint y versión ayuda a detectar deriva temprano y revertir con seguridad.
Si soportas snapshots o rollbacks en tu pipeline de entrega, encajan naturalmente con esta mentalidad: detectar deriva pronto y revertir sin obligar a clientes a adaptarse en medio de un incidente. (Koder.ai, por ejemplo, incluye snapshots y rollback como parte de su flujo, lo que encaja bien con un enfoque “contratos primero, cambios después”).
Incluso equipos que valoran la abstracción caen en patrones que parecen “prácticos” en el momento pero gradualmente convierten una API en un conjunto de casos especiales. Aquí algunos obstáculos recurrentes—y qué hacer en su lugar.
Los feature flags son geniales para rollout, pero el problema empieza cuando las flags se vuelven parámetros públicos y de larga duración: ?useNewPricing=true, mode=legacy, v2=true. Con el tiempo, los llamadores las combinan de formas inesperadas y acabas soportando múltiples comportamientos para siempre.
Un enfoque más seguro:
APIs que exponen IDs de tabla, claves de join o filtros “con forma de SQL” (p. ej., where=...) obligan a los clientes a aprender tu modelo de almacenamiento. Eso hace que los refactors sean dolorosos: un cambio de esquema se vuelve un cambio rompedor de la API.
En su lugar, modela la interfaz alrededor de conceptos del dominio y identificadores estables. Deja que los clientes pidan lo que quieren decir (“pedidos para un cliente en un rango de fechas”), no cómo lo guardas.
Ella popularizó la abstracción de datos y el ocultamiento de información, que se traducen directamente al diseño moderno de APIs: publicar un contrato pequeño y estable y mantener la implementación flexible. El beneficio es práctico: menos cambios incompatibles, refactors más seguros y integraciones más predecibles.
Una API fiable es aquella de la que los consumidores pueden depender a lo largo del tiempo:
La fiabilidad no es “nunca fallar”, sino fallar de forma predecible y respetar el contrato.
Escribe el comportamiento como un contrato:
Incluye casos límite (resultados vacíos, duplicados, orden) para que los consumidores puedan implementar y probar contra la promesa.
Un invariante es una regla que debe mantenerse dentro de una abstracción (p. ej., “la cantidad nunca es negativa”). Haz cumplir los invariantes en los límites:
Así se reducen los errores aguas abajo porque el resto del sistema deja de manejar estados imposibles.
El ocultamiento de información significa exponer operaciones y significado, no la representación interna. Evita acoplar a los consumidores a cosas que podrías cambiar después (tablas, cachés, claves de particionado, estados internos).
Tácticas prácticas:
usr_...) en lugar de IDs de fila de base de datos.Porque congelan tu implementación. Si los clientes dependen de filtros con forma de tabla, claves de join o IDs internas, entonces un refactor de esquema se convierte en un cambio rompedor de la API.
Prefiere preguntas del dominio sobre preguntas de almacenamiento, por ejemplo: “pedidos de un cliente en un rango de fechas”, y mantén el modelo de almacenamiento privado tras el contrato.
LSP significa: si el código funciona con una interfaz, debe seguir funcionando con cualquier implementación válida de esa interfaz sin casos especiales. En términos de API, es la regla de “no sorprender al llamador”.
Para que las implementaciones sean sustituibles, estandariza:
Atento a:
Si una implementación necesita restricciones extra, publica una interfaz separada o una capacidad explícita para que los clientes opten conscientemente.
Mantén las interfaces pequeñas y cohesivas:
options: any y montones de booleanos que crean combinaciones ambiguas.Diseña los errores como parte del contrato:
La consistencia importa más que el mecanismo exacto (excepciones vs tipos ) siempre que los consumidores puedan predecir y manejar los resultados.
status=3).reserve, release, list, validate).Si hay distintos roles o ritmos de cambio, separa módulos/recursos (para más sobre evolución, ver /blog/evolving-apis-without-breaking-users).
Ok | Error