Alura > Cursos de Programação > Cursos de Node.JS > Conteúdos de Node.JS > Primeiras aulas do curso Node.js: arquitetura de monolito modular

Node.js: arquitetura de monolito modular

Desvendando o monolito de lama - Apresentação

Apresentando o instrutor e o curso

Seja muito bem-vindo ou bem-vinda ao curso de Monolitos Modulares. Meu nome é Thiago Martins e serei o instrutor deste curso. Sou uma pessoa desenvolvedora back-end especialista em Node.js e Nest.js.

Audiodescrição: Thiago é um homem branco com cabelo castanho curto e barba curta. Ele está usando óculos e uma camisa verde clara. Atrás dele, há uma estante de livros e duas poltronas.

Introduzindo o conteúdo do curso

Este curso é avançado, destinado àquelas pessoas que desejam aprender a desenhar e arquitetar sistemas modulares. Ou seja, sistemas que possuem baixo acoplamento e resistem ao teste do tempo. Em outras palavras, a manutenção desses sistemas ao longo do tempo permanece mais ou menos constante.

O que vamos aprender no curso? Primeiramente, discutiremos o que são monolitos e quais são os desafios da manutenção de aplicações construídas dessa forma. Também aprenderemos a identificar o antepadrão chamado Big Ball of Mud (Grande Bola de Lama), compreendendo do que se trata. Simularemos o trabalho em sistemas legados para refletir um ambiente de trabalho real, como em empresas de verdade, onde normalmente lidamos com sistemas já em funcionamento e legados.

Explorando técnicas e padrões de design

Além disso, aprenderemos a identificar os domínios existentes nesses sistemas e como podemos extrair subdomínios.

Vamos utilizar o Strangler Fig Pattern (Padrão da Figueira Estranguladora) para aprender a migrar partes do sistema de maneira segura. Em seguida, aplicaremos essas extrações e implementaremos módulos utilizando o NestJS, realizando o que chamamos de Vertical Slices (Fatias Verticais).

Discutindo comunicação e evolução de sistemas

Por fim, discutiremos a comunicação síncrona e assíncrona, ou seja, em memória e em fila entre os módulos. Aplicaremos alguns conceitos de Domain Driven Design (Design Orientado a Domínio) na prática, como entidades e objetos de valor. Finalmente, prepararemos e discutiremos a evolução de um monólito para microserviços.

Estabelecendo pré-requisitos e motivação

Quais são os pré-requisitos para este curso? É extremamente importante ter algum conhecimento em NodeJS e NestJS, que são as ferramentas que utilizaremos, além de familiaridade com Docker e Docker Compose, que serão usados para levantar nossa infraestrutura local. Também utilizaremos o Postgres como banco de dados, então é altamente recomendado ter conhecimento sobre ele, assim como os conceitos de Domain Driven Design, tanto os estratégicos quanto os táticos.

Esperamos que todos estejam tão animados quanto nós para começar este curso. Será um curso prático, avançado e desafiador, mas valerá a pena. Vamos realizar isso juntos? Nos vemos no próximo vídeo.

Desvendando o monolito de lama - Entendendo o problema do software legado

Introduzindo sistemas legados

Se somos pessoas desenvolvedoras que já atuam na área há algum tempo, ou estamos prestes a começar, provavelmente já tivemos que lidar, ou iremos lidar, com sistemas legados. Muitas pessoas acreditam que um sistema legado é necessariamente ruim, mas isso não é verdade. Um sistema legado é, basicamente, um sistema que herdamos, construído por outras pessoas desenvolvedoras. Seja porque mudamos de empresa ou porque, dentro da mesma empresa, mudamos de equipe, precisamos lidar com um sistema que já existia.

A principal característica de um sistema legado é que ele já estava em produção, ou seja, já entregava valor para alguém e possuía usuários, estando, portanto, consolidado. Assim como uma casa antiga, um sistema legado foi construído em uma época diferente, atendendo a necessidades distintas. Com o tempo, ele pode apresentar problemas, assim como uma casa que começa a ter problemas no telhado, vazamentos, pisos quebrados ou problemas na fiação. O sistema legado, geralmente, apresenta problemas em sua arquitetura. Às vezes, devido à necessidade de atingir metas rapidamente, o desenvolvimento foi feito de maneira não muito controlada, resultando em problemas.

