A Arquitetura simples

a-arquitetura-simples

Disclaimer: Os exemplos usados aqui embora baseados em projetos reais sรฃo de minha autoria. Eu simplifico/altero alguns detalhes de forma a simplificar o post e preservar a IP das empresas. Esse post รฉ 100% baseado apenas em minhas opiniรตes e nรฃo reflete meus empregadores.

A ideia desse post surgiu de uma discussรฃo que eu tive com o @rponte. Com todos os debates recentes sobre “Arquitetura Limpa” e “Ports and Adapters” nรณs conversรกvamos como nos รบltimos 7 anos os projetos que eu passei foram estruturados em empresas como Amazon e Twitter.

A Arquitetura simples

Eu pensei nesse termo “Arquitetura Simples” como uma brincadeira com o termo arquitetura limpa. Esse estilo de arquitetura nรฃo รฉ proposital, ele nasce naturalmente a partir do momento que o time de desenvolvimento abraรงa os dois seguintes princรญpios:

  1. YAGNI – You aren’t gonna need it (Vocรช nรฃo vai precisar disso).
  2. KISS – Keep it Simple Stupid ou Keep it Super Simple (Mantenha simples estรบpido ou mantenha super simples).
    A ideia “central” รฉ que o time nรฃo vai gastar energia e esforรงo projetando sistemas tentando prever o futuro ou desacoplando camadas sem que os requisitos exijam isso.

Eu seria hipรณcrita em dizer que isso รฉ/foi uma escolha consciente. Nos รบltimos 8 anos que trabalhei em empresas como Amazon e Twitter nรณs nunca nos sentamos e decidimos explicitamente “essa serรก a arquitetura do nosso sistema”. Os sistemas simplesmente eram escritas e vinham “ร  tona” nessa forma simples. Talvez pela facilidade e naturalidade de como รฉ comeรงar um projeto dessa forma.

Tambรฉm รฉ importante mencionar que a forma como esses sistemas sรฃo escritos nรฃo รฉ a forma perfeita para todas as aplicaรงรตes e tรฃo pouco eu acho que essa seja a soluรงรฃo para todos os problemas. “There’s no silver bullet” (Nรฃo hรก bala de prata) รฉ um outro princรญpio que eu acredito e que eu acho que deve ser sempre ponderado por todos os times nos mais diversos projetos.

As “camadas” das “Arquitetura Simples”

As camadas da nossa arquitetura sรฃo o mais simples possรญvel (com o perdรฃo do trocadilho) e eu sei que isso pode incomodar muita gente. Normalmente as dependรชncias sรฃo diretas, por exemplo, um controller depende diretamente da classe que implementa o caso de uso daquele controller, ou o caso de uso depende diretamente da classe de persistรชncia que salva os dados ou recupera os dados que ele precisa.
Os componentes podem ser observados no diagrama abaixo:

Image description

Controlers

Um controller por API, aqui a gente recebe um RequestDTO que representa a requisiรงรฃo de uma API RPC, fazemos todas as validaรงรตes de input e outras coisas como logging.
Depois existem diversas opรงรตes aqui que variam de projeto pra projeto:

  1. Passamos o RequestDTO diretamente para o objeto de caso de uso. Essa opรงรฃo favorece simplicidade (KISS e YAGNI), porรฉm acopla teu objeto de caso de uso com modificaรงรตes no modelo da API. Muitas vezes, isso estรก totalmente ok.
  2. Esse RequestDTO รฉ traduzido pra uma entidade de negรณcio do caso de uso se as entidades forem extremamente anรชmicas. Essa opรงรฃo favorece simplicidade porรฉm em muitos casos a entidade vai ser criada incompleta e vai ser um objeto mutรกvel que รฉ preenchido com mais informaรงรตes na camada de negรณcio.
  3. Traduzimos o RequestDTO para algum outro DTO que รฉ usado exclusivamente como input da API de caso de uso. Esse รฉ o modelo mais flexรญvel que desacopla negรณcios da API porรฉm รฉ mais verboso e exige transferir os dados de um DTO pra outro. A flexibilidade ajuda se o caso de uso utilizar inputs diferentes, por exemplo, uma API sรญncrona e um worker assรญncrono.

