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.

When to use Layer 2: Use Layer 2 when you need to route, transform, aggregate, or filter messages between processes, systems, or services at a level above individual BPMN service tasks. If your routing logic is causing complexity in your BPMN diagrams, it's a sign that Layer 2 is the right abstraction.

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.

Blog post: Read the EIP Pipelines in Priostack article for worked examples of building real integration scenarios with Layer 2 patterns.