Explorando o custo de mudança em sistemas legados

Esse é um fenômeno que observamos ao longo de nossas carreiras de maneira pragmática, mas que já é estudado teoricamente desde 1981. Barry Bowen publicou um artigo na revista Software Engineering Economics, discutindo o custo de mudança de software ao longo do tempo. Ou seja, quanto mais o tempo passa, mais caro se torna fazer modificações no sistema, caso encontremos problemas ou os requisitos mudem, ou ainda, caso descubramos uma nova necessidade do usuário final.

Uma solução que frequentemente consideramos é reescrever o sistema legado em uma linguagem nova, como o Rust, que está em alta atualmente. Acreditamos que isso resolverá os problemas e eliminará decisões ruins feitas no sistema antigo. No entanto, na maioria das vezes, um sistema legado não se torna difícil de gerir por um problema técnico ou pela linguagem em que foi desenvolvido. Mesmo que comecemos a construir um sistema em Rust, inicialmente ele será simples, com poucas rotas e serviços. Podemos até achar que está funcionando bem, mas, com o tempo, pelos mesmos motivos que exploraremos nas próximas aulas, a complexidade aumenta exponencialmente e o custo de mudança para esse novo sistema se torna praticamente o mesmo que o do sistema antigo.

Lidando com a complexidade crescente e o acoplamento

Quando tínhamos poucos serviços, entidades e rotas, se fosse um sistema web, ele começa a crescer, proliferando o número de relações e comportamentos. Isso pode levar a situações em que um serviço ou entidade específica do sistema é utilizado por diferentes departamentos da empresa, ou seja, por diferentes usuários. Esses usuários são atores distintos que representam departamentos da empresa utilizando, talvez, um serviço específico.

Um problema comum que surge é quando fazemos uma mudança em uma entidade ou algo no sistema para atender a demanda de um ator, como um cliente final, mas acabamos afetando outro usuário do sistema, pois eles compartilham uma lógica emaranhada. Um exemplo prático é quando temos, no banco de dados, uma parte do sistema para fazer o onboarding de um usuário, ou seja, o cadastro. No banco, o campo "telefone" de uma pessoa que está se cadastrando é obrigatório. No entanto, mais adiante, descobrimos que usuários administradores gostariam de adicionar outros administradores sem precisar do telefone, apenas do e-mail. Como desenvolvemos o sistema ao redor de uma tabela e modelagem de usuário, acabamos com uma modelagem contraditória: para clientes, o telefone é obrigatório, mas, para administradores, é opcional.

Reconhecendo e abordando o Big Ball of Mud

Quando começamos a entrar em conflito e fazemos mudanças para atender a uma demanda, podemos acabar afetando outras partes e atores do sistema sem perceber. Isso é frequentemente chamado de acoplamento. Esse fenômeno é muito comum. À medida que o sistema cresce, se não dermos a devida manutenção e aplicarmos algumas técnicas que discutiremos aqui, começamos a ter um aumento significativo de acoplamento invisível. Isso ocorre quando modificamos uma parte do sistema e ela afeta outras partes que nem sabíamos que estavam relacionadas, por estarem conectadas de maneira indireta, mas compartilhadas.

Chamamos essa desordem de software ou sistema de Big Ball of Mud (grande bola de lama), um termo muito utilizado em livros sobre Domain Driven Design (Design Orientado a Domínio). O próprio Von Vernon, autor do livro Implementing Domain Driven Design (Implementando Design Orientado a Domínio), utiliza esse termo. Se já temos experiência no mercado, provavelmente já lidamos com isso.

Desmistificando soluções técnicas para sistemas emaranhados

Como reconhecemos uma grande bola de lama? É um sistema onde há falta de arquitetura, é difícil entender a finalidade de cada componente, e não conseguimos ver uma estrutura bem definida. Para adicionar, por exemplo, uma simples rota em um sistema web, ficamos perdidos, sem saber por onde começar. Muitas vezes, pessoas desenvolvedoras, principalmente as mais juniores, e até mesmo algumas mais experientes, acreditam que para resolver esse tipo de problema precisamos de soluções explicitamente técnicas, como Design Patterns (Padrões de Projeto).

