- Increase considère que les ressources d’API façonnent la compréhension qu’ont les utilisateurs du produit, et adopte le principe No Abstractions : plutôt que de masquer la complexité des réseaux de paiement, l’exposer
- Les abstractions à la Stripe sont efficaces pour une intégration rapide, mais les utilisateurs d’Increase recherchent une connexion directe et une intégration profonde fondées sur leur connaissance des payment networks
- L’API reprend telle quelle la terminologie des réseaux sous-jacents, comme dans la Nacha specification, et modélise le déroulement d’un ACH transfer à l’aide de sous-objets immuables
- Lorsque les actions possibles diffèrent fortement pour l’utilisateur, les ressources sont séparées, comme
ach_transferetinbound_ach_transfer, ce qui peut sembler verbeux au départ mais améliore la prévisibilité sur le long terme - Le niveau d’abstraction doit être défini en fonction de l’expérience métier du développeur chargé de l’intégration et de l’énergie qu’il est prêt à y consacrer ; si l’on choisit une faible abstraction, il faut ensuite rester cohérent avec ce principe
Les ressources d’API construisent le modèle mental des utilisateurs
- Une API resource est le nom de l’API ; choisir son nom et son modèle fait partie des tâches les plus difficiles et les plus importantes de la conception d’API
- Les ressources que l’on expose construisent le modèle mental qui permet aux utilisateurs de comprendre le fonctionnement du produit et les actions possibles
- Pour guider ces choix, Increase applique un principe de conception appelé « No Abstractions »
-
Différence entre l’abstraction façon Stripe et l’approche d’Increase
- Stripe excelle dans l’abstraction : transformer un domaine de paiement complexe en une API facile à manipuler
- Plusieurs réseaux de paiement sont modélisés via une API resource appelée
PaymentIntent, et les différences entre les chargeback reason codes de Visa et Mastercard sont regroupées dans un seul enum afin que l’utilisateur n’ait pas à raisonner séparément sur chaque réseau - Une grande partie des utilisateurs de Stripe sont des startups en phase initiale qui construisent un produit autre que le paiement lui-même ; elles veulent intégrer rapidement la solution puis revenir au développement de leur produit principal, plutôt que d’approfondir les détails des cartes bancaires
- Les utilisateurs d’Increase disposent déjà d’une solide connaissance des payment networks, travaillent en continu sur des sujets de technologie financière, et utilisent Increase pour une connexion directe aux réseaux et une intégration profonde
- Ils veulent savoir précisément quand la fenêtre FedACH se ferme et quand un transfer arrivera, et comprennent qu’un changement de Standard Entry Class code d’un ACH transfer peut aussi modifier le délai d’un return
- Regrouper ACH transfer et wire transfer dans une même API resource pour masquer la complexité des réseaux sous-jacents ne simplifierait pas les choses pour les utilisateurs d’Increase ; ce serait au contraire une gêne
Comment No Abstractions se reflète dans l’API
-
Utiliser la terminologie réelle des réseaux
- Increase préfère réutiliser le vocabulaire des réseaux sous-jacents plutôt que d’inventer de nouveaux noms pour ses API resources et attributs
- Lors de la conception de l’API pour un ACH transfer, les paramètres exposés reprennent les noms de champs de la Nacha specification
-
Ressources immuables et lifecycle object
- Les ressources sont elles aussi modélisées d’après les événements et messages du monde réel, ce qui conduit à rendre davantage d’API resources immuables
- Comme un ensemble de messages réseau pouvant être envoyés au cours du cycle de vie d’un ACH transfer, ces ressources immuables sont regroupées sous un lifecycle object en forme de machine à états
- L’objet
ach_transferpossède un champstatusqui évolue dans le temps, ainsi que plusieurs sous-objets immuables créés au fil de l’avancement du cycle de vie - Un nouvel
ach_transferpeut avoir unstatusàpending_approval, avecapproval,submissionetacknowledgementànull - Une fois soumis à FedACH, le
statusdevientsubmitted, etapproval,submissionetacknowledgementsont remplis avec les informations immuables correspondant respectivement aux moments d’approbation, de soumission et d’accusé de réception submissioncontient des valeurs commetrace_numberetsubmitted_at
-
Séparer les ressources selon les cas d’usage
- Même pour une même API resource, si l’ensemble des actions possibles diffère fortement selon les instances, Increase préfère séparer cela en plusieurs ressources
- Les actions possibles pour un ACH transfer émis et un ACH transfer reçu sont en pratique opposées, d’où la séparation entre
ach_transferetinbound_ach_transfer - Cette approche peut sembler plus verbeuse et plus intimidante au départ, au point de faire apparaître beaucoup de ressources dans la colonne de gauche de la documentation API
- En contrepartie, la relation entre ressources et actions devient plus prévisible sur le long terme
Un principe réduit les petites décisions de conception
- Concevoir une API complexe sur plusieurs années entraîne en permanence de petites décisions ; un principe fondateur défini tôt permet de réduire la charge cognitive associée à ces choix
- Lorsqu’un wire transfer est envoyé à la Federal Reserve, l’
Input Message Accountability Datasert d’identifiant global unique pour ce transfer - Dans une API très abstraite, un ingénieur pourrait hésiter à donner à cet élément un nom plus « convivial » comme
trace_number,reference_numberouid - Chez Increase, le nom de champ est simplement
input_message_accountability_data - Ce n’est peut-être pas le nom le plus immédiatement lisible pour un utilisateur qui découvre ce champ, mais il aide à comprendre tout de suite comment il se mappe au système sous-jacent
Critères pour choisir le niveau d’abstraction
- No Abstractions n’est pas un principe adapté à toutes les API
- Le bon niveau d’abstraction dépend de l’expérience métier du développeur d’intégration, de sa compréhension du domaine produit et de l’énergie qu’il est prêt à consacrer à l’intégration
- Si l’on conçoit une API très abstraite, il faut réfléchir en profondeur avant d’ajouter une nouvelle fonctionnalité
- Si l’on conçoit une API peu abstraite, il faut s’engager dans cette direction et résister à la tentation d’ajouter ensuite de l’abstraction
1 commentaires
Avis de Hacker News
On peut aussi toujours proposer les deux
Il suffit de fournir une API de bas niveau, qui permet un contrôle fin mais exige une expertise approfondie, puis de construire par-dessus une API de haut niveau qui mappe les cas d’usage courants sur quelques opérations simples. De toute façon, certains clients implémentent peut-être déjà eux-mêmes, de manière approximative, ce genre de couche de haut niveau
En séparant proprement les deux couches, on réduit la pression qui pousse à ajouter des abstractions à l’API de bas niveau, ou des aspérités et des cas particuliers à l’API de haut niveau. Si les clients veulent cela, c’est déjà présent dans une autre API
C’est encore mieux si l’on fournit aussi des ressources permettant aux clients d’apprendre à passer d’une couche à l’autre. Cela peut aussi attirer des clients qui ne connaissent pas encore en profondeur la structure interne des réseaux de paiement, mais qui veulent évoluer dans cette direction
Aujourd’hui, j’utilise la Web File System API, et il fallait 7 appels de fonction pour écrire une seule chaîne dans un fichier, la plupart asynchrones. Cela n’incluait même pas la gestion des erreurs, doit se faire dans un worker, et la configuration du worker elle-même est tout aussi fastidieuse. On voit des horreurs similaires avec IndexedDB, WebRTC ou de simples manipulations du DOM, et Vulkan, DirectX ou ffmpeg sont encore bien pires
La complexité se justifie dans une certaine mesure lorsqu’il faut gérer toutes sortes de cas particuliers, mais la plupart des cas ne relèvent pas de ces exceptions
La conception d’une API devrait commencer par esquisser à quoi ressemble le code qui l’utilise pour les cas courants, et ces cas devraient être aussi simples que possible. Par exemple, la fetch API s’en est plutôt bien sortie, contrairement à XMLHttpRequest
https://developer.mozilla.org/en-US/docs/Web/API/FileSystemS...
J’ai souvent pensé qu’il serait souhaitable d’avoir une API de couche de commodité unifiée pour toutes les Web API. Elle envelopperait toutes les fonctionnalités puissantes dans des wrappers de “bibliothèque standard” cohérents, prenant en charge au minimum les cas d’usage les plus courants. Les navigateurs modernes sont très puissants, mais chaque API est conçue à sa manière et inutilement difficile à apprendre ou à utiliser, si bien que cette puissance est peu connue ou sous-utilisée
Quelque chose de similaire à ce que jQuery a fait pour le DOM, mais avec moins de magie et moins de fonctionnalités supplémentaires, serait préférable. node.js dispose dans une certaine mesure d’API cohérentes, mais elles ont un peu vieilli et, par exemple, la prise en charge des Promise est irrégulière. C’est aussi comparable à la manière dont Python cherche à proposer des API “pythoniques”
Quand on s’habitue au point de vue de l’implémentation interne d’un outil, il est trop facile d’oublier comment les gens l’utilisent réellement
Il y a des commandes “porcelain” de haut niveau comme branch et checkout, et des commandes “plumbing” de bas niveau comme commit-tree et update-ref
https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
J’aime la partie où Increase explique pourquoi ils ont choisi une autre approche. Quand on conçoit des éléments fondamentaux, le contexte est très important, mais en général les gens ne le reconnaissent pas suffisamment
Ici, « pas d’abstraction » signifie en réalité utiliser tels quels les termes du système sous-jacent, ce qui est généralement un bon principe de nommage.
Le problème apparaît inévitablement avec le temps, quand les systèmes sous-jacents deviennent multiples, qu’on commence à donner des noms différents à la même chose, ou pire, à utiliser le même nom pour des choses différentes. Dans cet exemple, que faire si les modèles des fournisseurs de paiement sous-jacents diffèrent ? Et que faire si la Federal Reserve abandonne l’Input Message Accountability Data et le remplace par un nouveau concept ?
Le secteur des paiements est peut-être beaucoup plus simple que le transport ou les protocoles réseau. Si l’on a créé un produit de commutation de paquets fondé sur X.25 et que l’on veut ensuite prendre aussi en charge TCP/IP, quelle est la bonne abstraction ?
Pour la question de l’abandon, nous avons de la chance : le système sous-jacent ne change pas beaucoup, donc ça va. L’Input Message Accountability Data ne disparaîtra pas. Mais si, par exemple, nous commençons à émettre des cartes avec Mastercard en plus de Visa, nous rencontrerons des conflits.
Nous avons aussi expérimenté quelques abstractions, et cela pourrait arriver à cet endroit-là aussi. Une règle que nous avons toujours conservée est de ne pas abstraire les « objets sous-jacents », mais d’introduire, par commodité, des compositions de plus haut niveau. Par exemple, un « Card Payment » n’existe pas réellement (https://increase.com/documentation/api#card-payments). Ce n’est qu’une manière de regrouper l’autorisation de carte et les messages de règlement associés. Mais c’est très utile pour les utilisateurs, et faire soi-même le rapprochement n’est pas simple, donc nous avons essayé. Cela dit, je pense que les messages réseau sous-jacents, c’est-à-dire les « objets sous-jacents », ainsi que tous les champs d’origine, doivent aussi rester accessibles dans l’API.
Malheureusement, les API publiques sur lesquelles j’ai travaillé sont à 100 % dans le domaine des paiements ; j’aimerais avoir d’autres points de vue.
En DDD, on suit généralement les noms et le modèle conceptuel déjà construits par le domaine métier. Si l’on tente d’introduire son propre modèle ou sa propre terminologie « améliorés » [0], on crée des frictions et des malentendus, on augmente le risque de bugs d’intégration et on ignore une expertise éprouvée pendant des décennies, voire des siècles.
[0] https://xkcd.com/793/
Bon article.
Si vous aimez Stripe, et en tant que designer et fondateur technique je trouve moi aussi impressionnantes la simplicité de Stripe et ses compétences côté frontend, vous pouvez être tenté de les observer pour imiter leur capacité à simplifier et à offrir une expérience très aboutie.
Mais le vrai savoir-faire de Stripe tient au fait qu’ils connaissent bien leurs clients. Et qu’ils comprennent aussi très bien la simplicité à laquelle ces clients aspirent.
D’après cet article, Increase semble faire la même chose, et avoir construit d’excellentes lignes directrices de conception produit avec une focalisation tout aussi aiguë sur ce dont ses clients ont besoin. C’est encourageant.
Personnellement, je préfère quand c’est la seconde option qui arrive, mais il y a aussi là-dedans une décision esthétique.
Cela ressemble au pattern de conception du langage ubiquitaire en Domain-Driven Design. Il consiste à faire en sorte que l’implémentation utilise les termes du monde réel employés par les experts du domaine.
https://thedomaindrivendesign.io/developing-the-ubiquitous-l...
Cet article me donne l’impression d’une sorte de réaction d’évitement face à la honte. Les gens détestent pathologiquement dire « je me suis trompé » ou « nous nous sommes trompés », et finissent par pousser leurs métaphores dans tous les sens comme un enfant qui déplace les légumes dans son assiette pour donner l’impression de les avoir mangés.
Cela me rappelle aussi la formule « pas de défauts évidents » dans le discours de Hoare pour le prix Turing.
C’est un bon exemple du concept de langage ubiquitaire en Domain-Driven Design.
Il faut utiliser le langage que les experts du domaine comprennent. Si l’utilisateur connaît les fichiers NACHA, dès qu’on emploie un autre terme, il doit maintenir une correspondance dans sa tête.
À l’inverse, dans le cas de Stripe, les utilisateurs ne sont pas des experts du domaine ; il est donc utile de créer des abstractions compréhensibles qui masquent les détails inutiles. Si vous devez enseigner un langage à l’utilisateur, autant le rendre aussi simple que possible.
Sans une abstraction comme POSIX, les applications auraient dû écrire un adaptateur pour chaque système de fichiers pris en charge.
Intéressant.
Le titre de ce concept prête à confusion. Ici, « pas d’abstraction » ne signifie pas littéralement qu’il n’y a aucune abstraction, mais plutôt « nous utilisons cet ensemble particulier d’abstractions, et pas d’autres ». Le sous-ensemble précis qu’ils décrivent mérite d’être discuté, mais c’est évidemment un ensemble d’abstractions.
Par exemple, ils disent que « lorsqu’on crée une API pour les virements ACH, on nomme les paramètres exposés d’après les noms de champs de la spécification Nacha », mais la spécification elle-même est une abstraction.
Ils disent aussi : « Comme avec la terminologie réseau, nous essayons de modéliser les ressources d’après des événements réels, par exemple une action effectuée ou un message transmis. Résultat, davantage de ressources d’API deviennent immuables et sont regroupées sous un “objet de cycle de vie” à machine à états » ; or l’immutabilité dans ce sens, ainsi que l’« objet de cycle de vie », sont aussi des abstractions.
« Quand l’ensemble des actions qu’un utilisateur peut effectuer sur chaque instance d’une ressource d’API donnée varie beaucoup, nous avons tendance à la diviser en plusieurs ressources » est encore une autre abstraction. Elle découpe simplement à un niveau différent de l’API Stripe.
Au final, c’est un ensemble de décisions de conception et d’abstractions, pas un principe de « pas d’abstraction ». La décision la plus importante semble être de généraliser le moins possible, et la généralisation est elle aussi une forme d’abstraction. « Moins de généralisation » aurait probablement été un titre plus exact.
J’ai vu la partie disant que « les frais mensuels par utilisateur construits sur Increase dépendent du cas d’usage ».
Je suis en train d’ajouter un accès API public à un endpoint IA text-to-SQL avec RAG, et le plus gros problème, c’est la tarification. Est-ce que quelqu’un sait à peu près de quelle fourchette de prix on parle ? Le prix doit intégrer les tokens OpenAI, ou la possibilité pour l’utilisateur de fournir ses propres tokens OpenAI, l’utilisation de la base de données, et à l’avenir aussi la mise en cache et la configuration des limites de débit.
Par exemple, il me semble que Gong facture plus de 100 000 dollars par an à de nombreuses organisations ; même en tenant compte du stockage, du CPU et des autres coûts d’exploitation, il est impossible que ses coûts soient proches des coûts de calcul. L’écart est probablement d’au moins plusieurs fois. Mais comme les équipes commerciales génèrent du chiffre d’affaires de manière très directe, l’effet de levier qu’on peut acheter sous la forme d’un outil comme Gong a une valeur immédiate et évidente.
[1] : L’exception au principe selon lequel il faut éviter la tarification au coût majoré concerne les cas où l’on vend une commodité. Mais vous n’êtes pas dans cette situation !