Foto com um saleiro e um pimenteiro vistos de lado

Temperos de Arquitetura

Já faz alguns anos que eu estou trabalhando com modelagem de sistemas. Em alguns lugares chamam isso de “arquitetura”, mas uma colega arquiteta (CREA e “talz”) me explicou que não é muito adequado usar a palavra “arquitetura” para definir esse trabalho. Como não sou o especialista e nem estou interessado em me aprofundar nesse tipo de discussão resolvi aceitar os argumentos e não usar mais “arquitetura”.

Minha forma de trabalho é muito intuitiva e baseada em experiências práticas com coisas que deram certo e coisas que não funcionaram bem. Muitas coisas que fiz usando pura intuição se mostraram, mais adiante, como coisas que já existiam e já eram muito estudadas. Só eu é que ignorava.

Capa do livro Designing Data-Intensive Applications

Estou lendo um livro chamado “Designing Data-Intensive Applications” (Martin Kleppmann) que descreve vários desafios e soluções para desenvolvimento de sistemas distribuídos e conforme vou avançando na leitura vou sendo surpreendido com coisas que eu já fiz sem nem saber que aquilo tinha um nome. 🙂

Nesse artigo eu vou listar algumas técnicas que sempre uso para modelar sistemas em que estou trabalhando. É um ‘braindump’ de técnicas listadas sem nenhuma pretensão, estrutura, ou ordem de importância.

Provavelmente não é uma lista completa também. Uma coisa é ter um repertório de técnicas para usar no dia-a-dia. Outra coisa é lembrar de todas elas para escrever um artigo. Sou péssimo para lembrar das coisas.

As dicas de modelagem que apresentarei abaixo podem ser subdividas em duas categorias: Princípios e Práticas. Vamos dar uma olhada nelas.

Princípios

Os princípios que uso no meu trabalho são mais abstratos e servem para orientar minhas escolhas em um nível de abstração mais alto.

Menos é mais

Quando estou trabalhando na modelagem de um sistema eu gosto muito de limitar minhas ferramentas. Eu trabalho melhor com a restrição do que com a abundância. Eu também gosto de ser um pouco conservador nessa hora.

Um exemplo de conservadorismo e restrição é: se eu preciso oferecer uma API para ser usada publicamente eu provavelmente especificarei uma API HTTP REST. É simples, todo mundo conhece e sabe usar, tem ferramentas infinitas para tudo e é bem elegante.

Outro exemplo desse conservadorismo aparece sempre que eu preciso armazenar dados em algum lugar. As chances de eu escolher um banco de dados relacional (PostgreSQL, é claro 😉 ) para a solução é enorme. Eu escolho SQL porque esse modelo está “por aí” a décadas e tem um número incontável de pessoas usando. E servidores de banco de dados evoluíram absurdamente nos últimos anos.

Se os prazos são um pouco mais apertados (quando eles não são?) eu também escolho ferramentas que eu tenho um domínio maior. Exemplo: se eu precisar desenvolver uma solução distribuída é bem provável que uma linguagem com o perfil de Elixir tenha o ‘fit’ perfeito. Mas eu não conheço Elixir tanto quanto conheço Python. Então é provável que eu vá de Python. Com Django porque eu conheço melhor também.

Se essa escolha se mostrar equivocada (nunca aconteceu) a gente planeja a troca.

Quando chega aquela hora de dizer o nome das ferramentas e tecnologias que vamos usar eu também dou preferência para serviços gerenciados por terceiros à serviços que eu tenha que manter eu mesmo.

KISS

O princípio KISS (Keep It Super Simple*) é bastante antigo e a primeira vez que ouvi falar dele foi quando estava aprendendo a usar Unix. Antes de usar o Unix eu costumava usar o (MS|DR|Novell|PC)-DOS. No DOS, quando eu precisava mostrar o conteúdo de um arquivo eu fazia:

C:\> TYPE README.TXT

Se o arquivo fosse muito grande ele ia rolar a tela até o final e, diferente de hoje onde conseguimos rolar a janela para ver o que aconteceu, não conseguíamos ver o que estava no início do arquivo. Para ver o conteúdo de um arquivo pausando tela por tela a gente fazia:

