7 padrões de engenharia para integrações confiáveis ​​de mensagens do cliente


Um cliente envia uma mensagem, seu aplicativo cria um ticket de suporte e uma resposta automática confirma o recebimento. O fluxo de trabalho parece simples até que o mesmo evento chegue duas vezes, a API de mensagens expire ou uma atualização de status atrasada substitua as informações mais recentes.

Essas falhas acontecem nas fronteiras entre os sistemas. Uma solicitação HTTP bem-sucedida não garante que seu aplicativo tenha processado um evento corretamente, e uma solicitação malsucedida nem sempre significa que uma operação externa falhou.

Este artigo explora sete padrões para construir integrações de mensagens confiáveis ​​com Node.js e PostgreSQL. Os exemplos usam um formato de evento genérico em vez de qualquer API de provedor específico.

1. Separe o modelo de dados do seu aplicativo da carga útil do provedor

Os provedores de mensagens usam diferentes nomes de campos, identificadores e estruturas de eventos. Se esses detalhes se espalharem por todo o seu aplicativo, mudar de provedor se tornará uma reescrita.

Apresente um adaptador que traduz cargas externas em um pequeno modelo de evento interno:

{
  tenantId: "tenant_42",
  provider: "messaging-provider",
  eventId: "event_901",
  type: "message.received",
  conversationId: "conversation_18",
  messageId: "message_73",
  occurredAt: "2026-10-06T08:00:00Z",
  receivedAt: "2026-10-06T08:00:02Z",
  data: {
    text: "Where is my order?"
  }
}

Manter occurredAt e receivedAt separar. A primeira descreve quando a fonte diz que um evento aconteceu; o segundo registra quando seu aplicativo o recebeu. A diferença deles pode ajudar a revelar atrasos na entrega.

Uma plataforma de mensagens para clientes, como SleekFlow pode fornecer uma caixa de entrada compartilhada e automação de fluxo de trabalho, enquanto seu aplicativo mantém sua própria ordem, conta e lógica de autorização. Manter esse limite explícito torna a integração mais fácil de testar e alterar.

O adaptador deve lidar com:

  • Mapeamento de campos específicos do provedor.
  • Validação de campo obrigatório e comprimento.
  • Tipos de eventos suportados.
  • Normalização de identificadores sem descartar seu namespace.
  • Diferenças de versão do esquema.

Obtenha a propriedade do locatário a partir de uma configuração de integração confiável. Um identificador de locatário dentro de uma carga JSON recebida não deve decidir de forma independente qual conta de cliente receberá o evento.

Por exemplo, um ponto final de webhook autenticado pode mapear um identificador de integração para um inquilino antes de converter a carga útil.

2. Autentique webhooks antes de interpretar seu conteúdo

Um endpoint de webhook acessível publicamente recebe entradas não confiáveis. Conhecer sua URL não deve ser suficiente para criar mensagens ou atualizar conversas.

Implemente exatamente o mecanismo de autenticação documentado do provedor. Alguns provedores usam assinaturas HMAC; outros usam esquemas diferentes.

Para um protocolo HMAC-SHA256 hipotético, a verificação pode ser assim:

import {
  createHmac,
  timingSafeEqual
} from "node:crypto";

function verifySignature(rawBody, signatureHex, secret) {
  if (
    typeof signatureHex !== "string" ||
    !/^(a-f0-9){64}$/i.test(signatureHex)
  ) {
    return false;
  }

  const expected = createHmac("sha256", secret)
    .update(rawBody)
    .digest();

  const supplied = Buffer.from(signatureHex, "hex");

  return (
    supplied.length === expected.length &&
    timingSafeEqual(supplied, expected)
  );
}

Nó createHmac() calcula o resumo, enquanto timingSafeEqual() compara sequências de bytes de comprimento igual. O código de verificação circundante também deve evitar a introdução de deficiências de tempo. Veja o documentação oficial de criptografia do Node.js. Documentação do Node.js v26.10.0

Os bytes assinados são importantes. Analisar JSON e serializá-lo novamente pode alterar os espaços em branco ou a ordem das chaves, produzindo uma sequência de bytes diferente.

Para um aplicativo Express, configure a rota do webhook para reter o corpo da solicitação bruta antes que qualquer analisador JSON global o consuma. Defina um limite de tamanho de solicitação e lide explicitamente com JSON malformado após a autenticação bem-sucedida.

A verificação de assinatura também tem um limite: uma assinatura válida não estabelece que uma solicitação seja recente. Se o provedor oferecer suporte a carimbos de data/hora assinados, aplique suas regras de atualização documentadas. Caso contrário, a desduplicação durável de eventos torna-se especialmente importante para resistir ao processamento repetido.