Design Patterns são padrões de modelagem de partes do sistema, frequentemente utilizados com classes, mas também existem padrões funcionais que utilizam funções. As pessoas desenvolvedoras acreditam que com uma modelagem técnica, utilizando padrões de projeto, resolverão o problema do sistema estar emaranhado e acoplado. Inicialmente, isso pode até funcionar, criando interfaces e abstraindo partes do sistema, mas, ao longo do tempo, surge outro problema chamado de indireção.

Avaliando a proliferação de interfaces e a indireção

Para criar apenas uma nova rota, por exemplo, em um sistema web, que pode ser uma rota simples para retornar dados de um usuário, é necessário criar vários arquivos. Talvez já tenhamos passado por isso em nossa carreira. Um exemplo disso é quando temos um controller que recebe a requisição HTTP e depende de uma interface, como iService, que é implementada por iUserService. Esse serviço depende de outras duas interfaces, iRepository e AbstractFactory, cada uma com sua implementação, e essas implementações dependem de outras interfaces. Isso leva à proliferação de interfaces no sistema que não abstraem informações ou comportamentos úteis, apenas repassam chamadas sem prover valor.

É interessante considerar, como recomendado por Vladimir Korikov, autor do livro Unit Testing Principles, Practices and Patterns (Princípios, Práticas e Padrões de Testes Unitários), que para criar uma interface, o ideal é ter pelo menos duas implementações para justificá-la. Uma interface é uma abstração de algo que pode ser feito de mais de uma maneira ou que queremos esconder parte dos detalhes de implementação.

Questionando a eficácia de padrões de projeto

Voltando ao ponto, muitas vezes as pessoas desenvolvedoras acreditam que para resolver o problema do Big Ball of Mud, basta aplicar padrões de projeto ou arquiteturas específicas. No entanto, isso nos leva ao mesmo problema: um sistema acoplado, com comportamentos ou entidades compartilhadas por diferentes departamentos, e uma série de interfaces e abstrações que tornam até tarefas simples mais difíceis.

A pergunta que deixamos para responder no próximo vídeo é: será que é possível, de alguma maneira, reduzir essa curva de custo de mudança ao longo do tempo, para que ela não se torne exponencial, mas que possamos achatá-la para que se mantenha quase uniforme, como a curva azul que vemos aqui? Parece que todo software está fadado a se tornar impossível de manter ao longo do tempo, mas não é esse o caso. Veremos isso no próximo vídeo.

Desvendando o monolito de lama - A comunicação e a modelagem do sistema

Discutindo sistemas legados e suas dificuldades

No último vídeo, discutimos quatro pontos principais relacionados aos sistemas legados. Abordamos o que são esses sistemas, que são basicamente aqueles que já estão em produção há algum tempo e servem a alguns usuários. Não necessariamente são difíceis de modificar, mas, ao longo do tempo, se não forem cuidados adequadamente, podem se tornar de difícil manutenção. Analisamos a teoria por trás disso, que é a curva de custo versus tempo, a qual indica que, com o passar do tempo, o custo para modificar um software tende a crescer exponencialmente. Discutimos algumas das possíveis causas para isso.

Uma solução que muitos desenvolvedores acreditam ser eficaz é a reescrita do sistema. No entanto, vimos que isso é um mito. Quando tentamos reescrever o sistema do zero, acreditando que uma nova linguagem, framework ou biblioteca resolverá o problema, acabamos com dois sistemas problemáticos. O sistema novo, apesar de inicialmente parecer pequeno e de fácil manutenção, se não forem tomados os mesmos cuidados e se a raiz do problema não for resolvida, acabará se tornando novamente um "Big Ball of Mud" (grande bola de lama), um sistema totalmente acoplado e de difícil manutenção, onde mudanças em um local afetam outros de maneira indiscriminada.

Explorando a possibilidade de achatar a curva de custo