C:\> TYPE README.TXT /P

E pronto. Notem que o comando TYPE sabia mostrar o conteúdo do arquivo e pausar de tela em tela. Quando passei a trabalhar em um Unix (no estágio) me ensinaram que o comando usado para mostrar o conteúdo de um arquivo era o cat. Então fui lá e mandei um:

% cat README.TXT
cat: README.TXT: No such file or directory
% cat README  # ops! aprendendo que o filesystem do Unix é case-sensitive! :)

E então o conteúdo do arquivo despencou a rolar pela tela. Ótimo! Aprendi a ver o conteúdo do arquivo! Agora preciso ver qual o parâmetro para ele parar de tela em tela…

 % man cat

… e nada… Pensei: “mas que bela porcaria esse sistema, hein?”. Foi quando o meu supervisor de estágio chegou e falou: “No Unix os comandos fazem só uma coisa. E fazem bem essa coisa. Se você quer pausar a saída do cat (ele falou monoespaçado assim mesmo 😛 ) você precisa jogar a saída dele pro more. Assim ó…”. E digitou:

 % cat README | more

Pronto. Depois descobri que essa filosofia era chamada de KISS.

Essa história toda serve para ilustrar o que eu faço quando estou modelando um sistema: tento manter cada componente (seja ele um pacote, classe, microsserviço, etc) muito simples.

Esse princípio também pode ser chamado de “Single Responsibility Principle” (a letra “S” em SOLID). Eles dizem basicamente a mesma coisa mas o “Single Responsibility” formaliza mais o seu significado.

Quando cada um desses componentes é simples e tem uma única responsabilidade eles inevitavelmente serão também mais coesos. E coesão é algo desejável em um bom componente.

Baixo acoplamento e Alta coesão

Um dos melhores livros de programação orientada à objetos que li é o “Fundamentals of object-oriented design in UML” (Meilir Page-Jones). Nesse livro ele bate bastante na tecla de que um bom “objeto” (componente, pacote, etc) precisa ter as seguintes características:

Baixo acoplamento

Se você precisa fazer uma alteração simples em apenas um único comportamento da sua aplicação, quantos componentes diferentes do código você precisa mexer? A resposta para essa pergunta fala bastante sobre o acoplamento da sua aplicação.

O baixo acoplamento é desejável porque ele faz com que sua aplicação fique mais fácil de ser mantida e de ser estendida com novas funcionalidades. O melhor método de se chegar ao baixo acoplamento é por meio do processo de refactoring** constante.

Tentar desenvolver código com baixo acoplamento logo de largada é difícil, demorado, e pode te aprisionar em todo tipo de problema relacionado à early abstraction. Então encare o “baixo acoplamento” como objetivo e não como requisito.

Alta coesão

Esse conceito é um pouquinho mais complicado de explicar, mas o significado do adjetivo “coeso” pode nos dar uma dica. Coeso, segundo alguns dicionários, significa:

  1. Que se relaciona através da coesão, por meio da lógica, de forma harmônica: fala coesa, proposta coesa, atitude coesa.
  2. Intimamente unido; ligado com intensidade.
  3. Disposto de maneira equilibrada, proporcional; ajustada. (figurado)
  4. Seguindo um raciocínio lógico, com nexo; coerente. (figurado)
  5. Sinônimos: coerente, harmônico, ajustado.

Sei que avaliar um componente sob essa perspectiva é muito subjetivo e pode significar coisas diferentes para pessoas diferentes.

Como eu faço isso? Eu olho para o código do componente e procuro por todo tipo de coisa que não deveria estar ali. Pergunto-me porque aquilo não deveria estar ali e penso em algum jeito de mover essa parte para um local mais adequado.

Dividir para conquistar

Na nossa vida de programador a gente está sempre resolvendo problemas. E problemas tem tamanho. Tem problema pequeno, médio, grande, … Resolver problemas pequenos costuma ser (nem sempre é) mais fácil do que resolver problemas grandes.

Por isso um dos skills mais importantes que um bom programador precisa ter é a de dominar a arte de quebrar problemas.

