Layer 2: EIP Overview
Layer 2 is the set of Enterprise Integration Pattern (EIP) models in the qubit-core library: package integration, import path ideaswave.com/qubit/core/pkg/integration. You run it inside your own Go service with the SDK's camel runtime (ideaswave.com/qubit/sdk/camel). It is not a hosted Priostack feature: no priostack.com route accepts Layer 2 configuration, and the console deploys none. It sits beside the BPMN execution engine (Layer 1) and talks to it over the REST API.
Architecture Diagram
External Systems / APIs · Messages (JSON/XML/binary)
v
┌────────────────────────────────────────────────────────┐
│ LAYER 2: EIP │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Message │────►│ Message │ │
│ │ Channel A │ │ Router │──► Channel B │
│ └─────────────┘ │ (FEEL) │──► Channel C │
│ └─────────────┘──► Channel D │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Splitter │────►│ Pipeline │ │
│ │ (fan-out) │ │ (filter → │ │
│ └─────────────┘ │ translate)│ │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Aggregator │ │ Correlation│ │
│ │ (collect N)│ │ Context │ │
│ └─────────────┘ │ (dedup) │ │
│ └─────────────┘ │
│ │ │
│ ┌────────────▼────────────┐ │
│ │ Message Endpoint │ │
│ │ (bean calls REST API) │ │
│ └────────────────────────┘ │
└────────────────────────────────────────────────────────┘
│
v
┌────────────────────────┐
│ LAYER 1: BPMN │
│ Execution Engine │
└────────────────────────┘
Only the filter and aggregate steps run in the camel runtime today (plus JSON-Schema validate:, log: and your bean: handlers). The router, splitter, translator, endpoint and correlation context are declared in the model and carried out by your own code.
Patterns Reference
Each pattern is a Go struct in ideaswave.com/qubit/core/pkg/integration. The examples show its fields under their JSON names, which are the only names the model reads. In practice you rarely write this JSON: the camel runtime builds the same structs from a Camel Spring-DSL document (see Layer 2 for a complete routes file and its Go wiring).
Message Channel
A named conduit through which messages flow from producers to consumers. Channels decouple message sources from destinations. kind is point_to_point or publish_subscribe, and item_def_id scopes the channel to one item type.
// A publish-subscribe channel for order events
{
"id": "order-events",
"name": "Order events",
"kind": "publish_subscribe",
"item_def_id": "order"
}
Message Router
Routes a message to one output channel based on FEEL conditions evaluated against the message variables. Routes are tried in priority order, highest first, and the first match wins; a route with no condition is the default. The camel runtime does not execute a router today: your code applies it.
// Route by order amount
{
"id": "order-router",
"name": "Route by amount",
"source_uri": "direct:order-events",
"routes": [
{ "channel_id": "high-value-orders", "condition": "amount > 10000", "priority": 2 },
{ "channel_id": "standard-orders", "condition": "amount > 1000", "priority": 1 },
{ "channel_id": "small-orders" }
]
}
Aggregator
Collects related messages and releases them together. Messages are grouped by the value of correlation_expr, and the group is released as one batch when an arriving message makes completion_cond true. In the camel runtime a held message answers 202 and the released group reaches the bean as one JSON array. The model also has a timeout field, but the camel runtime refuses an aggregate that declares one (its route answers 501).
// Aggregate all order lines before processing
{
"id": "order-lines",
"name": "Collect order lines",
"correlation_expr": "orderId",
"completion_cond": "last = true",
"output_channel_id": "order-ready"
}
Correlation Context
Matches an incoming message to a waiting process instance using a FEEL key expression. Register records a wait under a key value; Match (or MatchVars, which applies the key expression) returns that wait and removes it, so each wait is resumed once. It is not a time-windowed deduplicator: there is no window and no TTL, and entries live in memory.
// Resume the instance waiting for this payment transaction
cc := integration.NewCorrelationContext("transactionId")
cc.Register("TX-981", "run_1ce49bbc5260b39423bd", "wait_payment")
entry := cc.MatchVars(map[string]any{"transactionId": "TX-981"}) // the wait, now removed
again := cc.MatchVars(map[string]any{"transactionId": "TX-981"}) // nil
Pipeline
A multi-stage processing chain (Pipes and Filters). Each step names a registered component by component_id and component_kind: router, filter, translator, aggregator, splitter, try_catch or endpoint. Steps are applied in order.
// Filter, translate, filter again
{
"id": "validated-orders",
"name": "Validate orders",
"source_uri": "direct:order-events",
"steps": [
{ "component_id": "not-test", "component_kind": "filter" },
{ "component_id": "legacy-to-order", "component_kind": "translator" },
{ "component_id": "positive-amount", "component_kind": "filter" }
]
}
Splitter
Takes a single message containing a list and emits one message per list item. Useful for fan-out scenarios where a batch message must be processed item by item. split_expr is a FEEL expression that yields the list.
// Split an order batch into individual orders
{
"id": "order-splitter",
"name": "One message per order",
"split_expr": "orders",
"output_channel_id": "individual-orders"
}
Message Translator
Declares that a message moves from one item definition to another, with the field mapping written as a FEEL context expression in mapping_expr. Field names, types, and structure can be remapped. Used to bridge differences between upstream and downstream message formats.
// Translate from legacy order format to canonical format
{
"id": "legacy-to-order",
"name": "Legacy to canonical order",
"source_item_def": "legacy-order",
"target_item_def": "order",
"mapping_expr": "{ orderId: legacy.order_no, customerId: legacy.cust_id, amount: legacy.amount_cents / 100, currency: upper case(legacy.curr) }",
"output_channel_id": "orders"
}
Message Filter
A single-stage filter that drops messages not matching a FEEL predicate. A dropped message stops there: there is no reject channel. In the camel runtime a dropped request answers 204, and a body that is not a JSON object is dropped too (the filter fails closed). An empty predicate passes everything.
// Only pass orders from the EU region
{
"id": "eu-only",
"name": "EU orders only",
"predicate": "region = \"EU\" and currency in (\"EUR\", \"GBP\", \"CHF\")",
"channel_id": "eu-orders"
}
Message Endpoint
Declares that a process or service reads from (inbound) or writes to (outbound) a channel; service_ref optionally names an ArchiMate business service. An endpoint starts nothing by itself. To start a BPMN process from a Layer 2 route, call POST /api/v1/process-instances from your bean.
// Orders leave Layer 2 for the order service
{
"id": "orders-out",
"name": "To order processing",
"channel_id": "validated-orders",
"direction": "outbound",
"service_ref": "order-service"
}
Integration with BPMN
Layer 2 and Layer 1 meet over the REST API. A bean in your camel runtime starts an instance with POST /api/v1/process-instances (one credit), or completes a job with POST /api/v1/jobs/{key}/complete. A process waiting at a message catch event is offered to workers as a job of type event:message:<elementId>: the hosted API has no message correlation route, so there is no correlation-key matching.
See the Architecture page for the full two-layer system diagram and how the layers interact.