Neste vídeo, responderemos à pergunta feita anteriormente: é possível achatar essa curva para que ela não seja exponencial e se mantenha mais constante ao longo do tempo? Para responder a essa pergunta, precisamos entender que o problema se divide em duas partes principais: problemas de comunicação e a modelagem do próprio sistema.

No que diz respeito à comunicação, enfrentamos um dilema. A comunicação ocorre quando uma pessoa, como Maria, que possui conhecimento sobre um assunto, tenta transferir esse conhecimento para outra pessoa, como João. O conhecimento de Maria é influenciado por seu contexto, educação e experiência. Para transferir esse conhecimento, Maria precisa codificá-lo, transformando algo abstrato em concreto, como imagem, texto ou fala, para externalizá-lo. João, ao receber essa informação, precisa decodificá-la e interpretá-la. O problema é que, muitas vezes, o que Maria quer dizer não chega exatamente como estava em sua mente para João, devido a diferenças de interpretação. Isso ocorre porque não temos uma maneira de transferir conhecimento de forma totalmente fidedigna entre cérebros, já que o conhecimento de João também é influenciado por seu contexto, experiência e educação.

Implementando ciclos de feedback para melhorar a comunicação

Para resolver esse problema de comunicação, é necessário um ciclo de feedback. João precisa comunicar a Maria o que entendeu, para que ela confirme se o entendimento está correto. Esse ciclo de feedback é essencial para corrigir informações deturpadas.

Agora, consideremos uma organização com comunicação hierárquica. Nesse caso, a transferência de informação passa por diversos níveis até chegar ao ponto final. Por exemplo, o usuário do sistema, que é um dos stakeholders, pode ter uma necessidade ou sugestão. Em organizações hierárquicas, essa informação passa por várias cadeias, como gerente, product owner e, finalmente, o desenvolvedor. A cada passagem, o dilema da comunicação se exacerba, resultando em uma "comunicação de telefone sem fio", onde parte da informação original se perde. Diferentes departamentos interpretam funcionalidades de acordo com suas próprias necessidades, o que agrava o problema.

Promovendo a lateralização na comunicação organizacional

Para resolver essa questão, é necessário implementar a lateralização, que consiste em diminuir o número de hierarquias sempre que possível. Em vez de passar por várias etapas, o ideal é que o desenvolvedor esteja em contato direto com os usuários finais. Isso pode ser aplicado na prática em empresas, onde, ao invés de se comunicar com gerentes que repassam informações, o desenvolvedor interage diretamente com o cliente final. Essa abordagem pode ser benéfica para equipes técnicas e líderes de times.

Já trabalhamos em funcionalidades ou na correção de problemas em sistemas nos quais, após a implementação e entrega, não soubemos, por muito tempo ou nunca, qual foi o resultado final daquela implementação. Não sabemos como o usuário final foi impactado, se o problema foi realmente resolvido. Em muitas empresas, as pessoas desenvolvedoras apenas resolvem o problema, implementam algo, mas não têm conhecimento de como o usuário final reagiu àquela modificação. Quanto mais conseguimos deixar os dois em contato direto, menos camadas de tradução e menos perda de informação teremos. Isso é crucial.

Abordando a modelagem técnica e suas implicações

Essa é a primeira parte do problema que, normalmente, leva a um big ball of mud (grande bola de lama). A segunda parte, que discutiremos agora, trata da modelagem técnica ou transacional. O que é isso? A modelagem técnica ou estritamente técnica ocorre quando a equipe de software, a equipe de desenvolvimento, modela o sistema e se comunica entre si, tanto por escrito quanto verbalmente, de maneira muito técnica, inclusive com o usuário final ou com quem está trazendo os requisitos do sistema. Um sintoma desse tipo de problema é quando a modelagem do sistema e sua arquitetura estão muito focadas em funções técnicas. Por exemplo, o usuário tem um gateway, um repositório, uma tabela X, e permissões. Às vezes, há algumas informações que remetem à linguagem do domínio, como endereço ou permissão, mas, na maior parte do tempo, a equipe se comunica de maneira muito técnica, não só entre si, mas também com outras equipes.

