DevSecOpsARTIGO

Segregação de Responsabilidades de Comando e Consulta (CQRS): Desacoplamento Extremo com Azure Cosmos DB e Azure AI Search

Segregação de Responsabilidades de Comando e Consulta (CQRS): Desacoplamento Extremo com Azure Cosmos DB e Azure AI Search
Imagem: Cláudio Raposo

Sistemas corporativos de alta demanda frequentemente colapsam quando forçados a executar operações de gravação transacional intensivas e consultas analíticas de texto livre sobre o mesmo motor de banco de dados relacional. O bloqueio de registros (locking) exigido para manter a integridade transacional ACID entra em conflito direto com as varreduras de tabela necessárias para agregações e buscas difusas. Escalar verticalmente o hardware mitiga o sintoma temporariamente, mas atinge um teto financeiro e técnico intransponível. A implementação do padrão Command Query Responsibility Segregation (CQRS) resolve essa contenção física de forma definitiva. Ao utilizar o Azure Cosmos DB (API NoSQL) como um log de eventos imutável para gravações em latência de milissegundos e projetar assincronamente essas mudanças para o Azure AI Search, arquitetamos vias de dados fisicamente separadas. Essa topologia garante que picos massivos de pesquisa orgânica jamais afetem o rendimento de ingestão de novos pedidos, entregando escalabilidade infinita e disponibilidade absoluta para aplicações de hiperescala hospedadas no Azure.

Pré-requisitos

O provisionamento desta arquitetura exige proficiência no design de tabela única (Single-Table Design) em bancos de dados NoSQL e em motores de indexação invertida. A infraestrutura como código necessita de orquestração via Terraform versão 1.7.0 ou superior, utilizando o provedor HashiCorp AzureRM versão 3.90.0 ou superior. A camada de computação de projeção requer o Python 3.12, acoplado ao modelo de programação v2 do azure-functions e às bibliotecas azure-cosmos e azure-search-documents. Identidades Gerenciadas (Managed Identities) são obrigatórias para dispensar o trânsito de chaves primárias no código fonte.

Passo a Passo

O Repositório Imutável com Azure Cosmos DB

A construção do modelo de gravação (Write Model) inicia com a adoção do Azure Cosmos DB configurado como um Event Store imutável. A justificativa técnica para rejeitar operações CRUD tradicionais (Create, Read, Update, Delete) repousa na preservação absoluta do histórico de negócios. Em vez de sobrescrever o saldo de uma conta, o sistema anexa um novo evento de “Crédito” ou “Débito” ao final de um log particionado (append-only). Dimensionamos a partição lógica (Partition Key) utilizando o identificador do agregado (Aggregate ID), garantindo que todos os eventos de uma mesma entidade corporativa residam no mesmo fragmento físico de armazenamento. Para orquestrar a replicação assíncrona exigida pelo CQRS, provisionamos um segundo contêiner oculto dedicado exclusivamente ao gerenciamento de concessões (Lease Container). Este contêiner atua como um rastreador de estado para o processador de fluxo, garantindo que eventos não sejam pulados ou lidos duplicadamente em caso de reinicialização da infraestrutura de computação.

aspnet
resource "azurerm_cosmosdb_account" "event_store" {
name = "enterprise-eventstore-db"
location = var.location
resource_group_name = var.resource_group_name
offer_type = "Standard"
kind = "GlobalDocumentDB"
consistency_policy {
consistency_level = "Session"
}
geo_location {
location = var.location
failover_priority = 0
}
}

resource "azurerm_cosmosdb_sql_database" "domain_db" {
name = "DomainEventsDB"
resource_group_name = azurerm_cosmosdb_account.event_store.resource_group_name
account_name = azurerm_cosmosdb_account.event_store.name
}

resource "azurerm_cosmosdb_sql_container" "events_container" {
name = "Events"
resource_group_name = azurerm_cosmosdb_account.event_store.resource_group_name
account_name = azurerm_cosmosdb_account.event_store.name
database_name = azurerm_cosmosdb_sql_database.domain_db.name
partition_key_paths = ["/aggregateId"]
partition_key_version = 2
}

resource "azurerm_cosmosdb_sql_container" "leases_container" {
name = "Leases"
resource_group_name = azurerm_cosmosdb_account.event_store.resource_group_name
account_name = azurerm_cosmosdb_account.event_store.name
database_name = azurerm_cosmosdb_sql_database.domain_db.name
partition_key_paths = ["/id"]
}

Como extraímos o fluxo contínuo de eventos recém-anexados deste repositório sem submeter o banco de dados principal a consultas onerosas baseadas em carimbos de data e hora?

Projeção Assíncrona via Change Feed e Azure Functions

A extração de mutações ocorre de forma reativa conectando uma Azure Function diretamente ao Cosmos DB Change Feed. O Change Feed opera como um sistema nervoso central nativo do banco de dados, emitindo um fluxo persistente e cronológico de todos os documentos inseridos. O raciocínio arquitetônico para utilizar funções sem servidor (serverless) acopladas ao Change Feed é o isolamento de desempenho. A inserção do comando na API do consumidor é concluída em milissegundos. Nos bastidores, a infraestrutura do Azure invoca o adaptador Python em lote, transferindo os novos eventos corporativos para o motor de projeção. O adaptador da função atua puramente como um tradutor Hexagonal. Ele recebe a carga de evento desnormalizada, aplica lógicas de formatação ou agregações de estado (como consolidar o perfil do usuário) e converte o dado em um documento achatado (flattened document) perfeitamente desenhado para consumo analítico.