Por que? ร‰ comum o controller fazer vรกrias regras referentes a obtenรงรฃo de dados, logging e outras validaรงรตes. Misturar isso com a coordenaรงรฃo entre diversos outros objetos como invocar serviรงos externos e salvar no banco de dados deixa o controller grande demais. No geral, nรณs quebramos o controller pra:

  • Reduzir o tamanho e complexidade da classe.
  • Facilitar a segregaรงรฃo de responsabilidades e consequentemente os testes de unidade.
    Sobre traduzir o RequestDTO para um outro modelo interno, รฉ comum a API e consequentemente seu DTO evoluir a passos diferentes do modelo interno. Em 100% dos projetos que participei, toda vez que tentamos utilizar um รบnico modelo interno e pra request a coisa desandou e depois tรญnhamos que fazer um refactoring pra desacoplar os modelos um do outro. Hoje em dia, eu jรก insisto em comeรงarmos com modelos separados pra evitar essa dor.

Casos de Uso/Entidades

As classes dessa camada normalmente caem em 2 grupos:

  • Classes de caso de uso: Essas classes implementam as regras de negรณcio da aplicaรงรฃo colando entidades, persistรชncia e dependรชncias. Aqui o modelo รฉ bem flexรญvel e varia de aplicaรงรฃo pra aplicaรงรฃo. No geral, se tem uma classe de caso de uso por caso de uso do usuรกrio mas diversos modelos e entidades de suporte que ajudam a extrair responsabilidades especรญficas em classes menores.
  • Entidades: Sรฃo as entidades do nosso sistema. Elas podem ser anรชmicas sendo apenas simples estrutura de dados ou agrupar comportamento que alteram o valor do objeto. As entidades tambรฉm nos ajudam a encapsular mรณdulos e comportamentos que sรฃo comuns ao resto da aplicaรงรฃo.

Por que? Uma coisa que eu aprendi ao longo dos anos รฉ ter muito cuidado em reusar essas classes de caso de uso para mรบltiplas APIs. No inรญcio essa ideia parece funcionar mas com o tempo as regras de negรณcio vรฃo mudando e as classes de caso de uso vรฃo ficando cheias de regras especiais para diferentes APIs. Um bom exemplo รฉ quando chamamos essas classes de “Manager”, por exemplo, ProductManager e aรญ vocรช implementa create/update/delete/read na mesma classe sรณ pra reusar algumas funรงรตes em comum. Agora imagine que o caso de uso de update tem 3 dependรชncias, o de delete tem 2 e o create tem outras 3. Mesmo que haja sinergia entre elas, as vezes as dependรชncias vรฃo ser diferentes e no pior dos casos a sua classe pode acabar com 8 dependรชncias diferentes. Nesse caso, talvez seja melhor quebrar em classes de caso de uso diferentes e abstrair os comportamentos comuns que sรฃo independentes de caso de uso nas entidades de domรญnio.

Ainda nessa camada a maior polรชmica รฉ, devo anotar nossas entidades com anotaรงรตes de persistรชncia ORM? Na maioria dos projetos que eu entrei entidades de persistรชncia e entidades de domรญnio sรฃo mantidas separadamente e sempre temos que converter de uma pra outra. Porรฉm, olhando pra trรกs eu me pergunto se essa separaรงรฃo รฉ realmente necessรกria. Novamente, nessa mesma maioria dos projetos, sempre houve um mapeamento 1:1 de um campo de domรญnio pra um campo na persistรชncia. Essa separaรงรฃo sรณ nos causa mais dor na hora de implementar algo novo e definitivamente quebra o nosso princรญpio KISS e YAGNI. Se eu fosse comeรงar um projeto novo, provavelmente eu comeรงaria com o domรญnio mapeado com o framework de ORM. Se as coisas comeรงarem a divergir entรฃo eu faria uma task para separรก-los.

Dependรชncias externas

Para cada serviรงo externo, onde externo significa uma chamada de rede, nรณs criamos uma classe. Essa classe funciona como uma Facade e Anti-Corruption Layer entre o nosso domรญnio e as dependรชncias externas. A API com essa classe normalmente se dรก em tipos simples ou atravรฉs das nossas prรณprias entidades.

