Correlation & conversations¶
When many instances of the same process are alive at once, an incoming message must reach exactly one of them — the instance whose business object (an order, a claim, a ticket) the message belongs to. gobpm resolves this with a correlation key: a value both sides derive from the message payload. On that key the engine either instantiates a fresh handler (a keyed message start event) or routes a follow-up back to the running instance that already owns the key (a keyed in-instance receiver). A sequence of keyed messages threaded through one instance is a conversation.
This is a runtime page: it explains the observable routing behavior and the public contracts you assemble to declare a key. The internal correlation table and its create-or-route-or-join decision live in ADR-016; event-triggered instantiation lives in ADR-015.
flowchart LR
subgraph A["order-source (started explicitly)"]
b((begin)) --> s1[send ORD-1] --> s2[send ORD-2] --> se((sent))
end
subgraph B["order-handler (auto-instantiated per key)"]
or((order-received)) --> report --> h((handled))
end
s1 -.->|order placed ORD-1| or
s2 -.->|order placed ORD-2| or
Two distinct order keys ⇒ two handler instances, disambiguated by the orderId
both sides derive from the payload.
The routing model¶
The broker matches subscribers by message name first; the correlation key then disambiguates which instance among same-named subscribers. Given a name match, the key drives one of three outcomes:
| Situation | Outcome |
|---|---|
| Keyed message start event, no instance owns the key yet | Instantiate — the engine spawns one handler for that key (event-triggered, not StartLatest). |
| Keyed message start event, an instance already owns the key | Join — delivered to the existing owner (see ADR-016). |
Keyed in-instance receiver (ReceiveTask / intermediate catch) |
Route — delivered to the running instance that seeded the key; never a sibling. |
| No key (only a name) | Wildcard — any same-named subscription matches. |
A distinct message name per interaction needs no key at all — the key only earns its keep when instances of the same named message compete.
The correlation contracts¶
A key is assembled bottom-up from three bpmncommon types. Each carries its
foundation.BaseElement identity and rejects empty/nil inputs at construction.
| Type | Role |
|---|---|
CorrelationPropertyRetrievalExpression |
binds one extraction expression (data.FormalExpression) to the Message it reads from. |
CorrelationProperty |
a named, typed partial key — one or more retrieval expressions that extract its value per message. |
CorrelationKey |
the composite key — a Name plus a slice of CorrelationProperty partial keys. |
Their constructors:
func NewCorrelationPropertyRetrievalExpression(
messagePath data.FormalExpression,
messageRef *Message,
baseOpts ...options.Option,
) (*CorrelationPropertyRetrievalExpression, error)
func NewCorrelationProperty(
name, pType string,
exprs []CorrelationPropertyRetrievalExpression,
baseOpts ...options.Option,
) (*CorrelationProperty, error)
func NewCorrelationKey(
name string,
props []CorrelationProperty,
baseOpts ...options.Option,
) (*CorrelationKey, error)
| Constructor | Rejects |
|---|---|
NewCorrelationPropertyRetrievalExpression |
a nil messagePath or nil messageRef. |
NewCorrelationProperty |
a blank name or an empty expression set. |
NewCorrelationKey |
a blank name or an empty property set. |
The key is then declared on both sides with a WithCorrelationKey option — one
per side, from the package that owns each end:
| Option | Side | Behavior |
|---|---|---|
activities.WithCorrelationKey(key *bpmncommon.CorrelationKey) SndTaskOption |
producer (SendTask) |
derive the key from the outgoing payload and stamp it onto the published Envelope. A nil key is a no-op (name-match only). |
events.WithCorrelationKey(key *bpmncommon.CorrelationKey) options.Option |
consumer (message start event) | the engine derives an incoming message's key from it to decide create-or-route-or-join. A nil key is rejected. |
Build it¶
The key is the shared contract. It reads one orderId out of the order placed
payload — a retrieval expression over the message, wrapped in a property,
wrapped in a key:
re, _ := bpmncommon.NewCorrelationPropertyRetrievalExpression(path, msgRef)
prop, _ := bpmncommon.NewCorrelationProperty("orderId", "string",
[]bpmncommon.CorrelationPropertyRetrievalExpression{*re})
key, _ := bpmncommon.NewCorrelationKey("orderKey",
[]bpmncommon.CorrelationProperty{*prop})
Producer side — each SendTask stamps the key derived from its payload
onto the outgoing envelope:
send, _ := activities.NewSendTask("send-"+id,
bpmncommon.MustMessage(messageName,
data.MustItemDefinition(values.NewVariable(""),
foundation.WithID(itemID(id)))),
activities.WithoutParams(),
activities.WithCorrelationKey(key))
Consumer side — the message start event carries the same key. No
StartLatest call is made for it; the engine auto-instantiates one handler per
distinct key:
start, _ := events.NewStartEvent("order-received",
events.WithMessageTrigger(events.MustMessageEventDefinition(
bpmncommon.MustMessage(messageName,
data.MustItemDefinition(values.NewVariable(""),
foundation.WithID("order_in"))),
nil)),
events.WithCorrelationKey(key))
Only the producer is started explicitly; the handler is registered and left for the engine to instantiate:
engine.RegisterProcess(consumer) // keyed start → auto-instantiated
engine.RegisterProcess(producer)
engine.Run(ctx)
engine.StartLatest(producer.ID()) // only the source is started
Both sides build the key from the same payload item with the same retrieval expression, so their derived values coincide — that match is what pairs a message to a key. The payload item id may differ by side (the producer binds
order_ORD-1, the consumer readsorder_in); only the derived value must be identical.
Run it¶
cd examples/inter-instance-correlation && go run .
Each order spawns its own handler, routed by the key:
✓ order "ORD-1" instantiated its own handler instance
✓ order "ORD-2" instantiated its own handler instance
✓ inter-instance correlation: 2 orders ⇒ 2 handler instances, routed by key
Conversations — threading a follow-up¶
A conversation threads a second keyed message back to the instance a first
one started. The key is seeded once, on the message start; a later message
carrying the same key routes to that instance's in-instance receiver — and the
receiver does not re-declare the key. The running instance already owns it,
so a plain ReceiveTask picks up the routed message:
await, _ := activities.NewReceiveTask("await-payment",
bpmncommon.MustMessage(paymentMsg, data.MustItemDefinition(
values.NewVariable(""), foundation.WithID(payItem))),
activities.WithoutParams())
The driver publishes both messages straight to the broker. messaging.Envelope
carries the routing value in its CorrelationKey field (a string; empty means
"no key"):
broker.Publish(ctx, messaging.Envelope{
Name: paymentMsg, Payload: o, CorrelationKey: o})
conversation-routing runs two
conversations (ORD-1, ORD-2) concurrently; each payment routes back to its
originating handler with no cross-talk:
handler reported order/payment: ORD-1/ORD-1
handler reported order/payment: ORD-2/ORD-2
OK: each payment routed to its originating handler conversation
Variations¶
- Publish straight to the broker. Instead of a
SendTask, hand the engine amembrokerand publish envelopes yourself — theEnvelope.CorrelationKeyfield is the routing value:
broker := membroker.New()
engine, _ := thresher.New("demo", thresher.WithMessageBroker(broker))
-
Multi-property keys.
CorrelationKey.Propertiesis a slice — add more than oneCorrelationPropertywhen a single business object needs several fields (e.g.tenantId+orderId) to be unambiguous. -
Same name, different key. With the same message name across competing instances, the key is what disambiguates which one receives it. Distinct names don't need a key.
See also¶
- Examples:
examples/inter-instance-correlation/·examples/conversation-routing/ - Related guides: Message · Send / Receive Task · How events are processed · Definition versioning
- Design: ADR-016 — Message correlation · ADR-015 — Event-triggered instantiation
- Full API:
go doc github.com/dr-dobermann/gobpm/pkg/model/bpmncommon