A dica aqui é simples de passar, mas difícil de dominar. Se está difícil resolver um problema:

  1. Pare
  2. Dê uns passos para trás
  3. Olhe para o problema e busque por pontos de quebra
  4. Quebre o problema
  5. Tente resolver uma parte
  6. Se funcionar: profit!
  7. Se não funcionar: volte para o passo 1.

Trabalhei muito com comércio eletrônico ao longo na minha vida e, nesse contexto, sempre encontrei diversos tipos de problemas para resolver. Um desses problemas é: gerenciamento de produtos.

É um problemão… tem questões de marca, especificações, preço, estoque, venda, promoções, kits, recomendações, etc. Não dá para resolver isso tudo de uma única vez e, mesmo se a gente dividir cada uma dessas coisas em várias, os problemas resultantes podem continuar gigantes.

Mas vou falar sobre um fluxo básico: eu sou vendedor (seller) de um canal (channel) de marketplace e cadastro um produto no site para vender. Só que tem outro vendedor que vende o mesmo produto.

Primeira vez que modelei esse problema eu fiz: SellerProduct e ChannelProduct. Tinha uma instância de SellerProduct para cada vendedor com seu respectivo estoque, e preço e um ChannelProduct no canal apontando para um desses produtos de um desses vendedores. A gente dizia que um desses vendedores estava ganhando a “buy box”.

Quando o primeiro vendedor cadastrou o produto ele informou que aquele produto era bivolt. O segundo vendedor falou que esse mesmo produto era 220V. Que informação eu coloco no site? Quem está certo?

Outro problema: o vendedor #1 é de Recife e o vendedor #2 é de POA o preço dos dois é igual e eles têm estoques parecidos. O cliente de Maceió chega no site para simular o valor do frete. É bem provável que o frete do vendedor #1 seja melhor, mas quem ‘ganhou a buy box’ foi o vendedor #2. Como calcular todos os fretes de todos os vendedores e escolher o melhor rapidamente? Perdemos a venda?

Como vocês podem notar a modelagem não está dando conta do recado. Precisamos repensar ela. Talvez a gente precise quebrar esse problema ainda mais.

Nesse processo a primeira coisa que ficou clara para gente é que “Produto” significa muitas coisas diferentes para pessoas e contextos diferentes.

Para o vendedor um produto é “um item no seu estoque”. Ou um “SKU em seu portfólio”. Para uma marca/fabricante/importador um produto é “algo que ele produz com certas características”. Para o canal de venda um produto é “algo que eu estou ofertando”. E para o cliente o produto é “algo que ele compra”.

Entenderam o raciocínio? O super problema “gerenciamento de produtos” precisa ser quebrado em problemas menores:

  1. Gestão de Portifólio (SKU e estoque)
  2. Gestão de Catálogo (características de um produto)
  3. Gestão de Ofertas/Distribuição (anúncios e vitrines)

E cada um desses sub-problemas ainda pode passar por mais um processo de quebra.

Lazy Preoccupation

Esse princípio foi adicionado mais recentemente ao meu repertório. E eu mesmo que dei esse nome (então nem adianta procurar ele na internet 😛 ). Esse princípio deriva da minha experiência de trabalho com metodologias ágeis de desenvolvimento de software.

O princípio da preocupação tardia (lazy preocupation) é: resolve o problema que tem para resolver agora e deixa os problemas futuros para serem resolvidos no futuro.

Parece bobo de tão óbvio (e é), mas é muito interessante ver como eu ainda falho na aplicação desse princípio em certas ocasiões.

A questão aqui é: se você já está trabalhando na solução do menor problema possível (ver tópico anterior) e está funcionando é provável que o mesmo aconteça com os futuros problemas quando chegar a vez deles serem resolvidos.

Quando o futuro chegar, também, é muito provável que sua compreensão sobre o domínio do problema já esteja mais evoluída e solucionar ele fique até mais fácil.

E mesmo nos casos onde isso não acontece e a solução do problema anterior trava a solução do problema futuro é só dar uns passos para trás e tentar outra abordagem. Agora você vai conseguir fazer isso de forma muito mais efetiva porque já tem conhecimentos complementares para te guiar.