Press enter or click to view image in full size

Diagrama de Sequência
aspnet
import logging
import azure.functions as func
from typing import List
import json

app = func.FunctionApp()
class ProjectionEngineAdapter:
def __init__(self):
# Em cenários reais, a injeção do SDK do AI Search ocorre aqui
pass
def project_events(self, events: List[func.Document]) -> None:
for event in events:
event_dict = json.loads(event.to_json())
aggregate_id = event_dict.get("aggregateId")
event_type = event_dict.get("eventType")

logging.info(f"Projetando evento {event_type} para o agregado {aggregate_id}")

# Lógica de tradução: Evento -> Documento de Busca
search_document = {
"id": aggregate_id,
"document_type": "CustomerProfile",
"last_event_applied": event_type,
"payload": event_dict.get("payload", {})
}

# Aqui, o documento seria enviado via API Bulk para o Azure AI Search
self._index_to_search_engine(search_document)

def _index_to_search_engine(self, doc: dict):
# Simulação de envio para o Azure AI Search
logging.info("Documento indexado com sucesso no modelo de leitura.")
projector = ProjectionEngineAdapter()

@app.cosmos_db_trigger(
arg_name="events",
container_name="Events",
database_name="DomainEventsDB",
connection="COSMOS_DB_CONNECTION",
lease_container_name="Leases",
create_lease_container_if_not_exists=False
)
def process_change_feed(events: func.DocumentList):
if not events:
return
logging.info(f"Lote de {len(events)} eventos recebido do Change Feed.")
projector.project_events(events)

Uma vez que o motor de projeção consolidou os eventos puros em visões materializadas ricas, como disponibilizamos esses dados para os clientes sem recriar esquemas relacionais inflexíveis?

Materialização do Modelo de Leitura no Azure AI Search

Disponibilizamos a visão projetada indexando os documentos achatados diretamente no Azure AI Search, estabelecendo-o como o Modelo de Leitura (Read Model) oficial e exclusivo da aplicação. A exigência arquitetônica por um motor de busca dedicado, em vez de outro banco de dados NoSQL genérico, fundamenta-se nas capacidades de pesquisa em texto completo (Full-Text Search), facetamento, roteamento geoespacial e, crucialmente, pesquisa vetorial avançada (Vector Search). O Azure AI Search analisa os tokens linguísticos durante a ingestão dos dados, criando índices invertidos altamente eficientes. Quando a aplicação front-end necessita listar clientes ou produtos, ela jamais interage com o Cosmos DB. As consultas são roteadas estritamente para o endpoint REST do AI Search, que absorve milhões de solicitações de leitura concorrentes sem impactar o banco de dados de gravação primário. Esta assimetria deliberada entre a topologia de gravação e a malha de leitura é a essência matemática que confere escalabilidade absoluta ao padrão CQRS.

Solução de Problemas Comuns

Um incidente operacional frequente durante a projeção de eventos manifesta-se através de paradas silenciosas no consumo do Change Feed. Os registros não acusam erros, mas os dados não chegam ao modelo de leitura. Isso ocorre quase universalmente devido à limitação de Unidades de Solicitação (RU/s) no Cosmos DB. O processador do Change Feed concorre pelo mesmo pool de recursos (Throughput) das operações primárias de gravação. Se as gravações da API esgotarem os RUs provisionados, o Azure Cosmos DB retornará erros HTTP 429 Too Many Requests nos bastidores para a Azure Function, forçando um recuo exponencial (exponential backoff) que interrompe temporariamente a projeção. A solução exige a alteração da configuração de provisionamento do banco de dados para o modo Autoscale, permitindo que a taxa de transferência escale dinamicamente durante picos de ingestão sem penalizar a leitura sequencial do log.

Outra anomalia crítica reside no comportamento de concorrência das instâncias da Azure Function. Se a escalabilidade orientada a eventos do Azure provisionar múltiplas instâncias da função projetora repentinamente, elas poderão tentar processar os mesmos eventos caso a topologia do contêiner de concessões (Leases) não acompanhe a fragmentação do banco principal. Isso é diagnosticado observando avisos de conflito de ETag nos logs da função. Para mitigar o problema de “split brain”, assegure que o contêiner Leases possua RUs suficientes para gerenciar o bloqueio de partições de forma ágil e certifique-se de que a configuração partition_key_paths do contêiner de concessões esteja estritamente definida como /id, otimizando a distribuição de metadados internos do SDK.

Conclusão

A segregação estrutural entre comandos e consultas proporcionada pela sinergia do Azure Cosmos DB com o Azure AI Search redefine os limites de desempenho de aplicações distribuídas. Ao isolar matematicamente as vias transacionais do esforço analítico de pesquisa, as corporações garantem latência previsível na ponta de ingestão, ao mesmo tempo em que entregam interfaces de pesquisa de extrema fluidez para o cliente. O aprofundamento contínuo desta topologia orienta os arquitetos de software a habilitar as capacidades de Busca Semântica (Semantic Search) e indexação vetorial dentro do Azure AI Search, permitindo que o modelo de leitura responda a consultas em linguagem natural baseadas puramente nos eventos de negócio projetados historicamente no ecossistema NoSQL.

Systems Architect Project Leader @NTT DATA👨‍💻 | 2y Microsoft MVP® Developer Technologies 🏆 | 1y Microsoft MVP Azure® 🏆 | 2y Docker Captain® 🏆 | Undergraduate Teacher @Moveedu 👨‍🏫 | 🇺🇸 DBA, Hon.D.Sc., MCS, MBA

Ver perfil