O bug está sempre na fronteira
Dentro de um módulo, o código é consistente consigo mesmo. Os bugs moram onde duas coisas com premissas diferentes se encontram.
Dentro de uma função bem escrita, bugs são raros. O autor segurou tudo na cabeça, os tipos batem, os testes cobrem. Os bugs sérios, os que custam dinheiro e chegam ao canal de incidentes, moram em outro lugar: nas emendas. Onde um serviço chama outro. Onde uma string vira número. Onde o relógio do navegador encontra o do servidor. Onde o código encontra o humano. Depois de centenas de auditorias, não procuro mais bugs no meio das coisas. Procuro as bordas e leio duas vezes.
O que é uma fronteira
Fronteira é qualquer lugar em que duas partes de um sistema têm premissas diferentes sobre o mesmo dado. Um serviço que guarda valores em centavos chama um que espera decimais. Um frontend que envia datas em hora local fala com um backend que guarda em UTC e esquece de converter. Uma fila entrega mensagens pelo menos uma vez para um consumidor escrito como se fosse exatamente uma vez. Um campo de formulário remove espaços e o índice único do banco não. Cada lado está correto nos próprios termos. O bug é o desacordo, e ninguém é dono do desacordo porque ele não está dentro do código de ninguém.
As fronteiras que confiro primeiro
Tipos cruzando a rede: todo payload JSON é tipado como string até prova em contrário, e 'true', true e 1 são três valores diferentes tratados como um. Tempo: qualquer lugar em que um timestamp é criado, interpretado ou comparado, com atenção especial a se o fuso está explícito. Dinheiro: qualquer lugar em que um valor monetário muda de representação, principalmente de float para qualquer coisa. Identidade: qualquer lugar em que um ID de um sistema é usado para buscar algo em outro. Semântica de entrega: qualquer lugar em que uma mensagem, webhook ou evento é consumido, e se o consumidor sobrevive a recebê-lo duas vezes ou fora de ordem. Autorização: qualquer lugar em que o código confia que quem chamou já conferiu quem é o usuário.
Num marketplace que auditei, o bug inteiro de precificação se resumia a uma fronteira: um total calculado como float num serviço e guardado como inteiro em outro, com o arredondamento acontecendo em lados diferentes dependendo do caminho do código. Todo módulo estava correto. A emenda não.
Por que fronteiras são pouco testadas
Testes unitários vivem dentro de um módulo, por definição. Testes de integração existem, mas são mais lentos, mais instáveis e menos numerosos, então cobrem o caminho feliz através da emenda e nada mais. Ninguém escreve o teste para o que acontece se o outro lado mandar um nulo aqui, porque o contrato do outro lado diz que nunca vai mandar, e o contrato é um comentário. O resultado é que as partes do sistema com mais premissas têm menos verificação.
Como tornar fronteiras seguras
Valide em toda fronteira, na entrada, com um esquema, e rejeite o que não conforma. Não confie no outro lado nem quando é seu próprio código, porque seu próprio código muda. Converta para uma representação interna canônica imediatamente: um tipo para dinheiro, um para tempo, um para identificadores, e nunca deixe o formato da rede vazar para dentro. Escreva o contrato como código, não como prosa, e versione. E teste a fronteira explicitamente: a mensagem duplicada, o campo ausente, o deslocamento de fuso, a string onde se esperava número. Esses testes são baratos de escrever depois que você decide que a fronteira é onde está o risco.
A fronteira humana
A última emenda é a que fica entre o sistema e a pessoa que o usa. O campo que aceita data em dois formatos e escolhe o errado em silêncio. O botão que pode ser apertado duas vezes. A confirmação que parece sucesso quando foi falha parcial. Cada um desses é uma fronteira com as mesmas propriedades das que ficam entre serviços, e a mesma regra vale: assuma que o outro lado vai fazer o inesperado, e garanta que o sistema continue correto quando isso acontecer.