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.

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.
Uma API confiável não é apenas “correta” em sentido teórico. É uma interface que ajuda um produto a andar mais rápido:
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.
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.
Ao final deste texto, você terá maneiras práticas de:
Se quiser um resumo rápido depois, vá para /blog/a-practical-checklist-for-designing-reliable-apis.
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.
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.
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).
O essencial é a promessa: pop() retorna o último push(). Se a pilha usa um array, uma lista ligada ou outra coisa é privado.
A mesma separação aparece em todo lugar:
POST /payments é a interface; checagens de fraude, retries e gravações no banco são implementação.client.upload(file) é a interface; chunking, compressão e requisições paralelas 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.
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.
Pense em um invariante como “a forma da realidade” para seu tipo:
Cart não pode conter quantidades negativas.UserEmail é sempre um endereço de e‑mail válido (não “validado depois”).Reservation tem start < 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.
Boas APIs aplicam invariantes nas fronteiras:
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”).
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:
Ao documentar um tipo de API, escreva:
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.
No mínimo, documente:
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.
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”.
Vago: “Cria um usuário.”
Melhor: “Cria um usuário com email único.
email deve ser um endereço válido; o chamador deve ter permissão users:create.userId; o usuário é persistido e imediatamente recuperável.409 se o email já existir; retorna 400 para campos inválidos; nenhum usuário parcial é criado.”Vago: “Retorna itens rapidamente.”
Melhor: “Retorna até limit itens ordenados por createdAt descendente.
nextCursor para a próxima página; cursors expiram após 15 minutos.”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.
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.
Quando clientes dependem apenas de comportamento estável, você pode:
…e a API permanece compatível porque o contrato não mudou. Esse é o retorno real: estabilidade para usuários, liberdade para mantenedores.
Algumas maneiras de internals escaparem acidentalmente:
status=3 em vez de um nome claro ou operação dedicada.Prefira respostas que descrevam significado, não mecânica:
"userId": "usr_…") em vez de números de linha do banco.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.
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.
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.
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.
Para suportar “plug‑ins” (múltiplas implementações), escreva a interface como um contrato:
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.
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.
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.
“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:
publish() vs publishDraft()), ouoptions pequeno 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.
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.
Separe uma API quando notar uma das duas coisas:
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.
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.
Semantic versioning é uma ferramenta de comunicação:
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.
Muitas mudanças quebradoras não aparecem no compilador:
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.
Deprecação funciona quando é explícita e com prazo:
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.
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.
Comece separando duas categorias:
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.
Seu contrato deve implicar o mecanismo:
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.
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.
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.
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.
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.
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.
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.
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”.)
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 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:
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.
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:
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.
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).
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.
Use blocos curtos e consistentes:
transfer(from, to, amount)amount > 0 e contas existemInsufficientFunds, AccountNotFound, TimeoutSe 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ê.
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.
Uma API confiável é aquela em que os chamadores podem confiar ao longo do tempo:
Confiabilidade é menos sobre “nunca falhar” e mais sobre falhar de forma previsível e cumprir o contrato.
Escreva o comportamento como um contrato:
Inclua casos de borda (resultados vazios, duplicatas, ordenação) para que os chamadores possam implementar e testar contra a promessa.
Um invariante é uma regra que deve sempre valer dentro de uma abstração (por exemplo, “quantidade nunca negativa”). Enforce invariantes nas fronteiras:
Isso reduz bugs a jusante porque o restante do sistema para de lidar com estados impossíveis.
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:
usr_...) em vez de IDs de linha do banco.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.
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:
Fique atento a:
Se uma implementação precisa de restrições extras, publique uma interface separada ou uma capacidade explícita para que os chamadores optem conscientemente.
Mantenha interfaces pequenas e coesas:
options: any e pilhas de booleanos que criam combinações ambíguas.Projete erros como parte do contrato:
Consistência é mais importante que o mecanismo exato (exceções vs. tipos de resultado), desde que os chamadores possam prever e tratar os resultados.
status=3reserve, 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).