Também aplico esse princípio para lidar com questões de modelagem vs. implementação. Quando estou criando a modelagem de um sistema eu tento não me preocupar com características de implementação. Qual banco de dados vou usar? A API vai ser REST ou gRPC? Vou usar serviços ‘serverless’? Isso vai ficar lento?

Esse tipo de preocupação, logo no começo, atrapalha demais o foco no problema e na modelagem na solução abstrata dele. O melhor momento para pensar na implementação da solução é no momento em que você for implementar ela.

Práticas

Abrace as falhas e as hostilidades

Programadores tentam escrever softwares sem falhas. Eles estudam para melhorar suas habilidades e produzir código com mais qualidade. Aprendem a fazer testes automatizados tanto para melhorar o desenho das suas implementações (TDD) quanto para garantir que o software funcione conforme o esperado. O problema está aí “conforme esperado”. O que isso significa exatamente? O que acontece quando um software falha? E quando isso acontece de forma “inesperada”? É possível escrever um software infalível? Não. Não é.

Se não é possível escrever software infalível porque a gente ainda escreve software esperando que o melhor aconteça? E porque a gente tenta esconder essas falhas dos clientes desse software?

Acho que isso acontece porque a gente, como programador, considera a falha de um sistema como uma falha pessoal. Algo que é responsabilidade nossa. “Como eu não pensei nesse cenário? Como sou burro!”, não é mesmo?

Ok… Mas se todos concordamos que é impossível escrever um software porque nos culpamos pelas falhas? Se todo sistema falha não seria melhor aceitar essas falhas de forma mais natural? Expor elas sempre que acontecerem? Falhar o mais rápido possível ao invés de segurar uma situação insustentável por mais tempo e aumentar o estrago?

Quando estou desenhando uma solução eu sempre carrego a premissa de que todos os componentes que estou escrevendo ou usando vão falhar em algum momento.

Resguardar todos esses pontos para garantir de que nenhum dessas partes irá causar um dano muito grande em caso de falha também é muito difícil. Talvez seja fundamentalmente impossível garantir isso (halting problem).

Quanto mais queremos proteger e acrescentar redundâncias e proteções à nossa solução, mais custo e complexidade vamos adicionando nela… o que eu faço então?

Eu sempre projeto sistemas que estejam prontos para continuar funcionando mesmo em cenários de falhas pontuais. Não dá para proteger todos os pontos, logo, se alguns componentes específicos falharem a coisa vai despencar completamente. À esses pontos então eu acrescento uma camada de redundância e uma camada de monitoramento e alertas mais rigorosos. Pronto.

Na minha apresentação sobre a arquitetura de uma das empresas onde eu trabalhei os nossos “Calcanhares de Aquiles” eram:

  • PaaS (Heroku) – se o Heroku caísse dava bem ruim. Não tinha redundância então era só monitoramento mesmo. Também usávamos o PostgreSQL deles. Mas nesse caso tinha uma fina camada de redundância (dentro do próprio Heroku que não é o ideal).
  • AWS SNS – se esse falhasse despencava todas as operações ‘online’ da empresa. Só monitoramento.
  • AWS SQS – se esse caísse a gente perderia dados em um nível muito grave. Então tinha monitoramento e sempre garantia um bom número de workers para esvaziar essas filas.
  • AWS – Se a AWS inteira caísse… bom… ficaria complicado haha 🙂 Mas nesse caso a Internet inteira estaria com problemas.
  • DNS – Esse é sempre um problema para a Internet inteira.

Qualquer outra coisa que ficasse fora do ar além dessas causaria alguma degradação ao sistema, mas ele se reestabeleceria com a normalização dos serviços. Tem mais detalhes sobre essa arquitetura nesses links aqui:

Nessa arquitetura, se um serviço falhasse ele retornava um código de falha. Se fosse um erro 5XX era um erro 5XX e ponto final. Não é vergonha retornar um 500 Internal Server Error se de fato um Erro Interno no Servidor (Internal Server Error dã!) aconteceu, oras! Não precisamos “passar pano” para erro de servidor. Se o servidor estivesse fora do ar para manutenção? 503! Se tivesse lento? Timeout! E assim vai.

