Padrão CQRS

Armazenamento

O CQRS significa Segregação de Responsabilidade de Comando e Consulta, um padrão que separa as operações de leitura e atualização de um armazenamento de dados. A implementação do CQRS em seu aplicativo pode maximizar o desempenho, a escalabilidade e a segurança. A flexibilidade criada pela migração para CQRS permite ao sistema evoluir melhor ao longo do tempo e impede que os comandos de atualização causem conflitos de mesclagem no nível de domínio.

Contexto e problema

Nas arquiteturas tradicionais, o mesmo modelo de dados é usado para consultar e atualizar um banco de dados. É simples e funciona bem para operações CRUD básicas. Em aplicativos mais complexos, no entanto, essa abordagem pode se tornar complicada. Por exemplo, no lado de leitura, o aplicativo pode executar muitas consultas diferentes, retornando objetos de transferência de dados (DTOs) com formas diferentes. O mapeamento de objetos pode se tornar complicado. No lado da gravação, o modelo pode implementar uma validação complexa e lógica de negócios. Como resultado, você pode terminar com um modelo excessivamente complexo que faz coisas em excesso.

As cargas de trabalho de leitura e gravação geralmente são assimétricas, com requisitos de desempenho e escala muito diferentes.

A traditional CRUD architecture

  • Geralmente, há uma incompatibilidade entre as representações de leitura e gravação dos dados, como colunas ou propriedades adicionais que devem ser atualizadas corretamente, embora não sejam necessárias como parte de uma operação.

  • A contenção de dados pode ocorrer quando as operações são executadas em paralelo no mesmo conjunto de dados.

  • A abordagem tradicional pode ter um efeito negativo sobre o desempenho devido à carga no armazenamento de dados e na camada de acesso a dados e à complexidade das consultas necessárias para recuperar informações.

  • O gerenciamento de segurança e permissões pode se tornar complexo, pois cada entidade está sujeita a operações de leitura e gravação, que podem expor dados no contexto errado.

Solução

O CQRS separa leituras e gravações em modelos diferentes, usando comandos para atualizar dados e consultas para ler dados.

  • Os comandos devem ser baseados em tarefas, em vez de centrados em dados. ("Reservar quarto de hotel", não "definir ReservationStatus como Reservado").
  • Os comandos podem ser colocados em uma fila para processamento assíncrono, em vez de serem processados de forma síncrona.
  • As consultas nunca modificam o banco de dados. Uma consulta retorna um DTO que não encapsula qualquer conhecimento de domínio.

Em seguida, os modelos podem ser isolados, conforme mostrado no diagrama a seguir, embora isso não seja um requisito absoluto.

A basic CQRS architecture

Ter modelos de consulta e atualização separados simplifica o design e a implementação. No entanto, uma desvantagem é que o código CQRS não pode ser gerado automaticamente a partir de um esquema de banco de dados usando mecanismos de scaffolding, como ferramentas O/RM.

Para maior isolamento, você pode separar fisicamente os dados de leitura de dados de gravação. Nesse caso, o banco de dados de leitura pode usar seu próprio esquema de dados, otimizado para consultas. Por exemplo, ele pode armazenar uma exibição materializada dos dados, para evitar mapeamentos de O/RM complexos ou junções complexas. Ele ainda pode usar um tipo diferente de armazenamento de dados. Por exemplo, o banco de dados de gravação pode ser relacional, enquanto o banco de dados de leitura é um banco de dados de documento.

Se forem usados bancos de dados de leitura e gravação separados, eles deverão ser mantidos em sincronia. Normalmente, isso é feito fazendo com que o modelo de gravação publique um evento sempre que atualiza o banco de dados. Para obter mais informações sobre como usar eventos, consulte o estilo de arquitetura controlado por eventos. A atualização do banco de dados e a publicação do evento devem ocorrer em uma única transação.

A CQRS architecture with separate read and write stores