3. Torne a entrega duplicada inofensiva

Considere esta sequência:

  1. Seu aplicativo recebe um evento.
  2. Ele armazena o evento com sucesso.
  3. A resposta HTTP foi perdida.
  4. O provedor tenta novamente a entrega.

A segunda solicitação é uma nova tentativa legítima. Processá-lo novamente pode criar um ticket duplicado ou acionar outra resposta automática.

Armazene eventos recebidos usando uma restrição de exclusividade:

CREATE TABLE inbound_events (
  tenant_id TEXT NOT NULL,
  provider TEXT NOT NULL,
  event_id TEXT NOT NULL,
  payload JSONB NOT NULL,
  received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  processed_at TIMESTAMPTZ,
  PRIMARY KEY (tenant_id, provider, event_id)
);

Em seguida, insira com um manipulador de conflitos:

INSERT INTO inbound_events (
  tenant_id,
  provider,
  event_id,
  payload
)
VALUES ($1, $2, $3, $4::jsonb)
ON CONFLICT (tenant_id, provider, event_id)
DO NOTHING
RETURNING event_id;

PostgreSQL ON CONFLICT DO NOTHING ignora uma inserção que entra em conflito com a restrição de exclusividade especificada. RETURNING relata as linhas realmente inseridas, permitindo que o aplicativo distinga um novo evento de um duplicado. postgresql.org

Isso é mais seguro do que verificar se existe um evento e inseri-lo em uma operação separada. Duas solicitações simultâneas podem passar nessa verificação preliminar.

No entanto, aceitar um evento uma vez é apenas o primeiro limite. Um trabalhador ainda pode travar após aplicar uma atualização de banco de dados, mas antes de marcar o evento como processado.

Para processamento somente de banco de dados, confirme a atualização comercial e o marcador de processamento na mesma transação. Quando o processamento também chamar uma API externa, introduza um mecanismo de entrega durável separado.

4. Use uma caixa de saída transacional para efeitos colaterais externos

Suponha que o recebimento de uma mensagem do cliente deva criar um ticket e agendar uma resposta de confirmação.

Uma implementação direta pode:

Create ticket
Call messaging API
Mark event processed

Se a chamada da API falhar, o ticket poderá existir sem resposta. Se a resposta for bem-sucedida, mas o processo travar antes de registrar o sucesso, tentar novamente poderá enviar outra resposta.

A caixa de saída transacional aborda o lado do banco de dados desse problema. Armazene a mudança comercial e a intenção de enviar uma mensagem em uma transação de banco de dados:

BEGIN;

-- Apply the business update here.

INSERT INTO outbound_messages (
  tenant_id,
  delivery_key,
  conversation_id,
  payload,
  status
)
VALUES (
  $1,
  $2,
  $3,
  $4::jsonb,
  'pending'
);

-- Mark the inbound event processed here.

COMMIT;

Um trabalhador separado lê os registros pendentes da caixa de saída e realiza a entrega. Se a transação for revertida, nem a mudança de negócio nem a intenção de envio persistirão.

AWS orientação de caixa de saída transacional explica essa abordagem para evitar gravações inconsistentes em bancos de dados e mensagens. Orientação prescritiva da AWS

O padrão não garante entrega exatamente uma vez para uma API externa.

Um trabalhador pode enviar com êxito e travar antes de salvar o resultado. Aborde essa incerteza usando o mecanismo de idempotência suportado pelo provedor ou reconcilie com os registros do fornecedor quando possível. Use uma chave de entrega estável em todas as tentativas.

Para trabalhadores simultâneos, reivindique empregos com arrendamento ou estratégia de bloqueio apropriada. Não mantenha uma transação de banco de dados aberta enquanto aguarda uma solicitação de rede.

5. Modelo de status de entrega como transições, não atualizações arbitrárias

Uma integração de mensagens geralmente recebe eventos de status separadamente da resposta original da API.

Uma mensagem de saída pode passar por:

pending → accepted → delivered → read

Mas os eventos podem chegar fora de ordem. Um atraso delivered notificação não deve substituir uma já gravada read status.

Para uma progressão simplificada, defina classificações explícitas:

const rank = {
  pending: 0,
  accepted: 1,
  delivered: 2,
  read: 3
};

function canAdvance(current, incoming) {
  return (
    Object.hasOwn(rank, current) &&
    Object.hasOwn(rank, incoming) &&
    rank(incoming) > rank(current)
  );
}

Esta função ilustra uma regra; não resolve atualizações simultâneas. Imponha a transição atomicamente no banco de dados ou sob bloqueio para que dois trabalhadores não possam substituir as decisões um do outro.

Mantenha também as falhas separadas desta progressão simples. Um envio rejeitado, uma mensagem expirada e uma mensagem lida têm significados diferentes. Seu modelo de estado deve refletir a semântica real do provedor.