Por que? Aqui queremos isolar as dependรชncias daquele sistema externo em um รบnico ponto. Logo se a API mudar, for deprecated ou algo especรญfico daquele sistema acontecer, apenas 1 classe do nosso cรณdigo รฉ afetada.

Camada de Persistรชncia

Similar a camada que interage com os serviรงos externos. Nรณs temos uma camada de persistรชncia que contรฉm a classe que faz a chamada ao banco de dados e as entidades de persistรชncia que sรฃo mapeadas por algum framework ORM (se vocรช jรก nao tiver feito isso no domรญnio).

Por que? Nรณs tentamos manter a persistรชncia tรฃo simples quanto possรญvel. Inclusive, hoje em dia รฉ possรญvel implementar a maioria dessas operaรงรตes utilizando frameworks, como por exemplo, o Spring DATA.

Configuraรงรตes e preocupaรงรตes transversais

Colando todas essas camadas nรณs temos a camada de configuraรงรฃo. Normalmente, essa camada รฉ representada pelo framework de injeรงรฃo de dependรชncias de sua escolha que cria todos os objetos e faz o wiring para que eles funcionem juntos.

ร‰ nessa camada tambรฉm que carregamos as variรกveis de ambiente que variam por regiรฃo(US, JP, BR, etc…) e/ou stage (dev, pre-prod e prod).

Outra configuraรงรฃo que eu colocaria nessa camada sรฃo preocupaรงรตes transversais. Classes que interceptam algum comportamento entre camadas, por exemplo. Esses interceptadores sรฃo bem comuns na camada de Controller, por exemplo. Eles podem realizar funรงรตes como validaรงรฃo de dados, autenticaรงรฃo, logging de chamadas, etc… tudo isso feito ANTES da chamada ser tratada pelo controller. Como essas funรงรตes sรฃo bem genรฉricas, รฉ fรกcil isolar elas e acopla-las com todos os controllers da aplicaรงรฃo.

Por que? Por convenรงรฃo. A maioria dos locais que eu trabalhei tem a convenรงรฃo de nรฃo colocar muitas annotations no cรณdigo da camada de casos de uso. Alรฉm disso, sobre as config classes, รฉ que muitos dos nossos objetos sรฃo criados com algumas lรณgicas interessantes. Logo usar config classes em vez de annotations acaba facilitando por nos dar mais controle e flexibilidade.

Eu jรก passei por alguns exemplos bem especรญficos onde as anotaรงรตes atrapalharam mas dito isso, pros projetos que nรณs tรญnhamos nรฃo hรก muita diferenรงa entre usar as anotaรงรตes ou nรฃo, logo, se estivesse comeรงando um projeto novo eu faria o que รฉ mais simples e segue as convenรงรตes da empresa.

Estudo de caso 1: ReviewsService

Para ilustrar a “arquitetura” que eu mencionei acima, vamos imaginar como seria o sistema de submissรฃo de avaliaรงรตes (reviews) de produto de um grande e-commerce.

Disclaimer: Qualquer semelhanรงa com a realidade รฉ mera coincidรชncia. ๐Ÿคญ

Imagine que nรณs temos que implementar o caso de uso: CreateReview. Depois de esboรงamos o system design e discutimos com o time nรณs chegamos ao seguinte fluxo:

Image description

  1. Usuรกrio invoca o serviรงo para criar uma nova review.
  2. reviewsService invoca um serviรงo externo para obter detalhes adicionais da review. Por exemplo, apelido do usuรกrio.
  3. reviewService salva a review no banco de dados.
  4. Banco de dados notifica um serviรงo de moderaรงรฃo com uma nova review criada.
  5. Serviรงo de moderaรงรฃo retorna o resultado da avaliaรงรฃo para o reviewsService.
  6. ReviewsService faz update no banco de dados com o resultado da avaliaรงรฃo.

Agora na hora da implementaรงรฃo nรณs imaginamos as seguintes classes/camadas:

Image description

Controllers

Nรณs temos 2 controllers:

  1. CreateReview que implementa nossa API. Esse controller recebe a requisiรงรฃo, faz todas as validaรงรตes de entrada necessรกrias e transforma o RequestDTO em CreateReviewDTO que รฉ enviado para a nossa classe de caso de uso. Essa transformaรงรฃo รฉ opcional e nem todo projeto faz isso, o porquรช eu fiz isso aqui? Pra manter consistรชncia com o UpdateReview controller/caso de uso. Mais detalhes no passo 8.
  2. UpdateReview controller que implementa uma outra API. Eu coloquei essa API aqui pra dar uma visรฃo pra um caso de uso que รฉ utilizado por duas entradas diferentes. Mais detalhes no passo 8 abaixo.