Quando a gente retornava um erro para o cliente (ex. worker) ele pode decidir como lidar com aquele erro. Tentar outra vez? Descartar a mensagem? Guardar o erro em um log? Não importa. Ele vai saber que aquela operação não aconteceu. Agora imagina se o servidor falhou na operação e retornou um 200 Ok dizendo que tá tudo sobre controle?

Então abracem as falhas. Os erros. Tratem seus serviços como falíveis e vocês vão sempre desenhar soluções mais robustas.

Idempotência é sua amiga

Eu disse no tópico anterior que se aconteceu uma falha no seu serviço você tem que deixar isso claro para seu cliente, certo? Mas não custa nada dar uma mãozinha para ele se recuperar dessa falha depois.

Vamos supor que tenho um worker que pegou uma mensagem de uma fila e precisa mandar 15 requests para uma API baseado nessa mensagem. Ele manda o primeiro e “ok”. Manda o segundo e… “ok”. Manda o terceiro e “ERRO!”. Tento mandar o quarto e… “ERRO!” e assim vai até o fim.

O que eu faço com os requests que falharam? Tento outra vez? E se continuarem a falhar? O que eu faço?

Em teoria você precisaria de um lugar para “anotar” quais requests falharam e quais tiveram sucesso em algum lugar para retentar só aqueles requests que falharam? Mas o worker não guarda estado. Ele só pega mensagem de uma fila e procede com os requests.

Criar um sistema só para guardar os requests que precisam ser feitos é muito complexo. E se esse sistema também ficar fora do ar? Entenderam o drama?

Pois bem. Fizesse a tal API ser idempotente? Se você repetir um request que já aconteceu antes ela responderia algo tipo: 304 Not Modified ou 303 See Other (que são 2 códigos HTTP de sucesso?).

Se sua API for implementada desse jeito o seu worker pode falhar completamente a transação (devolvendo a mensagem para a fila) porque quando ele precisar repetir essa operação ele vai repetir exatamente os mesmos 5 requests. E nenhuma informação vai ficar duplicada ou faltando do lado da API.

Tente sempre fazer com que suas interfaces privilegiem operações idempotentes (mesmo que para isso precise fazer umas concessões aos padrões de POST e PATCH).

HTTP é rei e a Web é uma API

Duas das coisas mais difíceis em computação, para mim, é dominar a arte secreta de se escrever bons protocolos e boas linguagens de programação. Só os grandes gênios da computação conseguem fazer isso bem feito.

Um dos protocolos mais elegantes que já vi é o HTTP. Ele é simples, poderoso, compreensível, escalável, bem conhecido e tem ótimas implementações disponíveis para todo mundo.

Com a explosão no surgimento de APIs REST foi possível mostrar que o protocolo HTTP, por si só, permite implementar um número gigantesco de soluções mesmo sendo um protocolo muito básico com apenas um punhado de métodos (GETPOSTPUTPATCHDELETE, etc).

É isso mesmo: APIs REST tem somente esses métodos definidos por RFCs. Então aqueles ‘verbos’ nas URLs (ex. /user/subscribe) da sua API são, no mínimo, uma licença poética 🙂

O que eu acho interessante nessa “limitação” é justamente isso: ela me força a refletir melhor sobre os objetos (ou documento, ou resource, ou …) expostos na minha API. É uma limitação que me força a pensar uma solução que consiga funcionar na simplicidade do protocolo.

Coisas como gRPC abrem esse leque de opções absurdamente e, apesar de facilitar o desenvolvimento e a entrega do produto final, exige muita cautela por parte do desenvolvedor para não criar um monstrengo de API com dezenas de métodos diferentes. E é aqui onde a gente reafirma o princípio do “Menos é mais” que listei lá em cima. Já interfaces com GraphQL tem um propósito muito específico: navegar por graphos e não deveriam ser abusadas para outros usos (até porque APIs GraphQL usam o protocolo HTTP de forma bem… estranha…).

