Abstração de Dados de Barbara Liskov: Construindo APIs Confiáveis
Aprenda os princípios de abstração de dados de Barbara Liskov para projetar interfaces estáveis, reduzir quebras e construir sistemas manuteníveis com APIs claras e confiáveis.

Por que Barbara Liskov ainda importa para o design de APIs
Barbara Liskov é uma cientista da computação cujo trabalho moldou discretamente a forma como equipes modernas constroem software que não desmorona. Sua pesquisa sobre abstração de dados, ocultamento de informação e, mais tarde, o Princípio da Substituição de Liskov (LSP) influenciou desde linguagens de programação até a maneira cotidiana de pensar sobre APIs: defina comportamento claro, proteja os internos e torne seguro para outros dependerem da sua interface.
“Interfaces confiáveis” em termos de produto
Uma API confiável não é apenas “correta” em sentido teórico. É uma interface que ajuda um produto a andar mais rápido:
- Novas funcionalidades são lançadas sem quebrar clientes existentes.
- Integrações continuam funcionando entre versões.
- Incidentes on‑call diminuem porque as falhas são previsíveis.
- Equipes podem mudar internals sem maratona de coordenação.
Essa confiabilidade é uma experiência: para o desenvolvedor que chama sua API, para a equipe que a mantém e para os usuários que dependem dela indiretamente.
Como a abstração de dados reduz bugs (e reuniões)
Abstração de dados é a ideia de que quem chama deve interagir com um conceito (uma conta, uma fila, uma assinatura) por meio de um pequeno conjunto de operações — não pelos detalhes bagunçados de como isso é armazenado ou calculado.
Quando você esconde detalhes de representação, remove categorias inteiras de erros: ninguém pode “acidentalmente” depender de um campo do banco de dados que não deveria ser público, ou mutar um estado compartilhado de forma que o sistema não consegue lidar. Igualmente importante, a abstração reduz o custo de coordenação: equipes não precisam de permissão para refatorar internals enquanto o comportamento público permanecer consistente.
O que você poderá aplicar depois deste artigo
Ao final deste texto, você terá maneiras práticas de:
- Escrever comportamentos de API como promessas claras (incluindo casos de borda).
- Manter interfaces pequenas e estáveis enquanto os sistemas evoluem.
- Projetar modos de falha previsíveis que os chamadores consigam tratar.
Se quiser um resumo rápido depois, vá para /blog/a-practical-checklist-for-designing-reliable-apis.
Abstração de Dados, explicada sem jargão
Abstração de dados é uma ideia simples: você interage com algo pelo que ele faz, não por como foi construído.
Pense em uma máquina de venda automática. Você não precisa saber como os motores giram ou como as moedas são contadas. Só precisa dos controles (“selecionar item”, “pagar”, “receber item”) e das regras (“se pagar o suficiente, recebe o item; se estiver esgotado, recebe reembolso”). Isso é abstração.
“O que faz” vs. “Como funciona”
Em software, a interface é o “o que faz”: nomes de operações, quais entradas aceitam, quais saídas produzem e quais erros esperar. A implementação é o “como funciona”: tabelas do banco, estratégia de cache, classes internas e truques de performance.
Manter esses dois separados é como você obtém APIs que permanecem estáveis mesmo quando o sistema evolui. Você pode reescrever internals, trocar bibliotecas ou otimizar armazenamento — enquanto a interface continua a mesma para os usuários.
Tipos Abstratos de Dados (ADTs) em um minuto
Um tipo abstrato de dados é um “contêiner + operações permitidas + regras”, descrito sem se comprometer com uma estrutura interna específica.
Exemplo: uma Stack (último a entrar, primeiro a sair).
- push(item): adiciona um item
- pop(): remove e retorna o item mais recentemente adicionado
- peek(): olha o item do topo sem removê‑lo
O essencial é a promessa: pop() retorna o último push(). Se a pilha usa um array, uma lista ligada ou outra coisa é privado.
Como isso se aplica a APIs reais
A mesma separação aparece em todo lugar:
- Endpoints REST:
POST /paymentsé a interface; checagens de fraude, retries e gravações no banco são implementação. - Métodos de SDK:
client.upload(file)é a interface; chunking, compressão e requisições paralelas são implementação. - Componentes de UI: um “DatePicker” expõe props/events; estrutura do DOM e plumbing de acessibilidade são implementação.
Ao projetar com abstração, você foca no contrato que os usuários usam — e ganha liberdade para mudar tudo nos bastidores sem quebrá‑los.
Invariantes: as regras ocultas que mantêm sistemas corretos
Um invariante é uma regra que deve ser sempre verdadeira dentro de uma abstração. Ao projetar uma API, invariantes são as guardrails que impedem seus dados de derivarem para estados impossíveis — como uma conta bancária com duas moedas ao mesmo tempo, ou um pedido “completo” sem itens.
Como invariantes se parecem (sem matemática)
Pense em um invariante como “a forma da realidade” para seu tipo:
- Um
Cartnão pode conter quantidades negativas. - Um
UserEmailé sempre um endereço de e‑mail válido (não “validado depois”). - Uma
Reservationtemstart < end, e ambos os horários estão no mesmo fuso.
Se essas afirmações deixam de ser verdade, seu sistema fica imprevisível, porque cada recurso agora precisa adivinhar o que dados “quebrados” significam.
Como invariantes guiam validação e tratamento de erros
Boas APIs aplicam invariantes nas fronteiras:
- Na criação: rejeitar entradas inválidas cedo (retornar erro claro).
- Nas atualizações: permitir apenas mudanças que mantenham o invariante.
- Na leitura/escrita: trate dados externos como não confiáveis; valide antes de armazenar.
Isso melhora naturalmente o tratamento de erros: em vez de falhas vagas depois (“algo deu errado”), a API pode explicar qual regra foi violada (“end deve ser depois de start”).
Não deixe invariantes vazarem pela interface
Chamadores não devem ter que memorizar regras internas como “este método só funciona depois de chamar normalize().” Se um invariante depende de um ritual especial, não é um invariante — é uma armadilha.
Projete a interface de modo que:
- estados inválidos sejam irrepresentáveis (ou difíceis de representar)
- métodos preservem o invariante automaticamente
Checklist prático de documentação
Ao documentar um tipo de API, escreva:
- As declarações de invariante (inglês simples, testável)
- Onde são aplicadas (construtor, setters, endpoints)
- O que acontece na violação (tipo/mensagem de erro, código de status)
- Quais métodos as preservam (e exceções, se houver)
- Exemplos de entradas válidas vs inválidas (curtos, concretos)
Contratos: deixe o comportamento claro para chamadores e mantenedores
Uma boa API não é apenas um conjunto de funções — é uma promessa. Contratos tornam essa promessa explícita, para que chamadores possam confiar no comportamento e mantenedores possam mudar internals sem surpreender ninguém.
O que explicar em um contrato
No mínimo, documente:
- Pré-condições: o que deve ser verdade antes de chamar (intervalos válidos, permissões necessárias, expectativas de thread‑safety).
- Pós-condições: o que será verdade após uma chamada bem‑sucedida (significado do valor retornado, mudanças de estado).
- Efeitos colaterais: o que mais muda (escritas em disco, chamadas de rede, atualizações em objetos passados por referência).
Essa clareza torna o comportamento previsível: chamadores sabem quais entradas são seguras e quais resultados tratar, e testes podem checar a promessa em vez de adivinhar a intenção.
Contratos reduzem a “conhecimento tribal”
Sem contratos, equipes dependem de memória e normas informais: “Não passe null ali,” “Essa chamada às vezes faz retry,” “Retorna vazio em caso de erro.” Essas regras se perdem durante onboarding, refatores ou incidentes.
Um contrato escrito transforma essas regras ocultas em conhecimento compartilhado. Também cria um alvo estável para code reviews: as discussões passam a ser “Essa mudança ainda satisfaz o contrato?” em vez de “Comigo funcionou”.
Bom vs. vago (exemplos)
Vago: “Cria um usuário.”
Melhor: “Cria um usuário com email único.
- Pré-condições:
emaildeve ser um endereço válido; o chamador deve ter permissãousers:create. - Pós-condições: retorna o novo
userId; o usuário é persistido e imediatamente recuperável. - Modos de falha: retorna
409se o email já existir; retorna400para campos inválidos; nenhum usuário parcial é criado.”
Vago: “Retorna itens rapidamente.”
Melhor: “Retorna até limit itens ordenados por createdAt descendente.
- Efeitos colaterais: nenhum.
- Consistência: pode estar até 60 segundos defasado.
- Paginação: use
nextCursorpara a próxima página; cursors expiram após 15 minutos.”
Ocultamento de informação: mantenha internals privados, mantenha APIs estáveis
Ocultamento de informação é o lado prático da abstração de dados: chamadores devem depender do que a API faz, não de como ela faz. Se os usuários não veem seus internos, você pode mudá‑los sem transformar cada release em uma mudança quebradora.
Exponha operações, não representação
Uma boa interface publica um pequeno conjunto de operações (create, fetch, update, list, validate) e mantém a representação — tabelas, caches, filas, layouts de arquivos, fronteiras de serviço — privada.
Por exemplo, “adicionar item ao carrinho” é uma operação. “CartRowId” do seu banco é um detalhe de implementação. Ao expor o detalhe, você convida usuários a construir lógica própria em cima dele, o que trava sua habilidade de mudar.
Por que esconder internals torna refactors seguros
Quando clientes dependem apenas de comportamento estável, você pode:
- trocar bancos ou formatos de armazenamento
- dividir um monólito em serviços
- adicionar cache ou mudar indexação
- reorganizar modelos internos
…e a API permanece compatível porque o contrato não mudou. Esse é o retorno real: estabilidade para usuários, liberdade para mantenedores.
Padrões comuns de vazamento para vigiar
Algumas maneiras de internals escaparem acidentalmente:
- Retornar IDs internos que só fazem sentido na sua camada de armazenamento (inteiros auto‑incrementais, chaves de shard).
- Expor estruturas mutáveis (por exemplo, retornar um objeto bruto que clientes podem alterar e reenviar), o que acopla clientes aos seus campos exatos.
- Permitir que clientes construam estado interno, como aceitar
status=3em vez de um nome claro ou operação dedicada.
Projetando formatos de resposta que permanecem estáveis
Prefira respostas que descrevam significado, não mecânica:
- Use identificadores públicos opacos e estáveis (ex.:
"userId": "usr_…") em vez de números de linha do banco. - Retorne cópias ou views somente leitura de coleções em vez de estruturas cuja ordenação ou campos internos sejam “acidentalmente” utilizados.
- Adicione campos de forma compatível; evite mudar o significado de campos existentes.
Se um detalhe pode mudar, não o publique. Se os usuários precisarem dele, promova‑o a parte deliberada e documentada da promessa da interface.
Princípio da Substituição de Liskov como promessa de interface
O Princípio da Substituição de Liskov (LSP) em uma frase: se um código funciona com uma interface, deve continuar funcionando quando você trocar por qualquer implementação válida dessa interface — sem casos especiais.
LSP é menos sobre herança e mais sobre confiança. Ao publicar uma interface, você está fazendo uma promessa sobre comportamento. LSP diz que todas as implementações devem manter essa promessa, mesmo se usarem abordagens internas muito diferentes.
LSP como “não surpreenda o chamador”
Chamadores confiam no que sua API diz — não no que ela faz hoje. Se uma interface diz “você pode chamar save() com qualquer registro válido”, então toda implementação deve aceitar esses registros válidos. Se a interface diz “get() retorna um valor ou um resultado claro ‘não encontrado’”, as implementações não podem, aleatoriamente, lançar novos erros ou retornar dados parciais.
Extensão segura significa que você pode adicionar novas implementações (ou mudar provedores) sem forçar os usuários a reescrever código. Esse é o benefício prático do LSP: mantém interfaces substituíveis.
Violações comuns de LSP em APIs
Duas formas comuns de violar a promessa são:
-
Entradas mais restritas (pré-condições mais rígidas): uma nova implementação rejeita entradas que a definição da interface permitia. Exemplo: a interface aceita qualquer string UTF‑8 como ID, mas uma implementação só aceita IDs numéricos ou rejeita campos vazios porém válidos.
-
Saídas mais fracas (pós-condições mais fracas): uma nova implementação retorna menos do que foi prometido. Exemplo: a interface diz que os resultados são ordenados, únicos ou completos — ainda assim uma implementação retorna dados sem ordenação, com duplicatas ou silenciosamente omite itens.
Uma terceira violação sutil é mudar o comportamento de falha: se uma implementação retorna “não encontrado” enquanto outra lança exceção para a mesma situação, chamadores não podem substituir uma pela outra com segurança.
Projetando comportamento plug-in sem surpresas
Para suportar “plug‑ins” (múltiplas implementações), escreva a interface como um contrato:
- Especifique quais entradas são válidas e mantenha esse conjunto consistente entre implementações.
- Especifique o que as saídas significam (incluindo ordenação, defaults e casos de borda).
- Padronize modos de falha: quais erros podem ocorrer e o que representam.
Se uma implementação realmente precisa de regras mais rígidas, não as esconda atrás da mesma interface. Ou (1) defina uma interface separada, ou (2) torne a restrição explícita como uma capacidade (por exemplo, supportsNumericIds() ou uma configuração documentada). Assim, os clientes optam conscientemente — em vez de serem surpreendidos por uma “substituição” que, na prática, não é substituível.
Boas interfaces são pequenas, coesas e fáceis de ler
Uma interface bem projetada parece “óbvia” de usar porque expõe apenas o que o chamador precisa — e nada mais. A visão de Liskov sobre abstração de dados puxa você para interfaces estreitas, estáveis e legíveis, para que usuários possam confiar nelas sem aprender detalhes internos.
Prefira coesão a “faz‑tudo”
APIs grandes tendem a misturar responsabilidades não relacionadas: configuração, mudanças de estado, relatórios e troubleshooting tudo no mesmo lugar. Isso torna mais difícil entender o que é seguro chamar e quando.
Uma interface coesa agrupa operações que pertencem à mesma abstração. Se sua API representa uma fila, foque em comportamentos de fila (enqueue/dequeue/peek/size), não em utilitários gerais. Menos conceitos significam menos caminhos de uso indevido.
Evite parâmetros excessivamente flexíveis que criam ambiguidade
“Flexível” muitas vezes significa “pouco claro.” Parâmetros como options: any, mode: string ou múltiplos booleanos (por exemplo, force, skipCache, silent) criam combinações mal definidas.
Prefira:
- métodos específicos para comportamentos distintos (ex.:
publish()vspublishDraft()), ou - um objeto
optionspequeno e bem tipado com defaults documentados e combinações inválidas.
Se um parâmetro exige que chamadores leiam o código‑fonte para saber o que acontece, ele não faz parte de uma boa abstração.
Nomear é parte da interface
Nomes comunicam o contrato. Escolha verbos que descrevam comportamento observável: reserve, release, validate, list, get. Evite metáforas criativas e termos sobrecarregados. Se dois métodos soam similares, chamadores vão supor que se comportam de forma similar — então faça com que isso seja verdade.
Quando dividir em múltiplos módulos/recursos
Separe uma API quando notar uma das duas coisas:
- papéis de usuário diferentes (ex.: “admin” vs “consumer”) precisando de capacidades distintas, ou
- taxas de mudança diferentes (uma parte evolui frequentemente, outra precisa permanecer estável).
Módulos separados permitem que você evolua internals enquanto mantém a promessa central estável. Se planeja crescimento, considere um pacote “core” enxuto mais complementos; veja também /blog/evolving-apis-without-breaking-users.
Evoluindo APIs sem quebrar usuários
APIs raramente ficam paradas. Novas funcionalidades chegam, casos de borda são descobertos e “pequenas melhorias” podem quebrar aplicações reais. O objetivo não é congelar uma interface — é evoluí‑la sem violar as promessas que os usuários já aceitam.
Versionamento semântico (prático, com limites)
Semantic versioning é uma ferramenta de comunicação:
- MAJOR: você fez uma mudança quebradora.
- MINOR: adicionou funcionalidade de forma compatível.
- PATCH: corrigiu bugs sem mudar comportamento pretendido.
Seu limite: ainda é preciso julgamento. Se um “bug fix” muda um comportamento do qual os chamadores dependiam, é quebrador na prática — mesmo que o comportamento antigo fosse acidental.
Mudanças quebradoras são sobre contratos, não só tipos
Muitas mudanças quebradoras não aparecem no compilador:
- Endurecer regras de entrada (rejeitar valores antes aceitos).
- Mudar significado (mesmos campos, interpretação diferente).
- Mudar timing (uma chamada que era rápida vira lenta ou bloqueia).
- Mudar comportamento de erro (novos códigos, retries diferentes, resultados parciais distintos).
Pense em termos de pré-condições e pós-condições: o que os chamadores devem fornecer e o que podem contar em retorno.
Caminhos de deprecação que usuários realmente conseguem seguir
Deprecação funciona quando é explícita e com prazo:
- Marque o comportamento antigo como deprecated na docs e nas respostas (warnings, headers, logs).
- Ofereça uma janela de suporte duplo (antigo e novo lado a lado).
- Publique um cronograma claro (ex.: “novo default em 60 dias, remoção em 180 dias”).
Como a abstração facilita evolução
A abstração ao estilo Liskov ajuda porque reduz o que os usuários podem depender. Se os chamadores dependem apenas do contrato da interface — não da estrutura interna — você pode mudar formatos de armazenamento, algoritmos e otimizações livremente.
Na prática, é aqui que ferramentas fortes ajudam. Por exemplo, se você está iterando rápido numa API interna enquanto constrói um app React ou um backend Go + PostgreSQL, um fluxo de trabalho que acelere a implementação pode ser útil, contanto que a disciplina central permaneça: contratos nítidos, identificadores estáveis e evolução compatível retroativamente. Velocidade é um multiplicador — vale a pena multiplicar os hábitos certos de interface.
Tratamento de erros e modos de falha: projete para previsibilidade
Uma API confiável não é aquela que nunca falha — é aquela que falha de formas que os chamadores entendem, tratam e testam. Tratamento de erros faz parte da abstração: define o que é “uso correto” e o que acontece quando o mundo (redes, discos, permissões, tempo) discorda.
Erros de programador vs. falhas em tempo de execução
Comece separando duas categorias:
- Erros do programador: o chamador violou o contrato (ex.: passar um ID em formato inválido, chamar métodos fora de ordem, esquecer campos obrigatórios). Devem ser capturados cedo e de forma ruidosa — muitas vezes com erros de validação que apontam diretamente para o uso indevido.
- Falhas de runtime: o chamador seguiu o contrato, mas algo externo falhou (timeouts, dependências indisponíveis, limites de cota, conflitos de concorrência). Devem ser representáveis e recuperáveis.
Essa distinção mantém sua interface honesta: chamadores aprendem o que podem consertar no código versus o que precisam tratar em tempo de execução.
Use o contrato para escolher a forma de falha correta
Seu contrato deve implicar o mecanismo:
- Erros (respostas de validação) para violações de contrato.
- Exceções para falhas verdadeiramente excepcionais e não‑locais em bibliotecas — ou quando não é razoável obrigar todos os callsites a ramificar.
- Tipos de resultado (ex.:
Ok | Error) quando falhas são esperadas e você quer que os chamadores as tratem explicitamente.
Qualquer que seja a escolha, seja consistente na API para que os usuários não fiquem adivinhando.
Torne modos de falha explícitos e testáveis
Liste falhas possíveis por operação em termos de significado, não detalhes de implementação: “conflito porque a versão está obsoleta”, “não encontrado”, “permissão negada”, “rate limited”. Forneça códigos de erro estáveis e campos estruturados para que testes possam afirmar comportamento sem depender de strings livres.
Retries, idempotência e sucesso parcial
Documente se uma operação é segura para retry, em quais condições e como alcançar idempotência (chaves de idempotência, IDs naturais de requisição). Se sucesso parcial for possível (operações em lote), defina como sucessos e falhas são reportados e qual estado os chamadores devem assumir após um timeout.
Testando abstrações: prove que a interface cumpre a promessa
Uma abstração é uma promessa: “Se você chamar essas operações com entradas válidas, terá esses resultados, e essas regras sempre serão verdadeiras.” Testar é como você mantém essa promessa enquanto o código muda.
Transforme contratos em testes unitários e de integração
Comece traduzindo o contrato em verificações automáticas.
Testes unitários devem verificar as pós-condições e casos de borda de cada operação: valores retornados, mudanças de estado e comportamento de erro. Se sua interface diz “remover item inexistente retorna false e não altera nada”, escreva exatamente isso.
Testes de integração devem validar o contrato através de fronteiras reais: banco de dados, rede, serialização e autenticação. Muitas “violações de contrato” aparecem apenas quando tipos são codificados/decodificados ou quando retries/timeouts ocorrem.
Testes por propriedade para invariantes
Invariantes são regras que devem valer em qualquer sequência de operações válidas (ex.: “saldo nunca negativo”, “IDs são únicos”, “itens retornados por list() podem ser obtidos por get(id)).
Property‑based testing verifica essas regras gerando muitos inputs e sequências de operações aleatórias porém válidas, buscando contra‑exemplos. Conceitualmente, você está dizendo: “Não importa a ordem das chamadas, o invariante vale.” Isso é especialmente bom para achar casos de borda que humanos não imaginam testar.
Contract testing dirigido por consumidores para APIs públicas
Para APIs públicas ou compartilhadas, permita que consumidores publiquem exemplos de requests que fazem e responses em que confiam. Provedores então executam esses contratos em CI para confirmar que mudanças não vão quebrar usos reais — mesmo quando a equipe provedora não antecipou aquele uso.
Monitore produção para detectar drift de contrato
Testes não cobrem tudo, então monitore sinais que sugerem que o contrato está mudando: alterações na forma de resposta, aumento de 4xx/5xx, novos códigos de erro, picos de latência e falhas de desserialização por “campo desconhecido”. Monitore por endpoint e versão para detectar drift cedo e reverter com segurança.
Se você suporta snapshots ou rollbacks na pipeline de entrega, eles combinam naturalmente com essa mentalidade: detectar drift cedo e reverter sem forçar clientes a se adaptarem no meio de um incidente. (Ferramentas que incluem snapshots e rollback no fluxo se alinham bem com a abordagem “contratos primeiro, mudanças depois”.)
Anti‑padrões comuns e como evitá‑los
Mesmo equipes que valorizam abstração caem em padrões que parecem “práticos” no momento, mas aos poucos transformam uma API em um amontoado de casos especiais. Aqui estão algumas armadilhas recorrentes — e o que fazer em vez disso.
Feature flags permanentes como botões de API
Feature flags são ótimas para rollout, mas o problema começa quando flags viram parâmetros públicos e de longa duração: ?useNewPricing=true, mode=legacy, v2=true. Com o tempo, chamadores os combinam de formas inesperadas e você acaba suportando múltiplos comportamentos para sempre.
Uma abordagem mais segura:
- Mantenha flags de rollout internas quando possível.
- Se o comportamento deve diferir, expresse‑o como uma nova capability com nome e ciclo de vida claros (e plano para remover o antigo).
- Documente combinações válidas; rejeite explicitamente as inválidas.
Vazar conceitos de banco de dados para a interface
APIs que expõem IDs de tabela, chaves de join ou filtros em formato “SQL” (ex.: where=...) forçam clientes a aprender seu modelo de armazenamento. Isso torna refactors dolorosos: uma mudança de esquema vira quebra de API.
Modele a interface em torno de conceitos de domínio estáveis. Deixe clientes pedirem o que querem dizer (“pedidos de um cliente em um intervalo de datas”), não como você guarda isso.
O reflexo de “só adicionar um campo”
Adicionar um campo parece inofensivo, mas mudanças repetidas de “mais um campo” podem borrar responsabilidades e enfraquecer invariantes. Clientes começam a depender de detalhes acidentais e o tipo vira um saco de coisas.
Evite o custo longo‑prazo:
- Introduza um novo tipo focado para um novo conceito.
- Agrupe campos relacionados em um objeto aninhado com significado claro.
- Trate cada adição como uma mudança de contrato: o que ela implica e o que deve sempre ser verdade?
Quando a abstração fica rígida demais
Abstrações demais podem bloquear necessidades reais — como paginação que não expressa “start after this cursor”, ou um endpoint de busca que não permite “match exato”. Clientes então contornam a API (múltiplas chamadas, filtragem local), causando pior performance e mais erros.
A solução é flexibilidade controlada: forneça um pequeno conjunto de pontos de extensão bem definidos (ex.: operadores de filtro suportados), em vez de uma saída aberta.
Simplificar sem remover capacidade
Simplificação não precisa tirar poder. Deprecie opções confusas, mas mantenha a capacidade subjacente com uma forma mais clara: substitua parâmetros sobrepostos por um objeto de requisição estruturado, ou divida um endpoint “faz tudo” em dois endpoints coesos. Então guie a migração com docs versionadas e um cronograma de deprecação (veja /blog/evolving-apis-without-breaking-users).
Checklist prático para projetar APIs confiáveis
Você pode aplicar as ideias de abstração de dados de Liskov com um checklist simples e repetível. O objetivo não é perfeição — é tornar as promessas da API explícitas, testáveis e seguras para evoluir.
Checklist curto
- Invariantes: O que deve sempre ser verdade sobre o dado ou recurso? (ex.: “saldo nunca negativo”, “IDs são únicos”, “itens retornados em ordem estável”).
- Contratos: Para cada operação, escreva pré-condições, pós-condições e efeitos colaterais (incluindo o que não muda).
- Representação oculta: Liste detalhes intencionalmente privados (formato de armazenamento, cache, IDs internos) e garanta que chamadores não possam depender deles.
- Plano de evolução: Decida como adicionar capacidades: estratégia de versionamento, política de deprecação e quanto tempo o comportamento antigo será suportado.
Fluxo de revisão de API rápido (repetível)
- Leia somente a interface (sem implementação). Um novo colega conseguiria prever o comportamento?
- Percorra 5 “testes de história”: um caso normal, um caso vazio, um caso limite, um caso de entrada inválida e um caso de falha.
- Cheque segurança de substituição: se existem várias implementações, trocar uma pela outra surpreenderia os chamadores?
- Procure acoplamento oculto: clientes são forçados a conhecer estados internos, timing ou detalhes de armazenamento?
- Escreva as mudanças quebradoras que você está prestes a introduzir, então redesenhe até a lista estar vazia (ou conscientemente aceita).
Templates de documentação (copiar/colar)
Use blocos curtos e consistentes:
- Operação:
transfer(from, to, amount) - Requer:
amount > 0e contas existem - Garante: saldos atualizados atomica‑mente; soma total preservada
- Erros:
InsufficientFunds,AccountNotFound,Timeout - Notas: idempotência, ordenação, expectativas de performance
Leitura opcional
Se quiser se aprofundar, pesquise: Abstract Data Types (ADTs), Design by Contract e o Princípio da Substituição de Liskov (LSP).
Se sua equipe mantém notas internas, linke‑as em uma página como /docs/api-guidelines para que o fluxo de revisão continue fácil de reaplicar — e se você constrói novos serviços rapidamente (manual ou com um builder orientado por chat), trate essas diretrizes como parte não negociável de “entregar rápido”. Interfaces confiáveis fazem a velocidade multiplicar em vez de se voltar contra você.
Perguntas frequentes
Por que o trabalho de Barbara Liskov ainda importa para design de APIs hoje?
Ela popularizou o conceito de abstração de dados e o ocultamento de informação, que se traduzem diretamente no design moderno de APIs: publique um contrato pequeno e estável e mantenha a implementação flexível. O ganho é prático: menos mudanças quebradas, refatorações mais seguras e integrações mais previsíveis.
O que significa “interface confiável” em termos de produto e engenharia?
Uma API confiável é aquela em que os chamadores podem confiar ao longo do tempo:
- Versões novas não quebram consumidores existentes.
- Modos de falha são consistentes e documentados.
- Internos podem mudar sem alterar o comportamento público.
Confiabilidade é menos sobre “nunca falhar” e mais sobre falhar de forma previsível e cumprir o contrato.
Como eu transformo um endpoint ou método de API em uma promessa de comportamento clara?
Escreva o comportamento como um contrato:
- Pré-condições: o que deve ser verdade antes da chamada (intervalos válidos, permissões).
- Pós-condições: o que será verdade após o sucesso (valores retornados, mudanças de estado).
- Efeitos colaterais: o que mais muda (gravações, chamadas de rede, atualizações de cache).
Inclua casos de borda (resultados vazios, duplicatas, ordenação) para que os chamadores possam implementar e testar contra a promessa.
O que são invariantes e onde uma API deve aplicá-las?
Um invariante é uma regra que deve sempre valer dentro de uma abstração (por exemplo, “quantidade nunca negativa”). Enforce invariantes nas fronteiras:
- Valide na criação/atualização.
- Rejeite entradas inválidas cedo com erros específicos.
- Evite “rituais” como “chame normalize() primeiro” obrigatórios.
Isso reduz bugs a jusante porque o restante do sistema para de lidar com estados impossíveis.
O que é ocultamento de informação e como aplicá-lo a formatos de resposta e IDs?
Ocultamento de informação significa expor operações e significado, não a representação interna. Evite acoplar consumidores a coisas que você pode querer mudar depois (tabelas, caches, chaves de shard, estados internos).
Táticas práticas:
- Use IDs públicos opacos e estáveis (ex.:
usr_...) em vez de IDs de linha do banco. - Não exija que clientes construam estado interno (evite
status=3). - Adicione campos de maneira compatível com versões sem alterar o significado dos existentes.
Por que vazar conceitos de banco de dados para a API é um problema comum a longo prazo?
Porque congelam sua implementação. Se clientes dependem de filtros com formato de tabela, chaves de join ou IDs internos, uma refatoração de esquema vira uma mudança de API quebradora.
Prefira perguntas de domínio em vez de perguntas de armazenamento, como “pedidos de um cliente em um intervalo de datas”, e mantenha o modelo de armazenamento privado atrás do contrato.
O que é o Princípio da Substituição de Liskov (LSP) em termos práticos de API?
LSP significa: se o código funciona com uma interface, ele deve continuar a funcionar com qualquer implementação válida dessa interface sem casos especiais. Em termos de API, é a regra “não surpreenda o chamador”.
Para suportar implementações substituíveis, padronize:
- Entradas válidas (nenhuma implementação adiciona pré-condições mais rígidas).
- Garantias de saída (ordem, completude, unicidade).
- Comportamento de falha (mesmos significados para erros e “não encontrado”).
Quais são violações comuns de LSP quando existem múltiplas implementações ou provedores?
Fique atento a:
- Entradas mais restritas: uma nova implementação rejeita entradas que a interface permitia.
- Saídas mais fracas: ela descarta itens, altera ordenação ou retorna dados parciais sem avisar.
- Semântica de falha diferente: uma retorna “não encontrado”, outra lança exceção ou retorna formato de erro distinto.
Se uma implementação precisa de restrições extras, publique uma interface separada ou uma capacidade explícita para que os chamadores optem conscientemente.
Como projetar uma API que permaneça pequena, coesa e fácil de entender?
Mantenha interfaces pequenas e coesas:
- Prefira operações focadas que correspondam a uma abstração.
- Evite
options: anye pilhas de booleanos que criam combinações ambíguas. - Use nomes que descrevam comportamento observável (
reserve,release,list,validate).
Se existem papéis diferentes ou ritmos de mudança distintos, separe módulos/recursos (para mais sobre evolução, veja /blog/evolving-apis-without-breaking-users).
Como devo projetar o tratamento de erros para que as falhas sejam previsíveis e testáveis?
Projete erros como parte do contrato:
- Separe erros do programador (violações de contrato) de falhas de tempo de execução (timeouts, conflitos, cotas).
- Documente códigos/estruturas de erro estáveis para que testes não dependam de mensagens de texto.
- Especifique segurança para retry e idempotência (chaves, IDs de requisição) e defina sucesso parcial para operações em lote.
Consistência é mais importante que o mecanismo exato (exceções vs. tipos de resultado), desde que os chamadores possam prever e tratar os resultados.