Casos de Uso/Entidades

  1. CreateReviewUseCase cola a nossa regra de negรณcio. Essa classe chama um serviรงo externo atravรฉs do Adapter DetalheReviewsAdapter. Com a informaรงรฃo do DTO e do retorno do Adapter, uma nova review รฉ criada.
  2. Review e outras entidades de domรญnio (nรฃo representadas no diagrama). Essas entidades estรฃo anotadas com nosso framework de ORM para simplificaรงรฃo.

Persistรชncia

  1. ReviewDAO persiste a nossa entidade Review no banco de dados. Essa classe รฉ fortemente acoplada ao nosso BD de escolha, nesse caso, o AWS DynamoDB

Esses dois componentes nรฃo sรฃo da camada de persistรชncia, mas eu vou deixar a explicaรงรฃo aqui por curiosidade/completude:

  1. O DynamoDB atravรฉs da funcionalidade de streams (post em breve, prometo ๐Ÿ˜Š) envia uma mensagem sempre que uma nova review รฉ criada. A mensagem contรฉm as informaรงรตes que foram criadas na review e รฉ enviada para uma fila no AWS SQS (outro post futuro).
  2. A fila do SQS รฉ consumida por um serviรงo de moderaรงรฃo de outro time. (Eu estou simplificando bastante aqui, dificilmente nรณs irรญamos expor uma fila SQS, isso tem mais cara de um tรณpico SNS com mรบltiplos subscribers).

Adapters/Integraรงรฃo

  1. Depois de moderada, a nossa review รฉ aprovada ou rejeitada. Um pequeno Worker implementado por uma classe chamada ModerationListener fica ouvindo por mensagens enviadas pelo sistema de moderaรงรฃo. O ModerationListener recebe a mensagem e transforma em um UpdateReviewDTO necessรกrio para chamar a nossa classe de UpdateReviewUseCase.

Percebeu agora porque eu decidi criar um DTO de entrada por caso de uso? Como meu requisito contรฉm mรบltiplas entradas no meu sistema eu nรฃo quis fazer o Listener depender de uma classe da camada de APIs (o RequestDTO).

Note que a soluรงรฃo aqui seguiu uma estratรฉgia “clรกssica”. Em um ambiente de cloud poderรญamos ter resolvido esse problema com um microserviรงo que roda separadamente dentro de um AWS Lambda.

Simplificaรงรตes e Trade-offs nesse caso de uso

Como escrever um sistema que escala para milhares de requisiรงรตes por segundo รฉ muito complexo, vรกrias simplificaรงรตes foram feitas:

  1. Vรกrias das chamadas que fazemos poderiam ser Decorators assรญncronos que reagem a um primeiro update no banco de dados. Algo mais event-driven para garantir mais escalabilidade. Um bom exemplo รฉ o DetalhesReviewAdapter que poderia ser, por exemplo, um decorator que adiciona informaรงรตes depois que a review รฉ escrita no nosso banco de dados e nos desacoplaria de esperar uma chamada sรญncrona ser feito a um serviรงo externo.
  2. Eu nรฃo adicionei em nenhum caso de uso outras checagens como Idempotรชncia. Normalmente vocรช faz isso assim que possรญvel para evitar chamadas a outras dependรชncias desnecessariamente.
  3. Toda a moderaรงรฃo, como isso afeta a review e como lidar com escritas concorrentes tambรฉm nรฃo foram lidadas aqui.
  4. Nรฃo estou usando outros padrรตes como “event source”. Estou considerando que os dados vรฃo ser stateful em vez de guardar as aรงรตes e dali tirar um snapshot do review resultante (Se estiver curiosa(o) dรช uma olhada no padrรฃo de Event Sourcing).