Isso é problemático porque começa a divergir quando falamos de termos muito técnicos. Surge mais uma camada de tradução para a linguagem que o usuário final do sistema utiliza, que geralmente não é técnica. Isso se reflete até na estrutura do sistema, como na estrutura de pastas. Se o sistema está dividido por funções, como Controllers, Services, Database, Gateways, DTOs, etc., e há uma série de arquivos dentro de cada uma dessas pastas, não há distinção das capacidades de negócio, dos subdomínios do problema. O sistema é arquitetado puramente de uma perspectiva técnica e funcional, focando na função de cada artefato.

Utilizando a linguagem do domínio para melhorar a modelagem

Para começar a corrigir esse problema, é necessário modelar o sistema e se comunicar utilizando a linguagem do domínio. Em vez de falar sobre termos técnicos, especialmente com outros times, devemos usar os termos que os usuários finais utilizam, os que fazem sentido no domínio em que estamos trabalhando. Por exemplo, o usuário possui uma conta, realiza uma compra, possui um risco associado, reside em um endereço. A mudança necessária é na comunicação, uma mudança mais psicológica. Uma solução estritamente técnica muitas vezes não resolve o problema, pois ele não é técnico. O problema está na maneira de pensar e modelar o sistema, que se afasta da realidade do negócio e de como o usuário final entende e utiliza o sistema.

A primeira parte é falar a linguagem do domínio. A segunda parte é entender as capacidades distintas do negócio, ou seja, os departamentos existentes na empresa, organização ou sistema. Devemos alocar as entidades, comportamentos e artefatos de software nesses diferentes subdomínios. Por exemplo, quando falamos de conta, estamos no contexto de pagamentos; endereço, no contexto de logística; vendas, no contexto de compra; compliance, no contexto de risco. Ao segregar e entender essas modalidades do negócio, não separamos mais o sistema por função, mas por modalidades do negócio em que cada artefato está alocado.

Refletindo a arquitetura do sistema nos domínios de negócio

Antes, com uma modelagem muito técnica, as entidades e comportamentos eram representados por uma única coisa. Por exemplo, um usuário que possuía todos os comportamentos do sistema. Ao modelar tudo através de um usuário, percebemos que, dependendo do contexto, ele pode ter nomes, comportamentos e atributos diferentes. No contexto de vendas, chamamos de cliente; em logística, de destinatário; em identidade, de usuário.

Quando isso se reflete na arquitetura do sistema, a mudança é significativa. Antes, com uma divisão funcional, agora temos uma divisão que reflete os domínios. A divisão de pastas e módulos está de acordo com as capacidades ou departamentos do sistema. Não falamos mais de controller, service, banco de dados, mas de departamentos como compliance, pagamentos, vendas, inventário, logística. A mudança é sutil, mas tem um grande impacto.

Transformando sistemas em monolitos modulares

O Big Ball of Mud, um sistema todo acoplado com informações compartilhadas em diversas partes, começa a se dividir e focar na modelagem do negócio. Cada parte do sistema fica em seu próprio contexto, em seu próprio casulo. O contexto de vendas, administração, suporte, por exemplo, cresce de maneira vertical, não mais horizontal ou desorganizada. Isso é chamado de arquitetura vertical, de camadas verticais. Cada contexto é responsável por definir suas entidades, necessidades e comunicação com serviços ou sistemas externos, tornando-se mais autocontido e menos dependente dos outros.

Na prática, esses contextos se traduzem em módulos, formando o que chamamos de monolitos modulares. Essa divisão em arquitetura vertical, onde cada parte do sistema é autocontida e a complexidade cresce verticalmente, compreendendo uma capacidade de negócio distinta, é o que caracteriza um monolito modular. No próximo vídeo, veremos como fazer isso na prática, transformando um sistema Big Ball of Mud, cheio de artefatos, serviços e classes compartilhadas, em um monolito com módulos bem definidos e independentes, limitando o impacto de alterações em um módulo sobre os outros.

Sobre o curso Node.js: arquitetura de monolito modular

O curso Node.js: arquitetura de monolito modular possui 404 minutos de vídeos, em um total de 56 atividades. Gostou? Conheça nossos outros cursos de Node.JS em Programação, ou leia nossos artigos de Programação.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Node.JS acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas