Sem Abstrações: os princípios de design de API da Increase
(increase.com)- A Increase entende que os recursos da API moldam a compreensão do produto pelos usuários e adota o princípio Sem Abstrações, revelando em vez de esconder a complexidade das redes de pagamento
- A abstração ao estilo Stripe é forte para integrações rápidas, mas os usuários da Increase querem conexão direta e integração profunda com base em seu conhecimento de redes de pagamento
- A API usa diretamente a terminologia da rede subjacente, como a especificação Nacha, e modela o processo de um ACH transfer com subobjetos imutáveis
- Quando as ações que o usuário pode realizar diferem muito, separa recursos distintos como
ach_transfereinbound_ach_transfer, o que pode parecer verboso no início, mas aumenta a previsibilidade no longo prazo - O nível de abstração deve ser definido de acordo com a experiência de domínio e a disposição do desenvolvedor de integração em investir esforço; se optar por baixa abstração, é preciso manter esse princípio depois
Os recursos da API moldam o modelo mental do usuário
- Um recurso de API é o substantivo da API, e definir seu nome e modelo é uma das partes mais difíceis e importantes do design de API
- Quais recursos são expostos determina o modelo mental que o usuário forma sobre como o produto funciona e que ações são possíveis
- Para ajudar nessa decisão, a Increase usa o princípio de design “Sem Abstrações”
-
A diferença entre a abstração ao estilo Stripe e a abordagem da Increase
- A Stripe se destaca em abstração, extraindo um domínio de pagamentos complexo para uma API que o usuário consegue manipular com facilidade
- Ela modela várias redes de pagamento como o recurso de API
PaymentIntente combina diferenças entre chargeback reason codes de Visa e Mastercard em um único enum, para que o usuário não precise considerar as duas redes separadamente - Muitos usuários da Stripe são startups em estágio inicial que estão criando produtos que não são, em si, pagamento; em vez de conhecer profundamente os detalhes de cartão de crédito, querem integrar rápido e voltar ao desenvolvimento do produto principal
- Os usuários da Increase já têm conhecimento profundo sobre payment network, continuam trabalhando com tecnologia financeira e usam a Increase para conexão direta à rede e integração profunda
- Eles querem saber exatamente quando a janela do FedACH fecha e quando uma transferência chega, e entendem que, se o Standard Entry Class code de um ACH transfer mudar, o timing de retorno também pode mudar
- Se ACH transfer e wire transfer forem agrupados em um único recurso de API para esconder a complexidade da rede subjacente, para os usuários da Increase isso não é simplificação, e sim inconveniência
Como o princípio Sem Abstrações aparece na API
-
Uso da terminologia real da rede
- Em vez de inventar nomes para recursos e atributos da API, a Increase prefere usar o vocabulário da rede subjacente
- Ao criar uma API para ACH transfer, os parâmetros expostos seguem os nomes de campo da especificação Nacha
-
Recursos imutáveis e lifecycle object
- Os recursos também são modelados de acordo com eventos ou mensagens do mundo real, e essa abordagem faz com que mais recursos da API se tornem imutáveis
- Assim como um conjunto de mensagens de rede que podem ser enviadas no lifecycle de um ACH transfer, os recursos imutáveis são agrupados sob um lifecycle object em forma de máquina de estados
- O objeto
ach_transfertem um campostatusque muda ao longo do tempo e vários subobjetos imutáveis que são gerados conforme o lifecycle avança - Um novo
ach_transferpode terstatusigual apending_approval, comapproval,submissioneacknowledgementcomonull - Depois de ser enviado ao FedACH, o
statuspassa asubmitted, eapproval,submissioneacknowledgementsão preenchidos com informações imutáveis dos momentos de aprovação, envio e confirmação, respectivamente submissioninclui valores comotrace_numberesubmitted_at
-
Separação de recursos por caso de uso
- Mesmo para o mesmo recurso de API, se o conjunto de ações possíveis variar muito entre instâncias, a Increase prefere dividir isso em vários recursos
- Como as ações possíveis em originated ACH transfer e received ACH transfer são praticamente opostas, eles são separados em
ach_transfereinbound_ach_transfer - No começo, essa abordagem pode parecer mais verbosa e até intimidadora, a ponto de muitos recursos aparecerem na lateral esquerda da documentação da API
- Em compensação, no longo prazo a relação entre recursos e ações se torna mais previsível
Um princípio reduz pequenas decisões de design
- Ao projetar uma API complexa ao longo de vários anos, pequenas decisões continuam surgindo, e princípios fundamentais definidos no início reduzem a carga cognitiva dessas escolhas
- O
Input Message Accountability Dataexigido ao enviar um wire transfer para o Federal Reserve funciona como o ID global exclusivo dessa transferência - Em uma API com muita abstração, um engenheiro poderia parar para pensar qual nome seria mais “amigável” para o usuário entre
trace_number,reference_numberouid - Na Increase, o nome do campo é simplesmente
input_message_accountability_data - Talvez esse não seja o nome mais fácil de reconhecer à primeira vista, mas ele ajuda a entender imediatamente como o campo se mapeia ao sistema subjacente
Critérios para definir o nível de abstração
- Sem Abstrações não é um princípio adequado para toda API
- O nível apropriado de abstração depende da experiência de domínio do desenvolvedor de integração, da compreensão da área do produto e da energia que ele pretende investir na integração
- Se você criar uma API com muita abstração, será preciso refletir profundamente antes de adicionar novos recursos
- Se você criar uma API com pouca abstração, precisa se comprometer com essa direção e resistir à tentação de adicionar abstrações depois
1 comentários
Comentários do Hacker News
Dá para oferecer os dois
Você pode fornecer uma API de baixo nível que permita controle detalhado, mas exija conhecimento profundo, e criar por cima dela uma API de alto nível que mapeie os casos de uso mais comuns em algumas operações simples. De todo modo, alguns clientes talvez já estejam implementando por conta própria, e de forma improvisada, essa camada de alto nível
Se as duas camadas forem separadas com clareza, diminui a pressão para colocar abstrações na API de baixo nível ou adicionar imperfeições e casos especiais à API de alto nível. Se o cliente quiser isso, já existe na outra API
Melhor ainda se você também oferecer material para que os clientes aprendam a migrar de uma camada para a outra. Assim, também pode atrair clientes que ainda não conhecem profundamente a estrutura interna das redes de pagamento, mas querem evoluir nessa direção
Hoje usei a Web File System API, e precisei de 7 chamadas de função para escrever uma única string em um arquivo, a maioria assíncrona. Isso sem incluir tratamento de erro, e ainda precisava ser feito em um worker, cuja própria configuração é igualmente trabalhosa. Dá para ver a mesma situação terrível no IndexedDB, WebRTC e até em manipulação comum de DOM; Vulkan, DirectX e ffmpeg são muito piores
Para lidar com todo tipo de caso especial, certo nível de complexidade é justificável, mas na maioria das vezes os usos não são esses casos especiais
O design de API deveria começar esboçando primeiro como o código de uso vai parecer nos casos comuns, e esses casos deveriam ser o mais simples possível. Por exemplo, a API
fetchfoi bem-sucedida nisso, enquantoXMLHttpRequestpassou longehttps://developer.mozilla.org/en-US/docs/Web/API/FileSystemS...
Já pensei várias vezes que seria ótimo ter uma camada de conveniência unificada para todas as Web APIs. Algo que envolvesse todos os recursos poderosos em wrappers consistentes de uma “biblioteca padrão”, dando suporte pelo menos aos casos de uso mais comuns. Os navegadores modernos são muito poderosos, mas como o design de cada API é diferente e desnecessariamente difícil de aprender ou usar, esse poder acaba sendo pouco conhecido ou menos usado
Seria algo parecido com o que o jQuery fez pelo DOM, mas com menos mágica e menos extras. O node.js tem APIs consistentes até certo ponto, mas está um pouco datado e, por exemplo, o suporte a Promise é irregular. Também seria parecido com a forma como Python busca APIs “pythônicas”
Ao se acostumar com a perspectiva da implementação interna da ferramenta, é fácil demais esquecer como as pessoas realmente a usam
Há comandos “porcelain” de alto nível, como
branchecheckout, e comandos “plumbing” de baixo nível, comocommit-treeeupdate-refhttps://git-scm.com/book/en/v2/Git-Internals-Plumbing-and-Po...
Gostei da parte em que a Increase explica por que escolheu uma abordagem diferente. Ao projetar algo básico, o contexto é extremamente importante, mas em geral as pessoas não reconhecem isso o suficiente
Aqui, “sem abstrações” na prática significa usar exatamente a terminologia do sistema subjacente, e isso em geral é um bom princípio de nomeação.
O problema surge inevitavelmente com o tempo, quando há vários sistemas subjacentes, eles dão nomes diferentes para a mesma coisa ou, pior ainda, começam a usar o mesmo nome para coisas diferentes. Neste exemplo, o que fazer se os modelos dos provedores de pagamento subjacentes forem diferentes? E se o Federal Reserve descontinuar o Input Message Accountability Data e o substituir por um conceito novo?
O setor de pagamentos talvez seja bem mais simples que transporte ou protocolos de rede. Se você criou um produto de comutação de pacotes baseado em X.25 e depois quiser dar suporte a TCP/IP também, qual seria a abstração correta?
Quanto à descontinuação, felizmente isso não é um problema porque os sistemas subjacentes não mudam muito. O Input Message Accountability Data não vai desaparecer. Mas, por exemplo, se começarmos a emitir cartões não só com a Visa, mas também com a Mastercard, aí vamos enfrentar conflitos.
Também experimentamos algumas abstrações, e isso pode acontecer nesse ponto também. Uma regra que mantivemos o tempo todo é não abstrair os “objetos subjacentes”, e sim introduzir composições de nível mais alto por conveniência. Por exemplo, “Card Payment” não existe de fato(https://increase.com/documentation/api#card-payments). É apenas uma forma de agrupar as mensagens relacionadas de autorização e liquidação de cartão. Ainda assim, é muito útil para o usuário e não é fácil trabalhar diretamente com a contraparte, então resolvemos tentar. Mas eu acho que as mensagens de rede subjacentes, isto é, os “objetos subjacentes”, e todos os campos originais também precisam estar acessíveis na API.
Infelizmente, as APIs públicas em que trabalhei eram 100% da área de pagamentos, então seria bom ter outra perspectiva.
Em DDD, normalmente se segue o modelo conceitual e os nomes que o domínio de negócio já criou. Se você tenta introduzir seu próprio modelo ou terminologia “melhorada” [0], surgem atritos e mal-entendidos, a chance de bugs de integração aumenta, e você acaba ignorando conhecimento especializado validado ao longo de décadas ou séculos.
[0] https://xkcd.com/793/
Gostei do texto.
Se você gosta da Stripe, eu também, como designer e fundador técnico, acho impressionantes a simplicidade deles e a capacidade no frontend, então dá vontade de olhar para eles e tentar imitar essa habilidade de simplificar e entregar uma experiência refinada.
Mas a verdadeira maestria da Stripe está em conhecer bem o cliente. E também em entender bem a simplicidade que o cliente deseja.
Por este texto, a Increase parece igual nesse aspecto, e parece ter criado ótimas diretrizes de design de produto com um foco igualmente afiado no que o cliente precisa. Isso é encorajador.
Pessoalmente, gosto mais quando acontece o segundo caso, mas há também uma decisão estética aí.
Isso é parecido com o padrão de design da linguagem ubíqua em domain-driven design. É uma forma de fazer a implementação usar exatamente a terminologia do mundo real que os especialistas do domínio usam.
https://thedomaindrivendesign.io/developing-the-ubiquitous-l...
Este texto me soa como uma espécie de reação para evitar constrangimento. As pessoas detestam de forma quase patológica dizer “eu estava errado” ou “nós estávamos errados”, então acabam empurrando metáforas de um lado para outro como uma criança que mexe os vegetais no prato para parecer que comeu.
Também me lembra a frase “não há defeitos óbvios” na palestra do prêmio Turing do Hoare.
Este é um bom exemplo do conceito de linguagem ubíqua em domain-driven design.
É preciso usar a linguagem que o especialista do domínio entende. Se o usuário conhece arquivos NACHA, no momento em que você usa outra terminologia ele precisa manter um mapeamento mental.
Em contrapartida, no caso da Stripe o usuário não é um especialista do domínio, então faz sentido criar abstrações que ele consiga entender e que escondam detalhes desnecessários. Se você precisa ensinar uma linguagem ao usuário, ela deve ser o mais simples possível.
Sem uma abstração como o POSIX, os aplicativos teriam de escrever um adaptador para cada sistema de arquivos suportado.
Interessante.
O título desse conceito induz ao erro. Aqui, “sem abstrações” não significa literalmente ausência de abstrações, mas sim “usa-se este conjunto específico de abstrações e não outros”. O subconjunto específico que eles descrevem vale a discussão, mas evidentemente ainda é um conjunto de abstrações.
Por exemplo, eles dizem “ao transformar uma transferência ACH em API, damos aos parâmetros expostos nomes baseados nos nomes de campo da especificação Nacha”, mas a própria especificação já é uma abstração.
Eles dizem que “assim como usam a terminologia da rede, tentam modelar recursos de acordo com eventos reais, como ações executadas ou mensagens transmitidas. Como resultado, mais recursos da API se tornam imutáveis e ficam agrupados sob objetos de ‘ciclo de vida’ de máquina de estados”, mas imutabilidade nesse sentido e “objetos de ciclo de vida” também são abstrações.
Dizer que “quando o conjunto de ações que os usuários podem realizar em cada instância de um determinado recurso da API varia muito, eles tendem a dividi-lo em vários recursos” também é outra abstração. Só está dividido em um nível diferente do da API da Stripe.
No fim, isso é um conjunto de decisões de design e abstrações, não um princípio de “sem abstrações”. A decisão mais importante parece ser generalizar o mínimo possível, e generalização também é um tipo de abstração. Talvez “menos generalização” tivesse sido um título mais preciso.
Vi a parte que diz: “a cobrança mensal por usuário criada em cima da Increase varia conforme o caso de uso”.
Estou adicionando agora acesso de API pública a um endpoint de texto-para-SQL com IA com suporte a RAG, e o maior problema é a precificação. Alguém tem ideia de mais ou menos que faixa de valores faz sentido? O preço precisa refletir tokens da OpenAI, ou então um modelo em que o usuário informe os próprios tokens da OpenAI, uso do banco de dados e, no futuro, também cache e configuração de limite de taxa
Por exemplo, pelo que sei, a Gong cobra mais de US$ 100 mil por ano de muitas organizações, e mesmo considerando armazenamento, CPU e outros custos operacionais, é improvável que o custo fique sequer perto do custo de computação. Provavelmente há pelo menos uma diferença de vários múltiplos. Mas equipes de vendas geram receita de forma muito direta, então a alavancagem que se pode comprar na forma de uma ferramenta como a Gong tem um valor imediato, claro e significativo.
[1]: Uma exceção ao princípio de evitar precificação por custo mais margem é quando você vende commodities. Mas não é esse o seu caso!