Una API aquí es la local: la firma de una función, una interfaz, la superficie exportada de una librería. Las mismas reglas rigen recursivamente para APIs HTTP y servicios RPC, pero el oficio de la API pequeña es el cimiento — la mayor parte del dolor de “los sistemas distribuidos son difíciles” es, al inspeccionarlo, que la API local estaba mal diseñada primero.
Este tema trata de los movimientos de diseño que hacen que las APIs sobrevivan a sus llamadores a lo largo de años de rotación de llamadores.
Diseñar la firma de una función
Una firma es un contrato escrito una vez y leído miles de veces. Tres reglas componen:
- Pocos parámetros posicionales. Más allá de tres argumentos posicionales, cada callsite falla al primer intento. Pasa a un objeto de opciones cuando
n > 3o cuando cualquier parámetro es opcional. - Obligatorios primero, opcionales al final. El orden sorpresivo es el bug más común al llamar. Obligatorio → opcional → defaults.
- Los booleanos suelen ser un olor.
setMode(true)se lee como código de formulario fiscal;enableFeature()ydisableFeature()se leen como intención. Prefiere enums o dos funciones cuando el booleano significa un cambio de modo.
Mal: redirectTo(user, targetUrl, true, false) // qué son 1, 0?
OK: redirectTo(user, { target: url, preserveHistory: true, force: false })
Mejor: redirectTo(user, { target: url }) // cuando los defaults sirven al 95% de las llamadas
Formas de retorno
Una función devuelve o un valor o una disposición (éxito o fallo). La forma del retorno le señala al llamador cómo manejarlo.
| Forma de retorno | Cuándo es correcta | Modo de fallo |
|---|---|---|
Valor o null | Un caso verdaderamente ausente | null se propaga; los llamadores olvidan el check |
| Valor o throw | Fallo verdaderamente excepcional | El catch-all se traga en el código del llamador |
Result<T, E> | Dos resultados esperados | Algunos lenguajes no tienen soporte nativo |
| Optional / monad nullable | La semántica de pipeline | Obliga al downstream a reconocer la ausencia |
La regla que envejece bien: prefiere Result/error sobre null sobre throw. El llamador no puede ignorar un Result — el compilador lo obliga a manejar ambos casos. null es olvidable. Las excepciones a veces se olvidan y a veces son demasiado ruidosas (la función aborta un test no relacionado del llamador).
Naming que escala
Un nombre es la documentación que viaja con la función. Tres pruebas:
- La prueba de una llamada: desde cualquier callsite, ¿se entiende solo el propósito de la función?
fetchUser(id)no debería necesitar un comentario. - La prueba de la señal: ¿el nombre se desambigua de sus hermanos?
getUseryfindUserdeberían diferir — uno lanza y el otro devuelvenull? NómbralosrequireUseryfindUser. - La prueba de la honestidad:
validateXno debería también normalizar X; si lo hace, renómbralo anormaliseX. Los verbos honestos permiten nombres honestos.
El costo de un mal nombre es cada lector futuro. Invierte el minuto extra.
El Pit of Success
Una API es un pit of success si la manera más fácil de usarla es la manera correcta. Lo opuesto — un pit of failure — hace que el camino incorrecto sea más corto que el correcto.
Pit of failure:
Map<String, Object> config = new HashMap<>();
config.put("timeout", 5000); // ¿segundos o milisegundos?
config.put("secure", "yes"); // cualquier string sirve, solo "yes" significa sí
Pit of success:
client.withTimeout(Duration.ofSeconds(5))
.withSecureFlag(true) // tipos fuertes, métodos nombrados
Tres propiedades crean el pit:
- Tipos fuertes — una clave de config mal escrita es un error de compilación, no un fallo silencioso en runtime.
- Estilo builder — cada método devuelve el tipo que se está configurando, así que el autocompletado guía al llamador.
- Defaults sensatos — el caso del 95% es una línea; el del 5% queda más claro que antes.
Versionado de internos
Las APIs internas sí necesitan versionado — cualquier función llamada desde más de un módulo es una API interna. El mínimo:
- Versionado semántico a nivel de paquete: MAJOR.MINOR.PATCH. Un cambio breaking es siempre un bump de major; el número de major es el contrato de compatibilidad.
- Ciclo de deprecación para cualquier cambio breaking:
- Marca el símbolo antiguo como
@deprecatedcon una nota de migración apuntando al reemplazo. - Añade el reemplazo en paralelo, habilitado por defecto para los nuevos llamadores.
- Tras un ciclo de release, rutea los llamadores restantes al nuevo símbolo.
- Tras otro ciclo de release, elimina el símbolo antiguo.
- Marca el símbolo antiguo como
Saltarse el ciclo es la causa de cada historia de “actualizamos y se rompió todo”.
Deprecación como proceso
Matar una API es más difícil que escribirla. El proceso:
| Etapa | Estado de la audiencia | Costo para el desarrollador |
|---|---|---|
| Anunciar | Todos los llamadores actuales | Escribir la doc de migración; desplegar el reemplazo |
| Coexistir | Código nuevo usa reemplazo; el viejo aún funciona | Mantener ambos durante la transición |
| Advertir | Todos los llamadores ven un log de deprecation | Algunos migrarán; otros no |
| Prohibir (si hace falta) | El símbolo viejo lanza o está gateado | Ahora eres dueño del costo del rollout |
| Eliminar | El símbolo viejo desaparece | La mayoría migró; el breakage audita al resto |
El periodo de gracia depende de tu audiencia. Una librería interna puede moverse en un trimestre. Una librería open-source pública puede necesitar años. La disciplina es: nunca rompas en silencio, nunca rompas sin un camino, nunca rompas sin una fecha.
Trayectoria de práctica
- Elige una función con
n > 3parámetros posicionales; refactoriza a un objeto de opciones. Compara los callsites antes y después. - Encuentra una función que devuelve
nullo nullable que los llamadores olvidan chequear; reescribe su retorno comoResult<T, E>(o el equivalente de tu lenguaje) y observa cómo el compilador obliga a manejarlo. - Audita un parámetro booleano en una función exportada: reemplázalo por dos métodos nombrados o un enum, y documenta cuál era el correcto para cada callsite.
- Añade un ciclo de deprecación a un símbolo en tu propio código: anuncia en una tag, coexiste en la siguiente, advierte en la siguiente.
- Lee una API de una librería desconocida que uses; identifica un pit of failure en ella. Propón — no en un PR, en papel — el cambio de forma más pequeño que la convertiría en un pit of success.
Cuándo es la herramienta correcta
| Situación | Conclusión |
|---|---|
| Firmas que acumulan más parámetros opcionales | Refactoriza a objeto de opciones antes de que la sorpresa del orden posicional muerda |
| Funciones que devuelven nullable y los llamadores lo ignoran | Pasa a Result para que el compilador enforce el manejo |
Hermanos indistinguibles en el naming (get vs find) | Renombra para expresar la disposición, no solo la acción |
| Cambio breaking propuesto | Agenda el ciclo de deprecación; nunca rompas en silencio |
| API nueva en diseño | Haz que el camino correcto sea el camino fácil: pit of success, no pit of failure |