Na minha apresentação “A Web é uma API” eu ilustro alguns conceitos que demonstram como a Web já é uma API inerentemente REST e no Toy, um framework de brinquedo, eu experimento esse conceito:

Event Sourcing / Fire and Forget

Event Sourcing é um modelo de arquitetura de software que casa super bem com o princípio da Lazy Preocupation e com o princípio do Menos é mais. Adotar esse modelo também permite que a gente trabalhe com APIs mais “burras” (simples/KISS) e, como veremos adiante, isso é desejável.

Em arquiteturas mais “tradicionais” é comum à um serviço comandar operações, ou seja, o serviço determina e chama as operações que precisam ser executadas indiferentemente de uma sequência específica (sync) ou não (async).

Um sistema que de gerenciamento de pedidos de um site de comércio eletrônico, por exemplo, ao receber um novo pedido, precisa gravar esses dados em um banco de dados, mandar um e-mail para o consumidor para confirmar o recebimento do pedido, avisar o sistema de fulfillment que tem um pedido novo que precisa ser preparado, etc, etc.

Note que nesse modelo o sistema de gerenciamento de pedidos precisa distribuir todas essas tarefas para sistemas externos e isso cria uma dependência de todos esses sistemas no sistema de gestão de pedidos.

Imaginemos que, no futuro, esse mesmo sistema de pedidos precise executar uma operação que envia os dados desse pedido para um novo subsistema de BI. Você vai implementar/implantar esse sistema de BI e vai ter que tambémadicionar uma chamada para ele no sistema de gestão de pedidos. Notaram o acoplamento aparecendo aqui?

Em uma arquitetura baseada em eventos (event sourcing) o sistema de gestão fica responsável apenas por registrar o novo pedido e avisar que tem um “pedido novo” à quem possa interessar publicando esse evento em um tópico (ou subject) de um barramento de mensagens (publish).

Se um sistema de fulfillment tem interesse nesse novo pedido (bem provável) ele só assina (subscribe) o tópico sobre novos pedidos e faz o que tem que ser feito em cada novo pedido.

Usar um sistema de filas persistentes para assinar esse tópico é bastante prudente porque facilita o processamento desses eventos mesmo em cenários de falhas de workers ou instabilidades.

Quando o nosso sistema de BI estiver implantado é necessário apenas conectar ele ao sistema de pedidos através de outra assinatura ao tópico de novos pedidos e, desse modo, o sistema de gesrenciamento de pedidos nem precisa tomar conhecimento desse novo sistema. É só disparar e esquecer. Fire and Forget.

APIs burras e autônomas, workers espertos e dependentes

Para que a gente considere uma API HTTP boa ela precisa apresentar um conjunto muito grande qualidades. Características como robustês, estabilidade e performance são só algumas dessas qualidades.

Quando uma API tem muitas responsabilidades e faz muitas coisas é bem provável que sua complexidade cresça demais e entregar essas qualidades começam a se tornar um desafio bem grande.

E se as APIs delegassem essa complexidade para outros agentes da arquitetura? E se elas fizessem menos coisas? E se elas fizessem só o básico de validação de dados, armazenassem os dados que precisam ser armazenados e avisassem que essa operação aconteceu?

Uma API que sabe validar, guardar e notificar o que aconteceu entrega tudo o que uma API precisa fazer. Mas ela precisa ser capaz de fazer isso sozinha. Se a validação de uma informação precisar de algum dado externo esse dado precisa ser “injetado” nessa API. Mesmo que isso cause uma duplicação (desnormalização).

Se a função de uma API se restringir à essas 3 operações fica muito fácil implementar uma API simples, rápida, robusta e estável porque o código dela será muito simples. Quase nenhuma regra de negócio incorporada ao seu funcionamento.

As regras de negócio mais complexas podem ficar em workers que estarão processando os eventos de uma fila (que assina um tópico) e terão todo o tempo do mundo para aplicar essas regras, fazer consultas em outras APIs, e gerar um resultado que será postado em outra API, na API originária, ou até mesmo em um tópico do barramento de mensagens.