Preserve eventos de status individuais para solução de problemas e, ao mesmo tempo, mantenha uma projeção do estado atual para consultas de aplicativos.

Se um painel precisar de atualizações em tempo real, publique as alterações de estado confirmadas no navegador. Guia do SitePoint para WebSockets e eventos enviados pelo servidor fornece informações básicas sobre como fornecer atualizações em tempo real. Uma conexão ativa deve complementar um endpoint de estado baseado em banco de dados que o navegador pode usar após a reconexão.

6. Tente novamente de acordo com o tipo de falha

Tentar novamente cada falha cria tráfego desnecessário e pode piorar a interrupção.

Classifique os resultados antes de decidir o que fazer:

Resultado Manuseio típico
Solicitação inválida ou carga incompatível Corrija ou rejeite a operação
Falha de autenticação Investigue credenciais ou configuração de integração
Limitação de taxa Siga as orientações documentadas para novas tentativas
Falha temporária do servidor Tentar novamente com espera limitada
Tempo limite após o envio Trate o resultado como incerto
Tentativas repetidas sem sucesso Mover para um estado de trabalho com falha revisável

Um tempo limite merece atenção especial. Seu aplicativo parou de esperar, mas o provedor pode ter aceitado a mensagem.

Quando tentar novamente for apropriado, use espera exponencial com jitter:

function retryDelayMs(attempt) {
  const baseMs = 1000;
  const capMs = 60_000;
  const ceiling = Math.min(
    capMs,
    baseMs * 2 ** attempt
  );

  return Math.floor(Math.random() * ceiling);
}

O jitter distribui novas tentativas ao longo do tempo, em vez de fazer com que todos os trabalhadores tentem novamente simultaneamente.

Honre o fornecedor Retry-After instruções, quando aplicável, e limitar as tentativas e a duração total das novas tentativas. Uma mensagem que foi útil imediatamente pode se tornar confusa horas depois.

Na produção, armazene o tempo da próxima tentativa de forma durável. Um temporizador JavaScript desaparece quando o processo é reiniciado.

7. Teste as falhas e observe todo o fluxo de trabalho

Um teste que verifica 200 A resposta verifica apenas uma pequena parte da integração.

Crie testes em torno das garantias do sistema:

Cenário Comportamento esperado
O mesmo evento entregue duas vezes Uma operação comercial
Duas solicitações duplicadas simultâneas Um registro de evento aceito
Assinatura inválida Nenhuma mudança nos negócios
Assinatura válida com JSON malformado Rejeição controlada
Banco de dados indisponível antes da persistência Nenhum reconhecimento bem-sucedido
Trabalhador sofre acidente após reivindicar emprego O trabalho torna-se recuperável após a sua locação
O envio externo foi bem-sucedido, mas a resposta foi perdida Reconciliação documentada ou nova tentativa idempotente
O status mais antigo chega depois do status mais recente Nenhuma regressão de estado não intencional
O evento tem como alvo a conversa de outro locatário Acesso rejeitado

Use testes unitários para regras de normalização e transição, testes HTTP para manipulação de solicitações e testes reais de integração de banco de dados para exclusividade e comportamento de transação.

SitePoints guia para testar APIs Node.js com Jest oferece um ponto de partida para testes de endpoint. As garantias de simultaneidade e transação precisam de testes adicionais no PostgreSQL, em vez de respostas de consulta simuladas.

Para operações, meça:

  • Idade do evento não processado mais antigo.
  • Tempo entre a aceitação e o processamento do evento.
  • Contagem de mensagens de saída pendentes.
  • Taxas de novas tentativas e falhas permanentes.
  • Falhas de assinatura.
  • Frequência de eventos duplicados.

A idade da fila pode revelar um trabalhador paralisado mesmo quando o endpoint do webhook continua retornando respostas bem-sucedidas.

Use identificadores de correlação para conectar registros de solicitação, evento, trabalho e entrega. Evite registrar assuntos completos de conversas, credenciais ou informações desnecessárias do cliente. Registre metadados operacionais que ajudam a explicar o que aconteceu.

Conclusão

Integrações confiáveis ​​de mensagens dependem de garantias claras em cada fronteira.

Autentique eventos recebidos, preserve-os de forma durável, processe duplicatas com segurança e separe as alterações do banco de dados da entrega externa. Trate as atualizações de status como transições controladas e os tempos limite como resultados incertos. Em seguida, teste as falhas que as solicitações comuns bem-sucedidas nunca revelam.

Essas decisões facilitam a recuperação, a investigação e a extensão de um fluxo de trabalho de mensagens à medida que o aplicativo cresce.



Source link