Jรก em termos de trade-offs, notem o seguinte:

  1. Percebem que nossas entidades de domรญnio estรฃo acopladas ao banco de dados. Por quรช? Na maioria dos projetos, o mapeamento รฉ quase 1-1. Se os dois comeรงassem a divergir, eu faria o seguinte refactoring.
    1. Criar um novo DTO especรญfico para a persistรชncia.
    2. Manter a entidade de domรญnio como estรก e copiar o cรณdigo para o novo DTO.
    3. Atualizar as classes de persistรชncia para usar o novo DTO.
  2. Eu adicionei uma complexidade extra com a adiรงรฃo de DTOs por caso de uso. As vezes, isso nem รฉ necessรกrio e vocรช pode simplesmente passar o DTO do controller direto.
  3. O nosso listener chama o caso de uso mas muitas vezes, nรณs queremos que todas as chamadas, mesmo quando feitas dentro do mesmo sistema passem pela API. Nesse caso, o nosso ModerationListener invocaria a API nรฃo tendo visibilidade ao caso de uso ou banco de dados. Isso protege os dados e nos dรก mais seguranรงa forรงando todo mundo a passar pela API.

As complexidades estรฃo nas “bordas”

Vocรช pode estar pensando. “Essa “arquitetura” รฉ simplรณria demais” ou “Mas e os testes?”. Pois bem, as soluรงรตes escritas nesse “padrรฃo” sรฃo todas bem testadas, tanto do ponto de vista de unidade quanto de end-to-end. Na unidade, com frameworks modernos como Mockito (Nem tรฃo moderno assim), nรณs conseguimos fazer mocks de classes concretas facilmente. Tambรฉm รฉ possรญvel criar fakes atravรฉs de heranรงa sobrescrevendo os mรฉtodos pรบblicos e dando o comportamento que desejamos.

Com relaรงรฃo a simplicidade, a ideia รฉ que seja simples mesmo. A maioria dos problemas que enfrentei nesses รบltimos 8 anos de carreira nรฃo foi relacionado a como implementar uma regra de negรณcio ou classe X estar fortemente acoplada a classe Y. Os problemas que enfrentei foram mais do tipo:

  • Serviรงo externo A retorna erro Y quando deveria retornar Z.
  • Execuรงรตes concorrentes afetando os dados
  • Infra nรฃo escalando ou problema especรญfico da infra.
  • Interaรงรฃo e coordenaรงรฃo entre 3 ou mais sistemas sem uso de transaรงรตes.
  • Mensagens duplicadas nas filas ou falta de retries.

Percebeu como um domรญnio desacoplado dificilmente nos ajudaria com os problemas acima? A maioria dos problemas grandes que enfrentamos se deram por decorrรชncia de erros no System Design ou de comportamentos inesperados entre sistemas, afinal de contas, sistemas distribuรญdos sรฃo estranhos.

Conclusรฃo

Nesse artigo eu apresentei uma proposta que vai de encontro a outros estilos de arquitetura como “Ports and Adapters” ou a “arquitetura limpa”. Nรฃo me entendam mal, eu nรฃo sou 100% contra esses modelos, e hรก exemplos de sucesso na indรบstria com o uso arquitetura hexagonal e/ou clean. Porรฉm tais necessidades devem emergir dos seus requisitos.

Vejam o caso do Netflix. Ali eles tinham a necessidade de conseguir trocar entre datasources de forma rรกpida e o input/output deles obedecia uma certa forma homogรชnea que o estilo da Arquitetura Hexagonal podia suprir.

Finalmente, esse artigo nรฃo รฉ uma receita do que vocรช deve fazer. Eu apenas decidi a minha experiรชncia e demonstrar que tem muito software sendo escrito, entregue e rodando com sucesso mundo afora que nรฃo segue os estilos de “arquitetura” que vemos nos livros. Esses sistemas sรฃo bem testados e rodam todo dia para milhรตes de usuรกrio simultรขneos.

Espero que vocรชs tenham gostado do artigo e que isso te faรงa pensar se vocรช realmente precisa de tantas classes na hora de escrever seu sistema. Se tiverem alguma dรบvida, nรฃo deixem de perguntar nos comentรกrios ou no twitter.

Total
6
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post
github-copilot-trial-ends,-now-what?

Github Copilot trial ends, now what?

Next Post
how-to-create-a-test-database-with-laravel-sail

How to create a test database with Laravel Sail

Related Posts