O repositório de leitura pode ser uma réplica somente para leitura do repositório de gravação, ou repositórios de gravação e leitura podem ter uma estrutura completamente diferente. O uso de várias réplicas somente leitura pode aumentar o desempenho da consulta, especialmente em cenários distribuídos em que as réplicas somente leitura estão localizadas perto das instâncias do aplicativo.

A separação dos repositórios de gravação e leitura também permite que cada um seja dimensionado adequadamente para corresponder à carga. Por exemplo, os repositórios de leitura normalmente encontram uma carga muito maior do que os repositórios de gravação.

Algumas implementações do CQRS usam o padrão de Evento de Fornecimento. Com esse padrão, o estado do aplicativo é armazenado como uma sequência de eventos. Cada evento representa um conjunto de alterações nos dados. O estado atual foi criado pela repetição dos eventos. Em um contexto CQRS, um benefício do Fornecimento de Eventos é que os mesmos eventos podem ser usados para notificar outros componentes , em particular, para notificar o modelo de leitura. O modelo de leitura usa os eventos para criar um instantâneo do estado atual, que é mais eficiente para consultas. No entanto, o fornecimento de evento adiciona complexidade ao design.

Os benefícios do CQRS incluem:

  • Dimensionamento independente. O CQRS permite que as cargas de trabalho de leitura e gravação sejam dimensionadas de forma independente e pode resultar em menos contenções de bloqueio.
  • Esquemas de dados otimizados. O lado de leitura pode usar um esquema que é otimizado para consultas, enquanto o lado de gravação usa um esquema que é otimizado para atualizações.
  • Segurança. É mais fácil garantir que apenas as entidades do direito de domínio estejam executando gravações nos dados.
  • Divisão de problemas. Isolar os lados de leitura e gravação pode resultar em modelos mais flexíveis e sustentáveis. A maior parte da lógica de negócios complexa vai para o modelo de gravação. O modelo de leitura pode ser relativamente simples.
  • Consultas mais simples. Ao armazenar uma exibição materializada no banco de dados de leitura, o aplicativo poderá evitar junções complexas durante as consultas.

Questões e considerações de implementação

Alguns desafios para implementar esse padrão incluem:

  • Complexidade. A ideia básica do CQRS é simples. Mas isso poderá resultar em um design de aplicativo mais complexo, especialmente se eles incluírem o padrão Fornecimento de Eventos.

  • Mensagens. Embora o CQRS não necessite de mensagens, é comum usar mensagens para comandos de processo e publicar eventos de atualização. Neste caso, o aplicativo deve tratar as falhas de mensagem ou as mensagens duplicadas. Consulte as diretrizes sobre filas de prioridade para lidar com comandos que têm prioridades diferentes.

  • Consistência eventual. Se você separar os bancos de dados de leitura e de gravação, os dados de leitura poderão ficar obsoletos. O repositório de modelos de leitura deve ser atualizado para refletir as alterações no repositório de modelos de gravação e pode ser difícil detectar quando um usuário emitiu uma solicitação com base em dados de leitura obsoletos.

Quando usar o padrão CQRS

Considere o CQRS para os seguintes cenários:

  • Domínios colaborativos em que muitos usuários acessam os mesmos dados em paralelo. O CQRS permite que você defina comandos com granularidade suficiente para minimizar conflitos de mesclagem no nível do domínio, e conflitos que surgem podem ser mesclados pelo comando.

  • Interfaces de usuário baseadas em tarefas, onde os usuários são guiados por um processo complexo como uma série de etapas ou com modelos de domínio complexos. O modelo de gravação tem uma pilha completa de processamento de comandos com lógica de negócios, validação de entrada e validação de negócios. O modelo de gravação pode tratar um conjunto de objetos associados como uma única unidade para alterações de dados (uma agregação, na terminologia DDD) e garantir que esses objetos estejam sempre em um estado consistente. O modelo de leitura não tem nenhuma pilha de validação ou lógica de negócios e apenas retorna um DTO para uso em um modelo de exibição. O modelo de leitura é, eventualmente, consistente com o modelo de gravação.

  • Cenários em que o desempenho das leituras de dados deve ser ajustado separadamente do desempenho das gravações de dados, especialmente quando o número de leituras é muito maior do que o número de gravações. Nesse cenário, você pode escalar horizontalmente o modelo de leitura, mas executar o modelo de gravação em apenas algumas instâncias. Um pequeno número de instâncias de modelo de gravação também ajuda a minimizar a ocorrência de conflitos de mesclagem.

  • Cenários onde uma equipe de desenvolvedores pode se concentrar no modelo de domínio complexo que faz parte do modelo de gravação e outra equipe pode se concentrar no modelo de leitura e nas interfaces de usuário.

  • Cenários onde o sistema deve evoluir ao longo do tempo e pode conter várias versões do modelo, ou onde as regras de negócio mudam regularmente.

  • Integração com outros sistemas, especialmente em combinação com fornecimento de evento, onde a falha temporal de um subsistema não deve afetar a disponibilidade dos outros.

Este padrão não é recomendado:

  • O domínio ou as regras de negócios são simples.

  • Uma interface de usuário simples no estilo CRUD e operações de acesso a dados são suficientes.

Considere aplicar CQRS em seções limitadas do seu sistema, onde será mais valioso.

Padrão CQRS e Fornecimento de Eventos

O padrão CQRS é frequentemente utilizado juntamente com o padrão de Fornecimento de Evento. Os sistemas baseados em CQRS utilizam modelos de dados de gravação e leitura separados, cada um adaptado a tarefas relevantes e, muitas vezes, localizado em repositórios separados fisicamente. Quando utilizado com o padrão Fornecimento de Evento, o repositório de eventos é o modelo de gravação e é a fonte oficial de informações. O modelo de leitura de um sistema baseado em CQRS fornece exibições materializadas dos dados, geralmente como exibições altamente desnormalizadas. Essas exibições são adaptadas às interfaces e aos requisitos de exibição do aplicativo, o que ajuda a maximizar tanto o desempenho de consulta como exibição.

Utilizando o stream de eventos como o repositório de gravação, em vez dos dados reais em um ponto no tempo, evita conflitos de atualização em um único agregado e maximiza o desempenho e a escalabilidade. Os eventos podem ser utilizados para gerar de maneira assíncrona exibições materializadas dos dados que são utilizadas para preencher o repositório de leitura.

Como o repositório de evento é a fonte oficial de informações, é possível excluir as exibições materializadas e reproduzir todos os eventos passados para criar uma nova representação do estado atual quando o sistema evoluir ou quando o modelo de leitura precisar alterar. As exibições materializadas são efetivamente um cache somente leitura durável dos dados.

Ao utilizar CQRS combinado com o padrão Fornecimento de Evento, considere o seguinte:

  • Como em qualquer sistema onde os repositórios de gravação e leitura são separados, os sistemas baseados nesse padrão são eventualmente consistentes. Haverá algum atraso entre o evento que estiver sendo gerado e o armazenamento de dados sendo atualizado.

  • O padrão adiciona complexidade porque o código deve ser criado para iniciar e tratar eventos e montar ou atualizar as exibições ou objetos apropriados exigidos por consultas ou um modelo de leitura. A complexidade do padrão CQRS quando utilizado com o padrão de Fornecimento de Evento pode dificultar a implementação bem-sucedida e requer uma abordagem diferente para a concepção de sistemas. No entanto, o fornecimento de evento pode tornar mais fácil para modelar o domínio e facilitar para recompilar exibições ou criar novas porque a intenção das alterações nos dados é preservada.

  • A geração de exibições materializadas para uso no modelo de leitura ou projeções dos dados, reproduzindo e manipulando os eventos para entidades específicas ou coleções de entidades pode exigir um tempo de processamento significativo e uso de recursos. Isto é especialmente verdadeiro se requer soma ou análise de valores em longos períodos, pois poderá ser necessário examinar todos os eventos associados. Resolva isso implementando instantâneos dos dados em intervalos agendados, como uma contagem total do número de uma ação específica que ocorreu ou o estado atual de uma entidade.

Exemplo de padrão CQRS

O código a seguir mostra alguns extratos de um exemplo de uma implementação CQRS que utiliza diferentes definições para os modelos de leitura e gravação. As interfaces modelo não ditarão quaisquer recursos dos repositórios de dados subjacentes, e elas podem evoluir e terem ajustes finos de maneira independente porque essas interfaces estão separadas.

O código a seguir mostra a definição do modelo de leitura.

// Query interface
namespace ReadModel
{
  public interface ProductsDao
  {
    ProductDisplay FindById(int productId);
    ICollection<ProductDisplay> FindByName(string name);
    ICollection<ProductInventory> FindOutOfStockProducts();
    ICollection<ProductDisplay> FindRelatedProducts(int productId);
  }

  public class ProductDisplay
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
    public decimal UnitPrice { get; set; }
    public bool IsOutOfStock { get; set; }
    public double UserRating { get; set; }
  }

  public class ProductInventory
  {
    public int Id { get; set; }
    public string Name { get; set; }
    public int CurrentStock { get; set; }
  }
}

O sistema permite aos usuários avaliar produtos. O código do aplicativo faz isso utilizando o comando RateProduct mostrado no código a seguir.

public interface ICommand
{
  Guid Id { get; }
}

public class RateProduct : ICommand
{
  public RateProduct()
  {
    this.Id = Guid.NewGuid();
  }
  public Guid Id { get; set; }
  public int ProductId { get; set; }
  public int Rating { get; set; }
  public int UserId {get; set; }
}

O sistema utiliza a classe ProductsCommandHandler para lidar com comandos enviados pelo aplicativo. Normalmente, os clientes enviam comandos para o domínio através de um sistema de mensagens, como uma fila. O manipulador de comando aceita esses comandos e invoca métodos da interface de domínio. A granularidade de cada comando é projetada para reduzir a chance de solicitações conflitantes. O código a seguir mostra uma estrutura de tópicos da classe ProductsCommandHandler.

public class ProductsCommandHandler :
    ICommandHandler<AddNewProduct>,
    ICommandHandler<RateProduct>,
    ICommandHandler<AddToInventory>,
    ICommandHandler<ConfirmItemShipped>,
    ICommandHandler<UpdateStockFromInventoryRecount>
{
  private readonly IRepository<Product> repository;

  public ProductsCommandHandler (IRepository<Product> repository)
  {
    this.repository = repository;
  }

  void Handle (AddNewProduct command)
  {
    ...
  }

  void Handle (RateProduct command)
  {
    var product = repository.Find(command.ProductId);
    if (product != null)
    {
      product.RateProduct(command.UserId, command.Rating);
      repository.Save(product);
    }
  }

  void Handle (AddToInventory command)
  {
    ...
  }

  void Handle (ConfirmItemsShipped command)
  {
    ...
  }

  void Handle (UpdateStockFromInventoryRecount command)
  {
    ...
  }
}

Próximas etapas

Os seguintes padrões e diretrizes serão úteis ao implementar esse padrão:

Postagens no blog de Martin Fowler:

  • Padrão de Fornecimento de Eventos. Descreve detalhadamente como o Fornecimento de Eventos pode ser utilizado com o padrão CQRS para simplificar tarefas em domínios complexos, ao mesmo tempo em que melhora o desempenho, a escalabilidade e capacidade de resposta. Além disso, como fornecer consistência para dados transacionais, ao mesmo tempo que mantém trilhas de auditoria completas e histórico que podem permitir ações de compensação.

  • Padrão de exibição materializada. O modelo de leitura de uma implementação CQRS pode conter exibições materializadas dos dados do modelo de gravação, ou o modelo de leitura pode ser utilizado para gerar exibições materializadas.