Idealmente um worker deve gerar uma única saída (ex. POST) para cada evento de entrada. Essa regra só pode ser violada nos casos onde a API que receberá essas saídas múltiplas for idempotente. Caso contrário você pode ter problemas com duplicação de registros.

Deixe as decisões para quem detém mais contexto

Quando a gente aplica o princípio da Lazy Preoccupation é bastante comum perceber que quanto mais um fluxo de processamento se adentra pelo sistema, mais informação de contexto ele carrega.

Isso significa que quando você recebe uma entrada no sistema ela tem apenas os dados informados ali. Conforme esses dados vão passando por outros sistemas ele pode ser enriquecido com informações adicionais e essas informações adicionais são muito úteis para tomar decisões importantes no fluxo das suas regras de negócio.

Então evite ao máximo lidar com problemas complexos logo no início dos fluxos. Se tá difícil resolver um problema em um determinado ponto desse fluxo é melhor deixar ele adentrar um pouco mais (lazy preoccupation) para que ele absorva mais dados de contexto que podem ajudar nas suas decisões relacionadas às regras de negócio.

Interfaces e protocolos guiam implementações

A sigla API significa “Application Programming Interface” e a palavra importante aqui é “Interface” em contraponto à palavra “Implementation”. Alterar implementações é fácil. Alterar interfaces não.

Quando eu altero uma implementação difilcilmente eu quebro um sistema. Mas se alteramos uma interface é quase certo que teremos problemas.

Então, ao desenvolver um sistema novo, dedique bastante tempo nessa etapa. Técnicas como TDD ajudam você a transitar por esse estágio quando não conhecemos muito bem o domínio do problema que estamos lidando.

Comece a implementar o sistema somente depois que você estiver confortável com o desenho das interfaces e modelos expostos por ela.

Append-only for the rescue

Os sistemas modernos que operam em uma escala muito grande invariavelmente precisam ser implementados usando modelos distribuídos e, em sistemas distribuídos, você vai acabar tendo que lidar com locks e implementações de mutexes. Muitas vezes o seu banco de dados é quem vai ter que dar cabo dessas operações mas o fato é que os locks e mutexes estarão lá para ferir a performance do seu sistema.

Uma forma de diminuir (e até mesmo evitar) algumas dessas travas é substituir operações de update por operações de insert no seu banco de dados. Quando atualizamos um registro em uma tabela o banco de dados vai ‘travar’ esse registro para escrita bloqueando outras transações que queiram fazer o mesmo***. Isso pode penalizar severamente a performance do seu sistema. Para o banco de dados, fazer um insert, é muito mais simples e as ‘travas’ vão ser usadas somente para atualização de índices.

Mas isso tem um custo muito alto no processo de recuperar essas informações. No lugar de recuperar apenas um registro do banco de dados você precisa buscar todas as operações relacionadas àquele registro e consolidar elas para obter a informação necessária. Muito ineficiente.

Para amenizar essa ineficiência é possível utilizar o design pattern CQRS (Command and Query Responsibility Segregation) que segrega as responsabilidades de escrita e leitura em dois segmentos diferentes do seu banco de dados permitindo escritas rápidas que serão ‘projetadas’ assíncronamente em um registro consolidado para consulta futura.

Notas

* Hoje traduzem esse acrônimo como “Keep It Super Simple” por causa da carga ruim que a palavra “Stupid” da tradução original (Keep It Simple Stupid) carrega.

** Refactoring é aperfeiçoar um pedaço do seu código sem alterar seu comportamento. O que garante que esse comportamento não está sendo alterado é um conjunto de testes automatizados. Ou seja, se não tem teste automatizado ou se o comportamento do software muda não é um refactoring. Se acontecer de algum teste quebrar durante um refactoring é interessante tentar entender se isso acontece porque seu código está mal implementado (ex. testando mais a implementação do que o comportamento) ou se a mudança está realmente mudando o comportamento do código. Vamos usar as palavras corretas para refactoring e para reescrita.

*** Esse assunto é um tanto mais complexo que isso e recomendo a leitura do livro “Designing Data-Intensive Applications” (Martin Kleppmann) para mais detalhes sobre esse assunto.