3 puntos por GN⁺ 2024-04-27 | 1 comentarios | Compartir por WhatsApp
  • Increase considera que los recursos de una API determinan cómo los usuarios entienden el producto, por lo que adopta el principio de No Abstractions: en lugar de ocultar la complejidad de las redes de pago, la expone
  • La abstracción al estilo Stripe es fuerte para integraciones rápidas, pero los usuarios de Increase buscan conexiones directas e integraciones profundas basadas en su conocimiento de las redes de pago
  • La API usa directamente la terminología de la red subyacente, como la especificación de Nacha, y modela el progreso de una transferencia ACH con subobjetos inmutables
  • Cuando las acciones posibles para el usuario difieren mucho, separan los recursos como ach_transfer e inbound_ach_transfer; aunque al principio sea más verboso, a largo plazo mejora la previsibilidad
  • El nivel de abstracción debe definirse según la experiencia de dominio y la disposición del desarrollador que integra; si se elige una abstracción baja, ese principio debe mantenerse después

Los recursos de API construyen el modelo mental del usuario

  • Un recurso de API es el sustantivo de una API, y decidir su nombre y modelo es una de las partes más difíciles e importantes del diseño de APIs
  • Qué recursos se exponen construye el modelo mental con el que el usuario entiende cómo funciona el producto y qué puede hacer con él
  • Increase usa un principio de diseño llamado “No Abstractions” para ayudar a tomar esa decisión
  • Diferencias entre la abstracción al estilo Stripe y Increase

    • Stripe destaca por su capacidad de abstraer un dominio de pagos complejo en una API fácil de usar
    • Modela múltiples redes de pago con un recurso de API llamado PaymentIntent, y unifica en un solo enum las diferencias entre los chargeback reason codes de Visa y Mastercard para que el usuario no tenga que considerar ambas redes por separado
    • Muchos usuarios de Stripe son startups tempranas que no están construyendo un producto de pagos en sí, sino otro tipo de producto, y quieren integrarse rápido y volver al desarrollo de su producto principal más que aprender en profundidad los detalles de las tarjetas
    • Los usuarios de Increase ya tienen un conocimiento profundo de las redes de pago, siguen trabajando con tecnología financiera y usan Increase para conectarse directamente a la red y hacer integraciones profundas
    • Quieren saber exactamente cuándo cierra una FedACH window y cuándo llegará una transferencia, y entienden que si cambia el Standard Entry Class code de una transferencia ACH, también puede cambiar el timing del return
    • Si se agruparan las transferencias ACH y wire transfer en un solo recurso de API para ocultar la complejidad de la red subyacente, para los usuarios de Increase eso no sería una simplificación sino una incomodidad

Cómo se refleja No Abstractions en la API

  • Uso de la terminología real de la red

    • Increase prefiere usar el vocabulario de la red subyacente en los nombres de recursos y atributos de la API en lugar de inventar términos nuevos
    • Al construir la API para una transferencia ACH, los parámetros expuestos siguen los nombres de campo de la especificación de Nacha
  • Recursos inmutables y lifecycle object

    • Los recursos también se modelan de acuerdo con eventos o mensajes del mundo real, y este enfoque hace que más recursos de la API sean inmutables
    • Como un conjunto de mensajes de red que pueden enviarse durante el lifecycle de una transferencia ACH, los recursos inmutables se agrupan bajo un lifecycle object con forma de state machine
    • El objeto ach_transfer tiene un campo status que cambia con el tiempo y varios subobjetos inmutables que se crean conforme avanza el lifecycle
    • Un ach_transfer nuevo puede tener status en pending_approval, mientras que approval, submission y acknowledgement pueden estar en null
    • Después de enviarse a FedACH, status pasa a submitted, y approval, submission y acknowledgement se llenan con información inmutable correspondiente a los momentos de aprobación, envío y confirmación
    • submission incluye valores como trace_number y submitted_at
  • Separación de recursos por caso de uso

    • Incluso dentro de un mismo recurso de API, si el conjunto de acciones posibles difiere mucho según la instancia, Increase prefiere dividirlo en varios recursos
    • Como las acciones posibles en una originated ACH transfer y una received ACH transfer son, en la práctica, opuestas, se separan en ach_transfer e inbound_ach_transfer
    • Al principio, este enfoque puede parecer más verboso e incluso intimidante, al punto de que se vean muchos recursos en la barra izquierda de la documentación de la API
    • A cambio, a largo plazo la relación entre recursos y acciones se vuelve más predecible

