Eventos, Outbox e Inbox¶
Core e Template Service escrevem outbox_events no contexto de seus tenants. O Dispatcher lê/atualiza essa mesma collection, acrescentando estado de transporte. Collections com o mesmo nome podem compartilhar um banco se a configuração resolver o mesmo tenant; ownership aqui é por operação, não uma alegação de exclusividade física.
Modelos visuais¶
Evento e registro de processamento¶
As duas collections se correlacionam pela identidade lógica do evento; o payload transporta a representação String.
Explorar diagramas detalhados com zoom
Legenda e convenções dos modelos. Os dicionários e evidências desta página permanecem a referência dos campos.
Persistência e transporte¶
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
flowchart TB
producer["Core / Template Service<br/><small>Alteração e evento de domínio</small>"]:::domain
outbox["outbox_events<br/><small>Documento Mongo por tenant</small>"]:::store
dispatcher["Outbox Dispatcher<br/><small>Claim, lease e retry</small>"]:::async
rabbit[["RabbitMQ<br/><small>gateway-events</small>"]]:::async
producer -->|Insert| outbox
dispatcher -->|Atualização condicional| outbox
dispatcher -->|Payload JSON / confirm| rabbit
classDef domain stroke-width:1.5px;
classDef store stroke-width:1.5px;
classDef async stroke-width:1.5px;
| Campo BSON | Produtor Java / Dispatcher Go | Escrita e significado |
|---|---|---|
_id |
ObjectId / primitive.ObjectID | Identidade do documento |
eventId |
ObjectId / string no modelo Go | Identidade do evento; contratos possuem diferença de tipo a validar, registrada na análise |
tenantId |
String no produtor; não declarado no struct Go | Contexto de domínio; também incluído no payload |
routingKey |
String / string | Evento de domínio |
payload |
Document / bson.Raw | Conteúdo enviado ao Rabbit; não é documento de negócio inteiro obrigatoriamente |
status |
Enum Java PENDING/SENT; Go PENDING/PROCESSING/SENT | PROCESSING é escrito pelo Dispatcher |
attempts |
Integer / int | Zero na criação; incremento em falha |
nextAttemptAt |
Instant / time.Time | Agora na criação; agenda backoff |
createdAt, updatedAt |
Herdados de BaseEntity Java | Timestamps do produtor; Go declara createdAt |
lastError |
String / *string | Falha; reset ao marcar SENT |
leaseUntil |
*time.Time no Go | Escrito no claim, limpo em conclusão/falha/reaper |
workerId |
string no Go | Dono do claim; filtro condicional nas atualizações |
sentAt |
*time.Time no Go | Conclusão do transporte |
exchange |
string no struct Go; ausente no modelo produtor | O fluxo de publicação usa o exchange da configuração, não esse campo |
Índices criados pelo Dispatcher: {status:1,nextAttemptAt:1,createdAt:1}, {status:1,leaseUntil:1} e {eventId:1} unique. Sem TTL/remoção de SENT declarado. FindOneAndUpdate reivindica PENDING vencido e define lease/worker; sucesso marca SENT; falha volta a PENDING com attempts+1 e backoff. Reaper libera PROCESSING com lease vencido.
Defaults do Dispatcher: polling 5 min, atualização de databases 10 min, batch 200, lease 30 s, backoff base 10 s/máximo 900 s. São defaults de código, não configuração corporativa. Publicação usa confirmação Rabbit; publicar e marcar SENT não são operação atômica. Repetição após falha nesse intervalo é possível.
Fontes: documento Go, claim/índices, dreno, config, publicador, produtor Core.
Contratos de payload¶
| Routing key | Conteúdo construído no código / consumidor |
|---|---|
gateway.published, gateway.deleted |
eventId string hexadecimal, occurredAt, tenantId, gatewayId; OpenAPI Doc gera/remove projeção |
gateway.setups_solved |
eventId, occurredAt, tenantId, data.{operationId,wizardId}; Template Service retoma a operação |
template.published, template.deleted |
eventId, occurredAt, tenantId, gatewayId (ID do GatewayTemplate); OpenAPI Doc distingue a origem pela routing key |
application.import.item |
Mensagem de trabalho publicada diretamente pelo Importer; não é outbox Mongo |
gateway, route, authorization, ip_policy |
Invalidação publicada pelo Cache Invalidator; header tenantId e corpo vazio, distinto de publicação de domínio |
Fragmento sanitizado derivado de OutboxEventService.createGatewayEvent: IDs, tenant e data foram substituídos. Este é o payload, não o documento outbox_events completo.
{
"eventId": "000000000000000000000001",
"occurredAt": "2026-10-13T00:00:00Z",
"tenantId": "tenant-exemplo",
"gatewayId": "000000000000000000000002"
}
Fontes adicionais: produtor Template, Importer messaging, invalidação.
Inbox e projeções¶
%%{init: {"theme":"base","themeVariables":{"fontFamily":"Arial, sans-serif","fontSize":"16px","lineColor":"#64748b","primaryTextColor":"#172b4d"},"flowchart":{"htmlLabels":true,"useMaxWidth":true,"nodeSpacing":28,"rankSpacing":38,"padding":14,"curve":"linear","subGraphTitleMargin":{"top":12,"bottom":16}},"sequence":{"useMaxWidth":true,"actorMargin":28,"width":145,"height":45,"diagramMarginX":32,"diagramMarginY":12,"messageMargin":24,"wrap":true,"mirrorActors":false},"themeCSS":".edgeLabel, .edgeLabel p {background-color:var(--diagram-surface)!important;color:var(--diagram-text)!important}\n.cluster rect {fill:var(--diagram-group)!important;stroke:var(--diagram-line)!important}\n.cluster-label span, .cluster-label text {color:var(--diagram-text)!important;fill:var(--diagram-text)!important}\n.flowchart-link, .actor-line {stroke:var(--diagram-line)!important}\nmarker path {fill:var(--diagram-line)!important;stroke:var(--diagram-line)!important}\n.actor {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\ntext.actor, .messageText, .loopText, .labelText {fill:var(--diagram-text)!important}\n.messageLine0, .messageLine1, .loopLine {stroke:var(--diagram-line)!important}\n.labelBox {fill:var(--diagram-surface)!important;stroke:var(--diagram-line)!important}\n.note {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.noteText {fill:var(--diagram-text)!important}\n.nodeLabel, .nodeLabel p, .nodeLabel span {color:var(--diagram-text)!important}\n.node .nodeLabel, .node .nodeLabel p {font-size:17px;font-weight:600;line-height:1.4;text-align:center}\n.nodeLabel small {display:inline-block;margin-top:4px;font-size:15px;font-weight:400;line-height:1.4;color:var(--diagram-description)!important}\ntext.actor, text.actor tspan {font-size:17px;font-weight:600}\n.node.entry rect, .node.entry polygon, .node.entry path, .node.entry circle {fill:var(--diagram-entry)!important;stroke:var(--diagram-entry-border)!important}\n.node.domain rect, .node.domain polygon, .node.domain path, .node.domain circle {fill:var(--diagram-domain)!important;stroke:var(--diagram-domain-border)!important}\n.node.security rect, .node.security polygon, .node.security path, .node.security circle {fill:var(--diagram-security)!important;stroke:var(--diagram-security-border)!important}\n.node.store rect, .node.store polygon, .node.store path, .node.store circle {fill:var(--diagram-store)!important;stroke:var(--diagram-store-border)!important}\n.node.async rect, .node.async polygon, .node.async path, .node.async circle {fill:var(--diagram-async)!important;stroke:var(--diagram-async-border)!important}\n.node.external rect, .node.external polygon, .node.external path, .node.external circle {fill:var(--diagram-external)!important;stroke:var(--diagram-external-border)!important}\n text.actor tspan, .loopText tspan, .labelText tspan, .messageText tspan, .noteText tspan {fill:var(--diagram-text)!important}\n\n"}}%%
flowchart LR
handler["OpenAPI Doc<br/><small>Processa evento</small>"]:::async
doc["gateway_openapi<br/><small>Projeção atualizada</small>"]:::store
inbox["inbox_processed<br/><small>eventId / processedAt</small>"]:::store
handler -->|Upsert / delete| doc
handler -->|Após processamento| inbox
classDef store stroke-width:1.5px;
classDef async stroke-width:1.5px;
InboxRepository faz insert; EventHandler registra depois do efeito. Não foi implementada consulta prévia de deduplicação nesse caminho. Portanto o registro não garante exactly-once. Os modelos/campos e ausência de unique/TTL estão em OpenAPI Doc.
Operações Templates usam stages, IDs clonados e lease de upgrade para reentrada/checkpoints, não uma collection Inbox equivalente. Importer mantém status/reprocessamento próprio. Filas e efeitos já documentados na matriz assíncrona não são novas collections.
Fontes: EventHandler, InboxRepository, Operation. Análises de dados concentra as divergências de tipos e garantias.