Modular Monoliths - Parte 2 | Identificando os módulos

No primeiro post desta série, contamos a história por trás da nossa decisão de implementar monolitos modulares. Nesta parte, vamos detalhar um pouco mais as características deles e o processo de descoberta dos módulos que utilizamos.

1 - Monolito e modular, as definições


     1.1 - Monolito

Em uma tradução livre, de acordo com a Wikipedia, um monolito é uma aplicação autocontida e independente de outras. Como filosofia de design, esse tipo de aplicação é responsável não apenas por executar uma tarefa, mas todos os passos necessários para completar uma função particular. (https://en.wikipedia.org/wiki/Monolithic_application)

 

A imagem acima tenta ilustrar essa ideia, onde todos os componentes necessários para a execução de uma determinada tarefa estão contidos dentro das fronteiras da aplicação. Normalmente, um monolito pode estar associado a uma unidade de deploy.


     1.2 - Modular

David Parnas escreveu, em 1971, um paper que, para nós, é a melhor referência sobre o que é um design modular e como construí-lo. 

Nesse trabalho, Parnas define que um sistema modular é aquele que é dividido em um número relativamente independente de módulos, com interfaces de comunicação bem definidas. Cada um desses módulos é pequeno e simples o suficiente para que um programador consiga entender seu funcionamento como um todo. (On the criteria to be used in decomposing systems into modules, David Parns, 1971 - https://www.win.tue.nl/~wstomv/edu/2ip30/references/criteria_for_modularization.pdf)


Projetar um sistema modular não é uma tarefa fácil: é necessário pensar em como os módulos podem ser separados; como estabilizar a comunicação entre eles; e como lidar com o acoplamento, o que pode tornar esse trabalho bem desafiador.


 

 

 


No diagrama acima, é possível ver como os módulos se comunicam por meio de interfaces bem definidas (as setas mostram a comunicação entre os módulos). A grande questão aqui é como identificar esses módulos. 

A partir das definições de monolito e modular, podemos avançar e entender como podemos utilizar esses conceitos juntos, por meio dos monolitos modulares.


2 - Como identificar os módulos?

Muito se discute sobre qual seria a melhor forma de dividir uma aplicação em módulos. Pela natureza do problema que resolvemos aqui na Beep, achamos que a melhor solução seria utilizar alguma técnica que evidenciasse o problema de negócio. Escolhemos utilizar as técnicas de Domain Driven Design.


     2.1 - Bounded Contexts

Na parte IV do livro Domain Driven Design, Eric Evans descreve o que ele chama de strategic design. É um conjunto de princípios que nos ajudam a guiar as decisões de design, para reduzir a interdependência entre as partes de um sistema.

Um conceito fundamental descrito nessa parte são os Bounded Contexts. O autor se baseia no princípio de que, em um mesmo sistema, existem múltiplos modelos de domínio diferentes. Além disso, argumenta que é inviável tentar definir um modelo de domínio único para um sistema inteiro. Ele sugere, então, dividir esses domínios em torno da linguagem comum, falada pelos especialistas. Um Bounded Context delimita o significado da linguagem e a aplicabilidade de um modelo de domínio. (Domain Driven Design - Tackling Complexity in the Heart of Software - Chapter 14 - Maintaining the model integrity)


Normalmente, no início de um projeto, é comum - durante o processo de entendimento do problema - tentar modelar o problema como um todo, definindo, assim, um único esquema de banco de dados e um único modelo de objetos, que representam o domínio. A figura abaixo ilustra um caso desse:


 

 


Essa figura representa uma parte do modelo de domínio - ou melhor, o modelo de tabelas - de uma aplicação. Repare na quantidade de dependências que esses objetos têm. Um modelo único como esse é complicado de evoluir e mudar. A proposta do Evans é: observar como o problema de negócio é organizado em torno da Ubiquitous Language - a linguagem comum falada pelos especialistas de domínio - e que, a partir daí, se extraiam os Bounded Contexts.

 



A figura acima mostra que, ao invés de ter o modelo inteiro, é possível dividi-lo em vários contextos, cada um com suas características específicas.

Usando a Beep como exemplo, podemos destacar alguns dos contextos que temos trabalhado. Lembrando: a Beep é uma empresa de saúde, que permite que os usuários agendem exames ou vacinas, e nós vamos até as suas casas para atendê-los. Abaixo, uma amostra do que temos modelado:



A definição desses Bounded Contexts reproduz, de certa forma, como o negócio se organiza. Mas o mais importante é perceber que as fronteiras destacadas limitam o uso da linguagem e o vocabulário. Vamos entender isso melhor mais à frente.


     2.2 - O processo


Uma pergunta fundamental é como identificar os Bounded Contexts. Talvez o principal guia na identificação de Bounded Contexts sejam as diferenças entre a linguagem, aquela falada pelos especialistas do domínio, e suas diferenças entre partes distintas do sistema.

Observar a linguagem falada nos permite entender melhor o significado de determinados termos dentro de uma comunicação específica. Por exemplo, aqui na Beep, o significado da palavra “paciente” pode variar dependendo de qual setor está envolvido na comunicação. Quando falamos sobre agendamento, algumas informações do paciente que estamos interessados são:

  • Qual é o nome e a data de nascimento?
  • Qual é o dia de agendamento?
  • Qual é o endereço de atendimento?

Quando estamos discutindo com a equipe de enfermagem sobre a vacinação de um paciente, estamos mais interessados no tipo de informação a seguir:

  • Como a caderneta de vacinação está organizada?
  • Qual é a próxima vacina a ser tomada?
  • Qual vacina foi aplicada?

E para terminar, em uma conversa com o Financeiro, algumas das informações de pacientes que eles estão interessados são:

  • Qual é o custo das vacinas aplicadas?
  • Foi usado algum desconto na compra?
  • Qual foi o meio de pagamento utilizado?
  • A nota fiscal foi emitida?

Esses são apenas três casos, nos quais a palavra “paciente” é usada com diferentes significados. Tentar representar todas essas características de paciente, em um único objeto, vai acabar gerando um modelo grande e complicado de adicionar mudanças. Observar a diferença entre os significados de determinado termo é uma dica importante da existência de contextos diferentes. No exemplo, poderíamos ter três candidatos que contextualizassem o uso do termo “paciente”: agendamento, enfermagem e financeiro.


Uma vez que temos esses candidatos, precisamos pensar em como podemos representar o termo “paciente” nesses contextos. De acordo com Domain Driven Design, não deveria haver traduções entre os termos que os especialistas de domínio usam e o código que está escrito. Isso significa que precisa existir algo que represente o conceito e seja nomeado como "paciente" no código. No nosso caso, como utilizamos uma linguagem orientada a objetos, provavelmente vamos ter uma classe no código para representar o paciente. Mas temos três contextos diferentes: o que fazer?


 

 


Na proposta acima, podemos notar que a classe “paciente”, nesse caso bem simplificada, é compartilhada pelos três contextos. Repare que, nessa mesma classe, tenta-se adicionar responsabilidades para satisfazer as necessidades dos três contextos. Esse tipo de solução acaba gerando a maioria dos problemas já listados, principalmente relacionados ao acoplamento e a dificuldade de suportar mudanças. A solução sugerida pelo Domain Driven Design é que cada contexto tenha sua própria implementação de “paciente”.


 


No diagrama acima, foi adicionada uma implementação de “paciente” para cada contexto. É importante notar que, além da classe “paciente” adicionada, cada contexto define a semântica de uso e as responsabilidades do paciente. Por exemplo: não faz sentido, no contexto de agendamento, ter uma implementação que verifique se a nota fiscal foi recebida. Da mesma forma que não é necessário saber sobre cadernetas de vacinação no contexto financeiro.

A vantagem de uma separação como essa é que é possível adicionar mudanças sem impactos descontrolados pelo modelo inteiro.


     2.3 - Responsabilidades de cada contexto


Uma vez que os candidatos aos contextos foram identificados, é muito importante começar a definir as responsabilidades de cada um. Isso vai permitir enxergar as fronteiras entre eles e como se comunicam. No início desse processo de descoberta, sabemos pouco sobre o problema que estamos explorando. Logo, é imprescindível fazer isso de maneira iterativa, ou seja, começar com um modelo mais simples e ir adaptando à medida que o conhecimento do problema vai se solidificando. 

Dessa forma, observar a linguagem vai lhe ajudar a refinar os contextos e suas responsabilidades. No exemplo que vimos até agora, só nos atentamos ao uso do termo “paciente”. Mas quais seriam as responsabilidades do contexto de agendamento? A resposta simples e direta poderia ser algo como: 


"Permitir que o paciente registre um agendamento para uma data e um horário.” 


Com essa definição, aparecem outros termos que, de alguma forma, devem estar explícitos dentro do contexto de agendamento: Agendamento, Data e Horário. De acordo com o Domain Driven Design, deveríamos conversar diretamente com os especialistas do domínio e modelar diretamente da linguagem usada por ele. Nesse caso, aqui na Beep, se formos falar com algum especialista de domínio sobre agendamento, ele poderia descrever essa mesma frase desta forma:


"Permitir que um cliente registre um agendamento em um slot disponível para ele ou algum dependente."

As duas frases têm diferenças importantes: a primeira mostra que o especialista (nesse caso, a Central de Relacionamento) não usa “paciente” diretamente para falar de agendamento; na verdade, quem agenda é um cliente. A segunda mostra que o especialista do domínio pode usar um vocabulário bem próprio do negócio e que, nesse caso, foi usada uma palavra diferente para referenciar data e horário: slot. Por fim, foi destacado que o cliente pode registrar o agendamento para um dependente.



Com esse refinamento, o contexto de agendamento já ganha novos elementos e uma correção no uso dos termos. Lembrando que aqui só estamos explorando a linguagem e o processo de descoberta dos Bounded Contexts.

 

Temos outro candidato que chamamos de “enfermagem”. Quais seriam as responsabilidades dele nesse contexto? Novamente, se não conversarmos com os especialistas de domínio, poderíamos chegar em uma definição simples:

 

"Registrar a aplicação de vacinas e coleta de amostras para exames de um paciente."

 

Nesse caso, percebe-se que existem duas coisas diferentes envolvidas: vacinação e exames. Além disso, as necessidades são diferentes: para vacinação, a parte importante pode ser resumida da seguinte forma: 

 

"Registrar a aplicação de vacinas em um paciente por uma técnica de vacina, com informação da dose aplicada, lote e data de validade das vacinas."

Nesse caso, temos novos termos e uma mudança significativa no nosso candidato a Bounded Context: não faz sentido um contexto com o nome “enfermagem”. De acordo com os especialistas de domínio, podemos chamar esse contexto de “vacinação”.



Além desse contexto, temos um novo candidato, ainda não explorado, mas provavelmente com grandes chances de se tornar um novo contexto, com suas próprias responsabilidades e modelo de domínio:



Nesse caso, é importante passar pelo processo de tentar conversar com os especialistas do domínio para entender melhor quais são as responsabilidades desse novo contexto. O nome correto é “exames”? Quais são os termos usados e como estão relacionados? Esses são exemplos de perguntas que podem lhe ajudar a observar a linguagem e como ela é usada.


3 - Onde estamos mesmo?


Este post tentou mostrar o processo de identificação e refinamento de módulos, usando uma abordagem baseada em Domain Driven Design, com ajuda de Bounded Contexts. O processo todo não é feito de uma vez. Muito provavelmente os primeiros desenhos e recortes dos Bounded Contexts vão ser simplistas e ingênuos. É preciso considerar que é um processo iterativo, que vai se refinando com o tempo. Então, estar aberto e preparado para mudanças é fundamental.

Na próxima parte desta série, vamos explorar a modelagem de domínio e como podemos tirar vantagem de um modelo de domínio rico para expressar, de maneira clara, as nuances do domínio, e como podemos permitir acomodar mudanças de maneira mais fácil.

 

Até lá!


Modular Monoliths - Parte 1 | A história por trás da decisão

1 - O contexto

O ano era 2018. A Beep tinha pouco mais de 50 colaboradores e crescia de maneira acelerada. A equipe de tecnologia era formada por 5 desenvolvedores, que estavam desde a concepção do negócio. Como qualquer startup em sua fase inicial, boa parte da base de código foi criada em torno de muita experimentação. Antes de encontrar o tão sonhado product market fit, o negócio mudava em questão de meses ou, até mesmo, em poucos dias. 

O resultado desse processo foi ter um legado sem muita sofisticação no quesito design. Além disso, em determinado momento, esse código começou a afetar nossa velocidade de mudança, principalmente por conta do acoplamento exagerado que existia.

Depois de uma série de tentativas, o negócio parecia ter encontrado, finalmente, seu product market fit ideal e começava crescer ainda mais rápido. Como a maioria da operação da Beep é suportada por tecnologia, precisávamos nos adaptar ao novo cenário. Fizemos um levantamento dos problemas que estávamos enfrentando inicialmente e chegamos a uma lista parecida com essa:

  • Dificuldade de mudança - se alterávamos uma parte do código, quebrávamos a outra; 
  • Dificuldade de adicionar novos membros no time - novos membros teriam de conhecer a base como um todo;
  • Número de bugs aumentando devido às mudanças rápidas que precisavam acontecer.

A resposta que encontramos para esse cenário, em 2018, parecia óbvia - afinal, era o hype da época: dividir nossa base de código em partes menores e publicá-las separadamente. Isso reduziria o acoplamento entre as partes e nós poderíamos controlar melhor as mudanças. Resumidamente, a ideia era distribuir nossa aplicação em uma arquitetura baseada em microservices. Isso pareceu uma ótima ideia inicialmente, então resolvemos começar a implementação dessa arquitetura, por meio de um componente menor, com menos riscos, onde poderíamos experimentar e aprender de maneira mais controlada.

Escolhemos implementar toda a lógica de gestão de equipes de atendimento em um componente separado. A gestão das equipes é um elemento central do nosso negócio e está diretamente relacionada com a quantidade de horários disponíveis para agendamento no aplicativo. Ou seja, para conseguirmos vender, precisamos ter equipes disponíveis.

 

2 - Microservices na Beep

2.1 - Arquitetura inicial

Antes de implementarmos microservices na Beep, contávamos, basicamente, com dois grandes sistemas: 

  • Agendamento, responsável, principalmente, pelas vendas e acompanhamento dos atendimentos diários; 
  • Operação, chamado carinhosamente de Oswaldo pelo nosso público interno. 

O Oswaldo é responsável por toda a gestão da nossa operação, desde definir quais são as áreas de atendimento onde atuamos até o controle de estoque das vacinas que aplicamos.

O diagrama abaixo ilustra uma versão mais simplificada da nossa arquitetura nessa época:

 

 

As plataformas de venda se comunicam com o nosso sistema de agendamento por meio de uma Http API. Esse sistema é responsável por validar se data e horário estão disponíveis e se é possível comprar o produto desejado. Além disso, ele confirma o pagamento e publica uma série de eventos para outros subsistemas interessados. O mais importante deles é o Oswaldo, que, por sua vez, registra esse agendamento e o disponibiliza para as equipes de Imunizações, Operações e Central de Relacionamento.

2.2 - Extraindo o primeiro Microservice

A gestão das equipes de atendimento era feita diretamente no Oswaldo e foi justamente esse o primeiro componente a ser extraído. Depois dessa alteração, o desenho da arquitetura ficou assim:

 

 

Resultado: o diagrama ficou ligeiramente mais complicado que o anterior. Agora, temos um conjunto mais amplo de relações entre os sistemas que precisamos monitorar. Essas relações estão representadas pelas setas, que são divididas em sólidas - indicam uma comunicação síncrona entre os componentes - e tracejadas - indicam uma comunicação assíncrona. Boa parte da comunicação entre esses componentes é feita de forma assíncrona, publicando eventos diretamente em um broker de mensagens, que, no nosso caso, é o RabbitMQ.

Durante a implementação desse primeiro componente, já notamos algumas dificuldades que não tínhamos no modelo anterior. Isso ficou mais evidente, inicialmente, na parte da infraestrutura. Vários questionamentos tiveram de ser respondidos quando resolvemos separar o código desse componente. Alguns deles não faziam sentido antes da separação, então, tivemos que lidar com algumas situações praticamente do zero:

  • Onde será feito o deploy desse componente? Uma instância nova EC2?
  • Como será feito o deploy? Vamos utilizar Containers? ECS? Kubernetes?
  • As APIs que estão expostas precisam de segurança?
  • Precisamos lidar com o versionamento dessas APIs, mesmo para uma aplicação interna nossa?
  • Como lidar com as transações que eventualmente podem ser distribuídas?
  • Como implementar o monitoramento dessa aplicação? Como monitorar traces distribuídos em várias aplicações?

Repare que todas essas perguntas são técnicas. Nenhuma resolve, de fato, o problema de negócio que esse componente tem como responsabilidade: gerir a disponibilidade das equipes nas agendas de atendimento.

A literatura já alertava sobre essas adversidades na distribuição de objetos há, pelo menos, 15 anos. Martin Fowler, em seu livro Patterns of Enterprise Applications, descreveu o que a comunidade chamou de “primeira lei dos objetos distribuídos”:

Don't distribute your objects.

Apesar de já termos alguma experiência com objetos distribuídos e saber que esses obstáculos eram comuns, resolvemos a maioria deles, atrasamos a decisão de outros e optamos por seguir com a estratégia.

Uma vez que esse microservice já estava no ar, começamos a colher alguns frutos interessantes: 

  • A lógica de disponibilidade, agora, estava totalmente isolada;
  • Adicionar ou alterar alguma funcionalidade seria possível com o conhecimento somente daquele domínio;
  • Era mais fácil ter uma cobertura de testes, pois a aplicação foi construída de maneira totalmente isolada do framework.

O resultado foi bom e resolvemos extrair um segundo componente.

2.3 - Extraindo a gestão de estoques para marketplace

Com os bons resultados do primeiro microservice, resolvemos seguir com a extração de mais um componente, que, em teoria, seria de baixo risco e isolado. Naquela época, a Beep tinha uma modalidade de vendas baseada em marketplace: uma clínica poderia publicar sua agenda em nosso aplicativo. Isso permitia que o cliente agendasse sua vacinação e fosse até uma clínica tomar a vacina. Um problema muito importante, que precisava ser resolvido nessa modalidade de vendas, era quantificar as vacinas que poderíamos vender. Ou seja, de alguma forma, era necessário que controlássemos o estoque das clínicas disponíveis no marketplace. Assim como a gestão de equipes, a gestão de estoques do marketplace também era responsabilidade do Oswaldo. Identificamos e deixamos explícito o Bounded Context, que cuidava do controle desse estoque, e extraímos para um novo microservice. O desenho da arquitetura ficou assim:

 

 

Com a introdução do novo componente, podemos observar um aumento nas relações entre eles, com mais linhas sólidas e tracejadas aparecendo. Isso, no fim das contas, se traduz em complexidade.

Esse componente, em especial, tinha uma característica muito mais transacional que o anterior: para toda venda que acontecia no marketplace, um evento era lançado e o componente era responsável por tratar aquele evento, para que, em um determinado momento, algum item fosse removido do estoque. O problema de controle de estoque já tem seus desafios habituais, então lidar com ele em um cenário distribuído começou a ficar ainda mais desafiador. 

As transações distribuídas ganharam nossa atenção. Antes, todo o processo de atualização da saída de produtos era feito no mesmo sistema de agendamento. Isso facilitava muito porque, caso algo desse errado, um rollback na transação era suficiente para solucionar o problema. Agora, com as responsabilidades divididas entre 3 componentes distribuídos, o rollback não era mais tão simples assim. Se um dos componentes falhar, dependendo de onde essa falha acontecer, desfazer esse processo pode ser muito trabalhoso. Imagine o exemplo:

  1. Cliente seleciona 3 vacinas diferentes e confirma o agendamento
  2. O sistema de agendamento inicia as validações do processo para confirmar o agendamento
    1. É verificado se a data ainda está disponível
    2. Confirma-se o pagamento
    3. Os eventos de agendamento confirmado são lançados
  3. Os sistemas interessados (inclusive o componente de estoque) reagem aos eventos e executam suas tarefas

Uma vez que o processo de agendamento é baseado em eventos assíncronos, se acontecer algo de errado na baixa do estoque, o que fazemos? A solução para esse tipo de problema é bem documentada na literatura, desde o uso de Two-phase commit até soluções mais sofisticadas com Sagas. O ponto aqui não é como resolver esse caso, mas o fato dessas situações estarem acontecendo apenas por conta da solução de arquitetura que escolhemos. Isso não tem uma relação direta com o problema de negócio que estamos tentando resolver: controlar a quantidade de vacinas disponíveis no marketplace.

2.4 - A realidade

Com a implementação desses dois componentes, aprendemos rapidamente que existe uma série de desafios para uma implementação bem sucedida de microservices. Alguns dos problemas que percebemos além dos que listamos aqui:

  • Monitoramento é fundamental para entender o que está acontecendo em cada componente;
  • Logs passam a ser um problema distribuído também - e você precisa deles; 
  • Debugar um problema que passa por vários componentes - que podem executar de maneira assíncrona - não é uma tarefa trivial;
  • Ter alguém dedicado a tarefas de infraestrutura, um DevOps ou alguém que faça esse papel é muito importante para o sucesso da implementação.

Percebemos que, para o nosso contexto específico da Beep (naquela época em que a equipe tinha 5 desenvolvedores, sem nenhum DevOps e com o negócio mudando rápido), o mais importante era resolver os problemas do negócio e não ficar travado por problemas técnicos, que drenavam nossa velocidade de entrega.

 

3 - A solução proposta

Em um determinado momento dessa jornada, nos deparamos com o seguinte tweet do Simon Brown:

 

Isso pareceu fazer muito sentido e fez nosso time refletir fortemente sobre quais eram as motivações que nos levaram a distribuir nossa aplicação. Durante essa reflexão, percebemos que o mais complicado era a maneira como estávamos desenvolvendo nosso código dentro do monolito. Todos os problemas pareciam se encontrar em um problema maior, que era o acoplamento exagerado entre as partes. Percebemos que estávamos nos deixando guiar pelo framework e não tomando os devidos cuidados com o design.

3.1 - Voltar pro monolito?

Naquele momento, nós já tínhamos entendido que continuar extraindo os componentes não estava ajudando muito. Quer dizer, até ajudava, mas, no fim das contas, estava mais complicado do que antes. Então, resolvemos procurar alternativas para tentar melhorar nosso design dentro dos nossos monolitos.

A equipe já tinha uma boa experiência com design orientado a objetos e conhecia bem as práticas de Domain Driven Design, só não estávamos aplicando aquilo de maneira organizada. A partir daí, resolvemos tentar entender como poderíamos tirar vantagem de uma abordagem de design para conseguir evoluir e manter o código dentro da mesma base de código.

Pesquisando um pouco mais sobre o que o Simon Brown estava querendo dizer naquele tweet, chegamos a uma proposta bem interessante, que ele chamou de Modular Monoliths.

3.2 - Modular Monoliths

A ideia básica por trás do conceito de monolito modular vem da organização e separação da sua aplicação em módulos ou componentes autônomos, que se relacionam entre si, mas estão dentro de uma mesma base de código.

Normalmente, as decisões sobre os estilos arquiteturais que podemos usar variam entre estas extremidades:

 

 

Nesse cenário, de um lado vemos um monolito tradicional, com todo o código na mesma base, mas sem necessariamente estar separado e organizado. Do outro lado, vemos um conjunto de componentes distribuídos.

Diferentemente de um monolito tradicional, o monolito modular preza por ter os componentes explícitos, seguindo os mesmo princípios de modularização que são usados para distribuir.

 

No exemplo acima, é possível observar que, dentro da mesma aplicação, o código está separado, se comunicando por interfaces bem definidas, de maneira que seja possível ter um controle mais detalhado do acoplamento entre eles, podendo até ter bases de dados diferentes para diferentes módulos.

Usando o primeiro diagrama como exemplo, os monolitos modulares são uma solução intermediária entre a base de código única e os componentes distribuídos.

 

Livre adaptação do diagrama de Simon Brown - http://www.codingthearchitecture.com/2014/07/06/distributed_big_balls_of_mud.html

 

Esse diagrama mostra que podemos ter uma solução intermediária entre as duas extremidades.

Foi então que, em 2019, resolvemos parar de distribuir nossas aplicações e implementar os monolitos modulares nas nossas bases de código. Isso tem nos ajudado a focar mais no problema de negócio, evitando, assim, os problemas listados, que estão muito mais relacionados com tecnologia.

Nas próximas partes deste artigo, vamos detalhar o processo por trás da implementação desse tipo de solução, explorar em detalhes como são organizados e quais técnicas nos baseamos para a sua implementação.

Até lá!


Camada de Aplicação, Domain Driven Design e Isolamento do Domínio

Após muito tempo sem escrever aqui, resolvi comentar sobre uma discussão que aconteceu na lista do .NetArchitects. A dúvida é sobre o que é a Camada de Aplicação em Domain Driven Design.

Não quero entrar em muitos detalhes sobre o que é Domain Driven Design, mas isolar seu domínio é algo fundamental. O Capítulo 4 — Isolating the Domain, do livro azul, discorre sobre o tema e introduz a noção de Arquitetura em Camadas (Layered Architecture). Divisão em camadas não é nenhuma novidade, mas Evans mostra como a arquitetura em camadas pode ser utilizada como mecanismo de isolamento do domínio. No livro, as camadas são separadas assim:

Essas camadas são formas de organizar as dependências dentro da sua aplicação. User Interface, Domain e Infrastructure são velhas conhecidas e normalmente não existe muita dúvida sobre quais são as responsabilidades de cada uma. Agora, o que é a Application Layer?