3 Punkte von GN⁺ 2024-04-27 | 1 Kommentare | Auf WhatsApp teilen
  • Increase ist der Ansicht, dass API-Ressourcen das Produktverständnis der Nutzer prägen, und verfolgt daher das Prinzip No Abstractions: Die Komplexität von Zahlungsnetzwerken wird nicht versteckt, sondern sichtbar gemacht
  • Abstraktionen im Stil von Stripe sind stark bei schneller Integration, doch Increase-Nutzer wollen auf Basis ihres Wissens über payment networks direkte Anbindungen und tiefe Integrationen
  • Die API verwendet Begriffe der zugrunde liegenden Netzwerke wie in der Nacha specification und modelliert den Ablauf eines ACH transfer als unveränderliche Unterobjekte
  • Wenn sich die möglichen Aktionen der Nutzer stark unterscheiden, werden Ressourcen wie ach_transfer und inbound_ach_transfer getrennt, was anfangs ausführlich wirkt, langfristig aber die Vorhersagbarkeit erhöht
  • Der Abstraktionsgrad sollte zur Domänenerfahrung und Einsatzbereitschaft der integrierenden Entwickler passen; wer sich für geringe Abstraktion entscheidet, sollte dieses Prinzip auch später beibehalten

API-Ressourcen formen das mentale Modell der Nutzer

  • Eine API resource ist das Substantiv einer API; Namen und Modelle festzulegen gehört zu den schwierigsten und wichtigsten Teilen des API-Designs
  • Welche Ressourcen offengelegt werden, prägt das mentale Modell, mit dem Nutzer verstehen, wie ein Produkt funktioniert und welche Aufgaben möglich sind
  • Increase nutzt als Entscheidungshilfe das Designprinzip „No Abstractions“
  • Abstraktion im Stil von Stripe und der Unterschied zu Increase

    • Stripe ist stark darin, eine komplexe Zahlungsdomäne in eine API zu abstrahieren, die Nutzer leicht verwenden können
    • Mehrere Zahlungsnetzwerke werden als API resource PaymentIntent modelliert, und Unterschiede bei chargeback reason codes zwischen Visa und Mastercard werden in einem einzigen enum zusammengeführt, sodass Nutzer die beiden Netzwerke nicht separat berücksichtigen müssen
    • Viele Stripe-Nutzer sind frühe Startups, die nicht primär Payments selbst, sondern ein Produkt bauen; sie wollen sich nicht tief mit Kreditkartendetails befassen, sondern schnell integrieren und dann zur Entwicklung ihres eigentlichen Produkts zurückkehren
    • Increase-Nutzer verfügen über tiefes Vorwissen zu payment networks, arbeiten kontinuierlich mit Finanztechnologie und nutzen Increase für direkte Netzwerkanbindungen und tiefe Integrationen
    • Sie wollen genau wissen, wann ein FedACH window schließt und wann ein transfer ankommt, und verstehen, dass sich bei einem anderen Standard Entry Class code eines ACH transfer auch das return timing ändern kann
    • Wenn ACH transfer und wire transfer in einer einzigen API resource zusammengefasst werden, um die Komplexität der zugrunde liegenden Netzwerke zu verstecken, ist das für Increase-Nutzer keine Vereinfachung, sondern eine Einschränkung

Wie No Abstractions in der API sichtbar wird

  • Verwendung echter Netzwerkbegriffe

    • Increase verwendet für API resource und attribute names eher den Wortschatz der zugrunde liegenden Netzwerke, statt neue Begriffe zu erfinden
    • Bei der Erstellung einer API für ACH transfer orientieren sich die offengelegten parameter an den field names der Nacha specification
  • Unveränderliche Ressourcen und lifecycle object

    • Auch Ressourcen werden nach Ereignissen oder Nachrichten der realen Welt modelliert; dieser Ansatz führt dazu, dass mehr API resources unveränderlich sind
    • Wie ein Bündel von Netzwerknachrichten, die im lifecycle eines ACH transfer gesendet werden können, werden unveränderliche Ressourcen unter einem lifecycle object in Form einer state machine gruppiert
    • Das ach_transfer object besitzt ein status field, das sich im Laufe der Zeit ändert, sowie mehrere unveränderliche sub-objects, die im Verlauf des lifecycle erzeugt werden
    • Ein neuer ach_transfer kann den status pending_approval haben, während approval, submission und acknowledgement null sind
    • Nach der Einreichung bei FedACH wird der status zu submitted, und approval, submission und acknowledgement werden jeweils mit unveränderlichen Informationen zu Genehmigung, Einreichung und Bestätigung gefüllt
    • submission enthält Werte wie trace_number und submitted_at
  • Ressourcen nach Use Case trennen

    • Wenn sich selbst bei derselben API resource die möglichen Aktionen je nach Instanz stark unterscheiden, teilt Increase sie eher in mehrere Ressourcen auf
    • Bei originated ACH transfer und received ACH transfer sind die möglichen Aktionen praktisch gegensätzlich, daher werden sie in ach_transfer und inbound_ach_transfer getrennt
    • Dieser Ansatz kann anfangs ausführlicher und einschüchternder wirken, etwa weil links in der API-Dokumentation viele Ressourcen erscheinen
    • Langfristig wird dafür die Beziehung zwischen Ressourcen und Aktionen vorhersagbarer

Prinzipien reduzieren kleine Designentscheidungen

  • Wenn man eine komplexe API über mehrere Jahre hinweg gestaltet, fallen ständig kleine Entscheidungen an; früh festgelegte Grundprinzipien reduzieren die kognitive Belastung solcher Entscheidungen
  • Die beim Senden eines wire transfer an die Federal Reserve benötigten Input Message Accountability Data dienen als global eindeutige ID dieses transfer
  • Bei einer API mit vielen Abstraktionen könnten Engineers überlegen, ob man dies „nutzerfreundlicher“ trace_number, reference_number oder id nennen sollte
  • Bei Increase wird der field name als input_message_accountability_data festgelegt, und damit ist die Entscheidung erledigt
  • Für Nutzer ist dieser field beim ersten Blick möglicherweise nicht sofort leicht zu erkennen, hilft aber dabei, direkt zu verstehen, wie er auf das zugrunde liegende System abgebildet wird

Kriterien für die Wahl des Abstraktionsgrads

  • No Abstractions ist kein Prinzip, das für jede API passt
  • Der passende Abstraktionsgrad hängt von der Domänenerfahrung der integrierenden Entwickler, ihrem Verständnis des Produktbereichs und der Energie ab, die sie in die Integration investieren wollen
  • Wer eine API mit vielen Abstraktionen baut, muss vor dem Hinzufügen neuer Funktionen gründlich nachdenken
  • Wer eine API mit wenig Abstraktion baut, muss sich auf diese Richtung festlegen und der Versuchung widerstehen, nachträglich Abstraktionen hinzuzufügen

1 Kommentare

 
GN⁺ 2024-04-27
Meinungen auf Hacker News
  • Man kann auch immer beides anbieten.
    Man stellt eine Low-Level-API bereit, die feingranulare Kontrolle ermöglicht, aber tiefes Fachwissen erfordert, und baut darauf eine High-Level-API, die gängige Anwendungsfälle auf einige einfache Operationen abbildet. Manche Kunden implementieren sich so eine High-Level-Schicht ohnehin möglicherweise auf unbeholfene Weise selbst.
    Wenn man die beiden Schichten sauber trennt, sinkt der Druck, Abstraktionen in die Low-Level-API einzubauen oder Macken und Sonderfälle zur High-Level-API hinzuzufügen. Denn wenn Kunden so etwas wollen, existiert es bereits in der anderen API.
    Noch besser ist es, zusätzlich Material bereitzustellen, mit dem Kunden lernen können, wie sie von einer Schicht zur anderen wechseln. So kann man auch Kunden gewinnen, die die internen Strukturen von Zahlungsnetzwerken noch nicht tiefgehend kennen, aber in diese Richtung wachsen wollen.

    • Es sollte eine Low-Level-API geben, die seltene komplexe Fälle abdecken kann, und darauf aufbauend eine einfache High-Level-API für die üblichen Fälle.
      Ich nutze heute die Web File System API, und um einen String in eine Datei zu schreiben, waren sieben Funktionsaufrufe nötig, die meisten davon asynchron. Fehlerbehandlung ist dabei noch nicht enthalten, es muss in einem Worker passieren, und das Einrichten des Workers selbst ist ähnlich umständlich. Ähnliche Grausamkeiten sieht man bei IndexedDB, WebRTC und gewöhnlicher DOM-Manipulation; Vulkan, DirectX und ffmpeg sind noch viel schlimmer.
      Um alle möglichen Sonderfälle abzudecken, ist ein gewisses Maß an Komplexität gerechtfertigt, aber die meisten Fälle sind eben keine solchen Sonderfälle.
      API-Design sollte damit beginnen, zu skizzieren, wie Code zur Nutzung der API in den häufigen Fällen aussieht, und diese Fälle sollten so einfach wie möglich sein. Die fetch API hat das zum Beispiel ziemlich gut gemacht, XMLHttpRequest überhaupt nicht.
      https://developer.mozilla.org/en-US/docs/Web/API/FileSystemS...
      Ich habe schon oft gedacht, dass es schön wäre, eine einheitliche Convenience-Layer-API für alle Web APIs zu haben. Eine konsistente „Standardbibliothek“-Wrapper-Schicht um alle leistungsfähigen Funktionen, die zumindest die häufigsten Anwendungsfälle unterstützt. Moderne Browser sind sehr leistungsfähig, aber weil das Design der einzelnen APIs uneinheitlich und unnötig schwer zu lernen oder zu verwenden ist, ist diese Stärke wenig bekannt oder wird zu wenig genutzt.
      Ähnlich wie jQuery es für das DOM getan hat, nur mit weniger Magie und weniger Zusatzfunktionen. node.js hat bis zu einem gewissen Grad konsistente APIs, ist aber etwas in die Jahre gekommen; zum Beispiel ist die Promise-Unterstützung uneinheitlich. Es ähnelt auch der Art, wie Python „pythonic“ APIs anstrebt.
    • Mir gefällt dieses Muster besonders, wenn sich die gewünschte High-Level-API außerhalb der Bibliothek implementieren lässt. Dann kann man sicherstellen, dass die Low-Level-API flexibel genug ist, und man nutzt als Entwickler auch selbst die eigene API aus Nutzersicht.
      Wenn man sich an die Perspektive der internen Implementierung eines Tools gewöhnt, vergisst man nur zu leicht, wie Menschen es tatsächlich verwenden.
    • Git ist ein Beispiel dafür.
      Es gibt High-Level-„Porcelain“-Befehle wie branch und checkout sowie Low-Level-„Plumbing“-Befehle wie commit-tree und update-ref.
      https://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
    • Auch .NET nutzt diesen Ansatz häufig. Dazu gibt es einen aktuellen Entwicklerblog-Beitrag über Datei-I/O: https://devblogs.microsoft.com/dotnet/the-convenience-of-sys...
    • Dafür verdoppelt sich allerdings die API-Oberfläche, was ein abzuwägender Trade-off ist. In vielen Fällen kann es die richtige Entscheidung sein.
  • Mir gefällt der Abschnitt, in dem erklärt wird, warum Increase einen anderen Ansatz gewählt hat. Beim Entwerfen grundlegender Dinge ist der Kontext sehr wichtig, was Menschen normalerweise nicht ausreichend anerkennen.

  • „Keine Abstraktionen“ bedeutet hier im Grunde: Verwende die Begriffe des zugrunde liegenden Systems unverändert — und das ist allgemein ein gutes Prinzip für Benennungen.
    Das Problem entsteht zwangsläufig mit der Zeit, wenn es mehrere zugrunde liegende Systeme gibt, dieselbe Sache unterschiedlich benannt wird oder, schlimmer noch, derselbe Name für unterschiedliche Dinge verwendet wird. Was wäre in diesem Beispiel, wenn die Modelle der zugrunde liegenden Zahlungsanbieter verschieden sind? Und was, wenn die Federal Reserve Input Message Accountability Data abschafft und durch ein neues Konzept ersetzt?
    Die Zahlungsbranche ist vielleicht deutlich einfacher als Transport oder Netzwerkprotokolle. Wenn man ein auf X.25 basierendes Paketvermittlungsprodukt gebaut hat und später auch TCP/IP unterstützen will: Was ist dann die richtige Abstraktion?

    • Danke fürs sorgfältige Lesen.
      Das Problem der Abschaffung ist glücklicherweise unkritisch, weil sich das zugrunde liegende System nicht stark verändert. Input Message Accountability Data wird nicht verschwinden. Aber wenn wir zum Beispiel anfangen, Karten nicht nur über Visa, sondern auch über Mastercard auszugeben, werden wir auf Konflikte stoßen.
      Wir haben auch mit einigen Abstraktionen experimentiert, und an dieser Stelle könnte das ebenfalls passieren. Eine Regel, an die wir uns durchgehend gehalten haben: „zugrunde liegende Objekte“ nicht abstrahieren, sondern der Bequemlichkeit halber höherstufige Kompositionen einführen. Zum Beispiel existiert so etwas wie eine „Card Payment“ eigentlich nicht (https://increase.com/documentation/api#card-payments). Es ist nur eine Art, die zugehörige Kartenautorisierung und die Settlement-Nachrichten zu bündeln. Für Nutzer ist das aber sehr hilfreich, und die Abstimmung selbst vorzunehmen ist nicht einfach, daher haben wir es ausprobiert. Allerdings denke ich, dass die zugrunde liegenden Netzwerknachrichten, also die „zugrunde liegenden Objekte“, und alle ursprünglichen Felder ebenfalls über die API zugänglich sein müssen.
      Leider beziehen sich die öffentlichen APIs, an denen ich gearbeitet habe, zu 100 % auf Zahlungen; ich wünschte, ich hätte auch andere Perspektiven.
    • Der Artikel sagt auch klar, dass „ähnliche Objekte nicht zusammengeführt werden“, und genau das macht die Benennungsentscheidungen möglich.
    • „Die Begriffe des zugrunde liegenden Systems unverändert verwenden“ klingt ein wenig nach Domain-Driven Design. Allerdings könnte das „zugrunde liegende System“ in diesem Fall etwas zu implementierungsnah sein, um wirklich die eigentliche Geschäftsdomäne zu sein.
      In DDD folgt man normalerweise den Namen und dem konzeptionellen Modell, die in der Geschäftsdomäne bereits existieren. Wenn man versucht, ein eigenes „verbessertes“ [0] Modell oder eigene Terminologie einzuführen, entstehen Reibung und Missverständnisse, die Wahrscheinlichkeit von Integrationsfehlern steigt, und man ignoriert Fachwissen, das über Jahrzehnte oder Jahrhunderte erprobt wurde.
      [0] https://xkcd.com/793/
  • Guter Artikel.
    Wenn man Stripe mag — und als Designer und technischer Gründer finde auch ich Stripes Einfachheit und Frontend-Kompetenz beeindruckend —, kann man versucht sein, ihre Fähigkeit zur Vereinfachung und zur Bereitstellung einer ausgereiften Experience nachzuahmen.
    Stripes eigentliche Meisterschaft liegt aber darin, seine Kunden gut zu kennen. Und sie wissen auch genau, nach welcher Einfachheit ihre Kunden verlangen.
    Nach diesem Artikel zu urteilen scheint es bei Increase ähnlich zu sein, und aus einem ähnlich scharfen Fokus darauf, was Kunden brauchen, sind offenbar hervorragende Leitlinien für das Produktdesign entstanden. Das ist ermutigend.

    • Wie Stripe APIs und Teams baut: https://www.youtube.com/watch?v=IEe-5VOv0Js
    • Auch in der Stripe-API sieht man Stellen, an denen die Spannung zwischen „Machen wir das potenziell universell“ und „Akzeptieren wir, dass das vermutlich nur für eine Zahlungsmethode in einem Markt gilt“ sichtbar wird.
      Persönlich gefällt es mir besser, wenn Letzteres passiert, aber dabei spielen auch ästhetische Entscheidungen eine Rolle.
  • Das ähnelt dem Entwurfsmuster der Ubiquitous Language im Domain-Driven Design. Dabei werden in der Implementierung die realen Begriffe verwendet, die Domänenexperten nutzen.
    https://thedomaindrivendesign.io/developing-the-ubiquitous-l...

    • Ich habe ähnliche Konzepte schon lange vor DDD gehört. Wenn die Substantive und Verben im Code nicht zur Problemdomäne passen, ist das ein Impedance Mismatch und wird irgendwann Probleme verursachen.
      Dieser Artikel liest sich für mich wie eine Art Schamvermeidungsreaktion. Menschen hassen es krankhaft, „ich lag falsch“ oder „wir lagen falsch“ zu sagen, und schieben dann Metaphern herum wie ein Kind, das Gemüse auf dem Teller verschiebt, damit es so aussieht, als hätte es etwas davon gegessen.
      Ich muss auch an Hoares Turing-Award-Rede und den Satz „no obvious deficiencies“ denken.
  • Das ist ein gutes Beispiel für das Konzept der Ubiquitous Language im Domain-Driven Design.
    Man sollte die Sprache verwenden, die Domänenexperten verstehen. Wenn Nutzer NACHA-Dateien kennen, müssen sie, sobald man andere Begriffe verwendet, eine Zuordnung im Kopf behalten.
    Bei Stripe hingegen sind die Nutzer keine Domänenexperten; daher ist es wertvoll, eine Abstraktion zu schaffen, die verständlich ist und unnötige Details verbirgt. Wenn man Nutzern eine Sprache beibringen muss, sollte man sie so einfach wie möglich machen.

    • Anders gesagt: Sie sind Domänenexperten für die Art von Transaktionen, die sie durchführen möchten, nicht dafür, wie Transaktionen in Finanzsystemen implementiert werden.
  • Ohne Abstraktionen wie POSIX müssten Anwendungen für jedes unterstützte Dateisystem eigene Adapter schreiben.

  • Interessant.
    Der Titel dieses Konzepts ist missverständlich. „Keine Abstraktionen“ bedeutet hier nicht wörtlich, dass es keine Abstraktionen gibt, sondern: „Wir verwenden diese bestimmte Menge von Abstraktionen und andere nicht.“ Die von ihnen beschriebene Teilmenge ist diskussionswürdig, aber natürlich ist sie eine Menge von Abstraktionen.
    Zum Beispiel hieß es: „Wenn wir ACH-Transfers als API modellieren, benennen wir die offengelegten Parameter nach den Feldnamen der Nacha-Spezifikation.“ Aber die Spezifikation selbst ist eine Abstraktion.
    Außerdem hieß es: „Wie bei der Verwendung von Netzwerkterminologie versuchen wir, Ressourcen an reale Ereignisse anzupassen, etwa an ausgeführte Aktionen oder übermittelte Nachrichten. Dadurch werden mehr API-Ressourcen unveränderlich und unter zustandsmaschinenartigen ‚Lifecycle Objects‘ gruppiert.“ Die Unveränderlichkeit in diesem Sinn und die „Lifecycle Objects“ sind ebenfalls Abstraktionen.
    Auch „Wenn sich die Menge der Aktionen, die Nutzer für einzelne Instanzen einer bestimmten API-Ressource ausführen können, stark unterscheidet, neigen wir dazu, sie in mehrere Ressourcen aufzuteilen“ ist eine weitere Abstraktion. Sie schneiden nur auf einer anderen Ebene als die Stripe-API.
    Am Ende ist das eine Menge von Designentscheidungen und Abstraktionen, kein Prinzip „keine Abstraktionen“. Die wichtigste Entscheidung scheint zu sein, so wenig wie möglich zu generalisieren — und Generalisierung ist ebenfalls eine Art von Abstraktion. „Weniger Generalisierung“ wäre vermutlich ein treffenderer Titel gewesen.

  • Ich habe den Teil gesehen: „Die monatlichen Gebühren pro Nutzer, die man auf Increase aufbaut, unterscheiden sich je nach Use Case.“
    Ich füge gerade öffentlichen API-Zugriff zu einem RAG-gestützten AI-Text-to-SQL-Endpoint hinzu, und das größte Problem ist die Preisgestaltung. Weiß jemand, von welcher Größenordnung wir da ungefähr sprechen? In den Preis müssen OpenAI-Tokens einfließen – oder die Variante, dass Nutzer ihre eigenen OpenAI-Tokens einbringen –, außerdem Datenbanknutzung und künftig auch Caching sowie Einstellungen für Rate Limits.

    • Grundsätzlich sollte sich der Preis nicht an den Kosten, sondern am Wert orientieren[1]. Man muss also überlegen, welchen Wert das für den Kunden hat, und von dort ausgehen.
      Soweit ich weiß, berechnet Gong vielen Organisationen über 100.000 US-Dollar pro Jahr; selbst wenn man Storage, CPU und andere Betriebskosten berücksichtigt, können die Kosten kaum in der Nähe der reinen Compute-Kosten liegen. Wahrscheinlich liegt mindestens ein einstelliger Faktor dazwischen. Da Vertriebsteams aber sehr direkt Umsatz generieren, ist der Hebel, den man in Form eines Tools wie Gong kaufen kann, unmittelbar und klar wertvoll.
      [1]: Eine Ausnahme von der Regel, Cost-plus-Pricing zu vermeiden, gilt beim Verkauf von Commodities. Aber in dieser Situation bist du nicht!