Comunicação Desacoplada (Event-Driven)
Para manter o isolamento rigoroso entre os módulos (ex: impedir que o módulo de Consultation dependa do módulo de Chat), a comunicação entre eles é estritamente controlada. Nunca permitimos importações diretas de lógica de negócio entre pacotes da pasta modules/.
Estratégia: Domain Events
A comunicação ocorre através de um barramento de eventos centralizado, localizado em core/domain-events. Quando uma ação relevante acontece em um domínio, ele "dispara e esquece" (fire and forget) um evento para o barramento.
Este padrão é essencial em sistemas de saúde para garantir que fluxos secundários (como enviar um SMS de confirmação) não bloqueiem o fluxo principal (como salvar os dados da consulta).
Exemplo Prático: Fluxo de Início de Teleconsulta (entre Módulos)
Imagine que, ao iniciar uma consulta, precisamos criar automaticamente uma sala de chat e notificar o sistema de logs de auditoria.
[Módulo Consultation] ----> Publica: CONSULTATION_STARTED
|
v
[Core/DomainEvents] -------- Barramento de Eventos (Mediator)
|
+-------------------------+-------------------------+
| |
v v
[Módulo Chat] [Módulo AuditLog]
Escuta: CONSULTATION_STARTED Escuta: CONSULTATION_STARTED
Ação: Cria sala privada entre Ação: Registra log de acesso
médico e paciente. ao prontuário.Benefícios do Modelo de Eventos
- Desacoplamento Temporal e Funcional: O domínio de
Consultationnão precisa saber se oChatexiste ou se está funcionando corretamente. Sua única responsabilidade é notificar que a consulta começou. - Extensibilidade: Podemos adicionar um novo módulo (ex:
Schedulingpara agendamento de retorno) que escuta o mesmo eventoCONSULTATION_STARTED, sem alterar uma única linha de código no módulo deConsultation. - Resiliência: Se o módulo de
Chatfalhar ao carregar, o médico ainda consegue visualizar os dados da consulta e o prontuário, pois os domínios são independentes.
Regras e Governança de Interação
Para que este modelo escale, seguimos regras rígidas:
- Payloads Minimalistas: Os eventos devem carregar apenas IDs e status (ex:
{ consultationId: "abc-123", patientId: "user-456" }). O domínio interessado deve usar esses IDs para buscar os dados que precisa através de sua própria API ou store. - Contratos Compartilhados: As definições de nomes de eventos e tipos de payload devem residir em
packages/contracts, garantindo que o publicador e o assinante falem a mesma língua. - Side-Effects em Sagas: A inscrição em eventos de domínio geralmente ocorre dentro de um Redux-Saga. O Saga fica "ouvindo" o barramento e, ao detectar o evento, dispara as ações internas do seu próprio domínio.
Exemplo de Implementação: Barramento de Eventos
Abaixo, demonstramos como o contrato e a implementação do barramento garantem o desacoplamento.
1. Definição do Contrato (Contracts)
// packages/contracts/src/events.ts
export enum DomainEventNames {
CONSULTATION_STARTED = 'CONSULTATION_STARTED',
CONSULTATION_FINISHED = 'CONSULTATION_FINISHED'
}
export interface DomainEventPayloads {
[DomainEventNames.CONSULTATION_STARTED]: {
consultationId: string;
patientId: string;
doctorId: string;
};
}2. Publicação do Evento (Publisher Module)
O módulo de Consultation não conhece o Chat. Ele apenas notifica o sistema.
// packages/modules/consultation/store/sagas.ts
import { domainEventsBus } from 'core/domain-events';
import { DomainEventNames } from 'contracts/events';
function* handleStartConsultationSuccess(data: any) {
// Notifica o ecossistema que a consulta iniciou
domainEventsBus.publish(DomainEventNames.CONSULTATION_STARTED, {
consultationId: data.id,
patientId: data.patientId,
doctorId: data.doctorId
});
}3. Subscrição do Evento (Subscriber Module)
O módulo de Chat ou Audit reage ao evento de forma independente.
// packages/modules/chat/store/sagas.ts
import { take, put } from 'redux-saga/effects';
import { domainEventsBus } from 'core/domain-events';
import { DomainEventNames } from 'contracts/events';
export function* watchConsultationEvents() {
// Cria um canal de eventos para escutar o barramento
const channel = domainEventsBus.createChannel();
while (true) {
const event = yield take(channel);
if (event.name === DomainEventNames.CONSULTATION_STARTED) {
// Inicia a sala de chat específica para esta consulta
yield put(chatActions.initializeRoom({
consultationId: event.payload.consultationId,
members: [event.payload.patientId, event.payload.doctorId]
}));
}
}
}