Los principios reducen las pequeñas decisiones de diseño

  • Al diseñar una API compleja durante varios años, siguen apareciendo pequeñas decisiones, y los principios base definidos al inicio reducen la carga cognitiva de tomarlas
  • El Input Message Accountability Data requerido al enviar una wire transfer a la Reserva Federal funciona como el identificador único global de esa transferencia
  • En una API muy abstraída, un ingeniero podría preguntarse cuál sería el nombre más “amigable para el usuario”: trace_number, reference_number o id
  • En Increase, simplemente se define el nombre del campo como input_message_accountability_data
  • Puede que no sea un nombre fácil de reconocer de inmediato la primera vez que el usuario lo ve, pero ayuda a entender enseguida cómo se mapea con el sistema subyacente

Criterios para decidir el nivel de abstracción

  • No Abstractions no es un principio adecuado para todas las APIs
  • El nivel apropiado de abstracción depende de la experiencia de dominio del desarrollador que integra, de su comprensión del área del producto y de cuánta energía piensa dedicar a la integración
  • Si se construye una API con mucha abstracción, hay que pensar profundamente antes de agregar nuevas funciones
  • Si se construye una API con poca abstracción, hay que comprometerse con esa dirección y resistir la tentación de añadir abstracciones

1 comentarios

 
GN⁺ 2024-04-27
Comentarios de Hacker News
  • Siempre se pueden ofrecer ambas
    Puedes ofrecer una API de bajo nivel que permita un control detallado pero requiera conocimientos profundos, y encima de ella crear una API de alto nivel que mapee los casos de uso comunes a unas cuantas operaciones simples. De todos modos, algunos clientes quizá ya estén implementando torpemente por su cuenta este tipo de capa de alto nivel
    Si separas bien ambas capas, reduces la presión por meter abstracciones en la API de bajo nivel o por agregar imperfecciones y casos especiales a la API de alto nivel. Si el cliente quiere eso, ya existe en la otra API
    Aún mejor si también ofreces material para que los clientes aprendan a pasar de una capa a la otra. Así también puedes atraer a clientes que todavía no conocen a fondo la estructura interna de las redes de pago, pero quieren crecer en esa dirección

    • Debería haber una API de bajo nivel para manejar los casos complejos poco frecuentes, y una API de alto nivel simple para los casos comunes construida encima
      Hoy usé la Web File System API, y para escribir una sola cadena en un archivo necesité 7 llamadas a funciones, la mayoría asíncronas. Eso sin contar el manejo de errores, y además debe hacerse en un worker, cuya configuración también es igual de engorrosa. Se puede ver una complejidad similar en IndexedDB, WebRTC y operaciones normales del DOM, y en Vulkan, DirectX y ffmpeg es muchísimo peor
      Si necesitas cubrir todo tipo de casos especiales, cierto nivel de complejidad está justificado, pero la mayoría de los casos no son así
      El diseño de APIs debería empezar por bosquejar cómo se verá el código que usa la API en los casos comunes, y esos casos deberían ser lo más simples posible. Por ejemplo, la API de fetch lo hizo bastante bien, mientras que XMLHttpRequest no lo hizo en absoluto
      https://developer.mozilla.org/en-US/docs/Web/API/FileSystemS...
      Varias veces he pensado que sería bueno tener una capa API de conveniencia unificada para todas las Web APIs. Algo que envuelva toda esa funcionalidad poderosa en wrappers consistentes de una “biblioteca estándar”, al menos para soportar los casos de uso más comunes. Los navegadores modernos son muy potentes, pero como cada API está diseñada a su manera y es innecesariamente difícil de aprender o usar, ese poder pasa desapercibido o se aprovecha menos
      Algo parecido a lo que jQuery hizo por el DOM, pero con menos magia y menos extras. node.js tiene APIs algo consistentes hasta cierto punto, pero están un poco anticuadas y, por ejemplo, el soporte de Promise es irregular. También se parece a cómo Python busca APIs “pythónicas”
    • Me gusta especialmente este patrón cuando la API de alto nivel que quieres puede implementarse fuera de la librería. Así puedes comprobar si la API de bajo nivel es lo bastante flexible, y además terminas usando tu propia API directamente desde la perspectiva del usuario
      Cuando te acostumbras a la perspectiva de la implementación interna de una herramienta, es demasiado fácil olvidar cómo la usa la gente en realidad
    • Git es un ejemplo de esto
      Tiene comandos “porcelain” de alto nivel como branch y checkout, y comandos “plumbing” de bajo nivel como commit-tree y update-ref
      https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
    • .NET también usa mucho este enfoque. Hay una entrada reciente del blog de desarrollo sobre entrada/salida de archivos: https://devblogs.microsoft.com/dotnet/the-convenience-of-sys...
    • A cambio, la superficie de la API se duplica, así que es un trade-off que hay que considerar. En muchos casos puede ser la decisión correcta
  • Me gustó la parte donde explican por qué Increase eligió otro enfoque. Al diseñar algo fundamental, el contexto es muy importante, pero normalmente la gente no lo reconoce lo suficiente

  • Aquí, “sin abstracciones” en realidad significa usar tal cual la terminología del sistema subyacente, y en general es un buen principio para poner nombres.
    El problema aparece de forma inevitable con el tiempo, cuando los sistemas subyacentes pasan a ser varios, empiezan a ponerle nombres distintos a lo mismo o, peor aún, usan el mismo nombre para cosas distintas. En este ejemplo, ¿qué pasa si los modelos de los proveedores de pagos subyacentes son diferentes? ¿Y qué pasa si la Federal Reserve elimina Input Message Accountability Data y lo reemplaza por un concepto nuevo?
    La industria de pagos quizá sea mucho más simple que el transporte o los protocolos de red. Si construiste un producto de conmutación de paquetes basado en X.25 y luego quieres soportar también TCP/IP, ¿cuál sería la abstracción correcta?

    • Gracias por leer con tanto cuidado.
      Por suerte, el problema de la eliminación no nos afecta mucho porque el sistema subyacente no cambia tanto. Input Message Accountability Data no va a desaparecer. Pero, por ejemplo, si además de Visa empezáramos a emitir tarjetas también en Mastercard, ahí sí tendríamos conflictos.
      También hemos probado algunas abstracciones, y en ese punto podría pasar. Una regla que hemos mantenido siempre es no abstraer los “objetos subyacentes”, sino introducir combinaciones de más alto nivel por conveniencia. Por ejemplo, “Card Payment” en realidad no existe(https://increase.com/documentation/api#card-payments). Es solo una forma de agrupar los mensajes relacionados de autorización y liquidación de tarjeta. Aun así, es muy útil para el usuario y no es fácil trabajar directamente con las contrapartes, así que lo intentamos. Pero creemos que los mensajes de red subyacentes, es decir, los “objetos subyacentes”, y todos sus campos originales también deben estar accesibles desde la API.
      Lamentablemente, las APIs públicas en las que he trabajado han sido 100% del ámbito de pagos, así que me habría gustado tener otra perspectiva.
    • El artículo también deja claro que no significa “unificar objetos parecidos”, y eso es lo que permite tomar decisiones de naming.
    • “Usar tal cual la terminología del sistema subyacente” suena un poco parecido a domain-driven design. Solo que en este caso el “sistema subyacente” podría estar algo sesgado hacia la implementación para ser considerado el dominio real del negocio.
      En DDD normalmente se siguen los nombres y el modelo conceptual que ya creó el dominio del negocio. Si intentas introducir tu propio modelo o terminología “mejorada” [0], aparecen fricciones y malentendidos, aumenta la posibilidad de bugs de integración y terminas ignorando conocimiento especializado validado durante décadas o siglos.
      [0] https://xkcd.com/793/
  • Me gustó el artículo.
    Si te gusta Stripe, yo también, como diseñador y fundador técnico, veo como algo impresionante la simplicidad de Stripe y su capacidad de frontend, y al mirarlos uno puede querer imitarlos en esa habilidad de simplificar y ofrecer experiencias pulidas.
    Pero la verdadera maestría de Stripe está en que conoce muy bien a sus clientes. Y también entiende muy bien la simplicidad que ellos anhelan.
    Por este artículo, Increase parece igual en ese sentido, y da la impresión de haber creado excelentes guías de diseño de producto con un enfoque igual de agudo en lo que necesitan sus clientes. Es inspirador.

    • Cómo Stripe construye su API y sus equipos: https://www.youtube.com/watch?v=IEe-5VOv0Js
    • Incluso en la API de Stripe se ven puntos donde existe esa tensión entre “hagamos esto potencialmente universal” y “aceptemos que esto probablemente solo aplique a un método de pago en un mercado”.
      En lo personal, me gusta más cuando ocurre lo segundo, aunque ahí también entra una decisión estética.
  • Esto se parece al patrón de diseño de lenguaje ubicuo de domain-driven design. Es una forma de hacer que la implementación use tal cual los términos del mundo real que usan los expertos del dominio.
    https://thedomaindrivendesign.io/developing-the-ubiquitous-l...

    • Ya había escuchado una idea parecida mucho antes de que existiera DDD. La idea era que, si los sustantivos y verbos del código no encajan con el dominio del problema, hay un desajuste de impedancia y eso tarde o temprano causará problemas.
      Este artículo me suena como una especie de reacción para evitar la vergüenza. A la gente le desagrada de forma casi patológica decir “yo estaba equivocado” o “estábamos equivocados”, así que termina empujando metáforas de un lado a otro como un niño que mueve las verduras en el plato para que parezca que se las comió.
      También me recuerda la frase “no obvious deficiencies” del discurso de aceptación del Turing Award de Hoare.
  • Este es un buen ejemplo del concepto de lenguaje ubicuo en domain-driven design.
    Hay que usar el lenguaje que entiende el experto del dominio. Si el usuario conoce los archivos NACHA, en cuanto usas otro término obligas a que mantenga ese mapeo mentalmente.
    En cambio, en el caso de Stripe el usuario no es un experto del dominio, así que tiene valor crear abstracciones que sean comprensibles y que oculten detalles innecesarios. Si tienes que enseñarle un lenguaje al usuario, deberías hacerlo lo más simple posible.

    • Dicho de otra forma, ellos son expertos del dominio del tipo de transacción que quieren realizar, no expertos en cómo se implementan las transacciones dentro del sistema financiero.
  • Sin abstracciones como POSIX, las aplicaciones habrían tenido que escribir un adaptador para cada sistema de archivos compatible.

  • Interesante.
    El título de esta idea induce a error. Aquí “sin abstracciones” no significa literalmente ausencia de abstracciones, sino “usamos este conjunto particular de abstracciones y no otras”. El subconjunto específico que describen sí vale la pena discutir, pero evidentemente sigue siendo un conjunto de abstracciones.
    Por ejemplo, cuando dicen “al construir una transferencia ACH como API, nombramos los parámetros expuestos según los nombres de campos de la especificación Nacha”, la especificación en sí misma ya es una abstracción.
    Cuando dicen “como al usar terminología de red, intentamos modelar los recursos según hechos reales, por ejemplo, acciones realizadas o mensajes enviados. Como resultado, más recursos de la API se vuelven inmutables y quedan agrupados bajo objetos de ciclo de vida de máquina de estados”, tanto esa inmutabilidad en ese sentido como los “objetos de ciclo de vida” también son abstracciones.
    Decir que “si en un recurso específico de la API varía mucho el conjunto de acciones que el usuario puede tomar para cada instancia, tendemos a dividirlo en varios recursos” también es otra abstracción. Solo que está dividido en un nivel distinto al de la API de Stripe.
    Al final, esto es un conjunto de decisiones de diseño y abstracciones, no un principio de “sin abstracciones”. La decisión más importante parece ser generalizar lo menos posible, y la generalización también es un tipo de abstracción. Quizá “menos generalización” habría sido un título más preciso.

  • Vi la parte que dice: “La tarifa mensual por usuario que se construye sobre Increase varía según el caso de uso”.
    Ahora mismo estoy agregando acceso por API pública a un endpoint de texto a SQL con IA compatible con RAG, y el mayor problema es el pricing. ¿Alguien sabe más o menos de qué rango de precios estamos hablando? En el precio hay que reflejar los tokens de OpenAI, o alternativamente permitir que el usuario ponga sus propios tokens de OpenAI, el uso de la base de datos, y más adelante también caché y configuración de limitación de velocidad

    • En esencia, el precio debe definirse en función del valor y no del costo[1], así que hay que pensar qué valor tiene para el cliente y partir de ahí.
      Por ejemplo, entiendo que Gong le cobra a muchas organizaciones más de USD 100,000 al año, y aun considerando almacenamiento, CPU y otros costos operativos, es imposible que su costo esté cerca del costo de cómputo. Probablemente haya una diferencia de al menos varios múltiplos. Pero como los equipos de ventas generan ingresos de manera muy directa, el apalancamiento que se puede comprar en forma de una herramienta como Gong tiene un valor inmediato y claro.
      [1]: Una excepción al principio de evitar el pricing de costo más margen es cuando vendes commodities. Pero ese no es tu caso.