Skip to content
Art2link ESB HomeDocumentationBlogContact
Updated May 5, 2026
Tutorials/Routing & pub-sub/Order fan-out

Order fan-out

One sales order arrives, and three different teams each need to act on it. The warehouse needs a file so they can pick and pack the goods. Finance needs a row in the invoicing system so the customer can be billed. The account manager wants an email so they know their customer just bought something. It is the same order three times over, but each team wants it in their own way. The aim is to take that one order and hand a copy to all three, with each copy in the form that team can use. If one of the three handovers fails, say the warehouse server is down, the other two still go through, and the failed one is held and retried rather than lost.

So the shape of the problem is one order coming in and three deliveries going out, each to a different team and in a different form. The deliveries do not depend on each other. If the warehouse copy cannot be sent right now, finance and the account manager should not have to wait for it.

One order, three teams served New order arrives once Hand a copy to each team Warehouse pick file to pack the goods Finance invoice row to bill the customer Account manager email that the order came in a failed copy is held and retried, not lost

Here is how Art2link ESB builds that. The trigger is an order arriving from the storefront. Intake is the online order intake build: an API Listener (the adapter, the connector to a particular kind of system, here an HTTP endpoint) backs a receive port (the entry point that takes a message in), a map (a rule that rewrites one shape into another) normalizes the order, and a canonical Order message lands on the bus (the shared backbone all messages travel across). The producer publishes once and is done; it neither knows nor cares who reads the order. Three send ports (ports that deliver a message to an outside system) then each pick the order up. Rather than being handed work, each one states which messages it wants, a subscription, and carries out its own delivery: a warehouse pick file, a finance database row, and an account-manager email. (This walkthrough builds those three subscribers; if the intake build is not already in place, Step 6 stands up a minimal receive port so you have something publishing an Order to test against.)

EXPRESSION
{{Message.MessageType}} == "Order"

Each port turns the single canonical Order into exactly the shape its consumer needs: the SFTP Caller maps it to the warehouse’s pick-file layout via its own outbound map, the SQL Caller maps it to the finance system’s invoice shape the same way, and the O365 Mail port composes its notification from promotions on the Order. One message in, three different shapes out, and each port also runs its own retries and failure handling, so one slow SFTP server never delays the database row or the email.

API Listener Order published BUS SFTP Caller map → pick file SQL Caller map → invoice O365 Mail map → email Warehouse SFTP Finance DB Inbox

The payoff shows up on the day a fourth consumer appears, say a BI feed. That is a new send port with the same one-line subscription: no change to the storefront, no change to the three existing ports, no redeploy. Subscribe, don't hardcode.

When it fails. Each port fails alone. Set one shared Exception Message Type on all three ports, OrderDeliveryFailed, and subscribe one operations alert port to it, the re-published payload and tracking identify which delivery broke. A warehouse SFTP outage suspends only the pick-file delivery; finance and the account manager already have theirs, and the held file is replayed from tracking when the server returns.

EXPRESSION
{{Message.MessageType}} == "OrderDeliveryFailed"
Fan-out is not a transaction. The three deliveries are independent by design, there is no "all or none." If a consumer must never act on an order that another consumer rejected, that is a process decision to model explicitly, not something to wish onto pub-sub.

Build it, step by step. The steps run in dependency order: every object is created before the object that selects it. The three deliveries all feed off canonical Order messages; the fields this example relies on:

JSON
{
  "orderNumber": "SO-10472",
  "region": "EMEA",
  "customer": { "id": "C-2201", "name": "Nordwind Retail" },
  "accountManager": { "email": "j.driessen@example.com" },
  "total": 1249.50,
  "currency": "EUR"
}
Before you start. Everything below lives inside the same Application as the intake build. If you are starting clean, create AcmeOrders with Namespace AcmeOrders under Applications and select it, so every port and map below is created in its scope.
1
Step One
Create the message types
The two types

Under the application’s Message types, create the two types this flow routes on. A message type is a name plus a format, no schema required:

NameFormatPurpose
OrderJSONthe canonical order all three deliveries consume
OrderDeliveryFailedJSONthe exception type the failure path publishes under (Step 10)

Promotions live on the message type, so add them while you are here. The pick file’s filename (Step 7), the notification’s mail fields (Step 9), and the alert subject (Step 10) bind these values, and adapter parameters and mail fields are plain strings: they bind {{…}} tokens but never evaluate a body path.

Promotions on Order
PromotionPath
OrderNumber$.orderNumber
CustomerName$.customer.name
ManagerEmail$.accountManager.email
TotalLine$.total
Promotion on OrderDeliveryFailed

A failed delivery re-publishes the order payload intact, but promotions are type-qualified, so the exception type needs its own:

PromotionPath
OrderNumber$.orderNumber

2
Step Two
Create the Authentications

Four external parties, four credentials. Under the application’s Authentications, the dialog asks for a Name, the Adapter it pairs with, and a Definition, the credential shape that adapter offers, with the Application preset; the Definition decides the config section that follows.

Storefront credential, for the API Listener
SettingValue
NameStorefrontToken
AdapterAPI Listener
DefinitionAPI Listener Token
Token (Partner Config)the secret the storefront will present on every call
Warehouse server, for the SFTP Caller
SettingValue
NameWarehouseSftp
AdapterSFTP Caller
DefinitionSFTP Password Authentication
Username / Passwordthe warehouse’s SFTP account
Host Key Fingerprintoptional; when set, the connection is refused unless the server’s key matches
Finance database, for the SQL Caller
SettingValue
NameFinanceDb
AdapterSQL Caller
DefinitionSQL Server Connection
Connection String (Database Config)the invoicing database’s connection string, credentials included
Sending mailbox, for O365 Mail

Both mail ports (Steps 9 and 10) send through it.

SettingValue
NameO365Ops
AdapterO365 Mail Sender
DefinitionMicrosoft Graph
Tenant Id / Client Id / Client Secret (Graph AuthConfig)an app registration with Mail.Send granted

3
Step Three
Create the database objects

Create the table and procedure in your invoicing database; the finance port (Step 8) calls the procedure. BookInvoice loads the mapped invoice JSON with OPENJSON and is idempotent on the invoice number, so a replayed order books once.

The invoice ledger and booking procedure
SQL
CREATE TABLE dbo.InvoiceLedger (
    InvoiceNumber VARCHAR(40)   NOT NULL PRIMARY KEY,
    CustomerId    VARCHAR(40)   NOT NULL,
    Amount        DECIMAL(18,2) NOT NULL,
    Currency      CHAR(3)       NOT NULL,
    BookedAt      DATETIME2     NOT NULL CONSTRAINT DF_IL DEFAULT SYSUTCDATETIME()
);

CREATE OR ALTER PROCEDURE dbo.BookInvoice
    @InvoiceJson NVARCHAR(MAX)
AS
BEGIN
    SET NOCOUNT ON;
    MERGE dbo.InvoiceLedger AS tgt
    USING (
        SELECT invoiceNumber, customerId, amount, currency
        FROM OPENJSON(@InvoiceJson)
        WITH (
            invoiceNumber VARCHAR(40)   '$.invoiceNumber',
            customerId    VARCHAR(40)   '$.customerId',
            amount        DECIMAL(18,2) '$.amount',
            currency      CHAR(3)       '$.currency'
        )
    ) AS src
        ON tgt.InvoiceNumber = src.invoiceNumber
    WHEN NOT MATCHED THEN
        INSERT (InvoiceNumber, CustomerId, Amount, Currency)
        VALUES (src.invoiceNumber, src.customerId, src.amount, src.currency);
END;

4
Step Four
Create the pick-file map
Create the map

Under the application’s Maps, create the map the warehouse port (Step 7) selects. A map is not a file, it is a named resource stored in the platform:

Map settingValue
NameOrderToPickFile
Source message typeOrder, from Step 1
Targetthe warehouse’s pick-file layout (CSV)
Author the XSLT

Maps are authored in XSLT 3.0 and run on Saxon HE 12.9. Order-level fields the warehouse needs; if your canonical Order carries line items, iterate $in?lines?* here instead:

XSLT 3.0
<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <!-- The Order JSON arrives as a string; parse-json() turns it into an XPath 3.1 map -->
  <xsl:template match="/">
    <xsl:variable name="in" select="parse-json(.)"/>
    <xsl:text>ORDER;CUSTOMER;REGION;TOTAL&#10;</xsl:text>
    <xsl:value-of select="string-join((
      $in?orderNumber,
      $in?customer?name,
      $in?region,
      string($in?total)), ';')"/>
    <xsl:text>&#10;</xsl:text>
  </xsl:template>
</xsl:stylesheet>

5
Step Five
Create the invoice map
Create the map

The finance delivery gets its own map, which reshapes the canonical Order into the invoice shape the booking procedure (Step 3) expects:

Map settingValue
NameOrderToInvoice
Source message typeOrder, from Step 1
Targetthe finance system’s invoice shape (JSON)
Author the XSLT
XSLT 3.0
<xsl:stylesheet version="3.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="text"/>
  <xsl:template match="/">
    <xsl:variable name="in" select="parse-json(.)"/>
    <xsl:value-of select="serialize(
      map {
        'invoiceNumber': concat('INV-', $in?orderNumber),
        'customerId': string($in?customer?id),
        'amount': number($in?total),
        'currency': string($in?currency)
      },
      map { 'method': 'json', 'indent': true() })"/>
  </xsl:template>
</xsl:stylesheet>

6
Step Six
Create the receive port
General

If the online order intake build already lives in this Application, skip this step: its API Listener and WebOrderToOrder map are the intake. On an empty environment, stand up a minimal edge instead. This walkthrough starts where a canonical Order is already on the bus, so the test rig publishes one directly, no map. Create a receive port WebshopOrders. (The dropdowns can also create types and maps inline; building the dependencies first keeps each dialog a selection.)

SettingValue
AdapterAPI Listener
WayOne
AuthenticationStorefrontToken, from Step 2
Message Type FallbackOrder, from Step 1
Declare the routing headers

Declare the two matching headers that keep this Listener unique on the shared ingress (routing on a shared ingress covers the full rule):

HeaderValueWhy
AppAcmeOrdersthe Application’s namespace, narrows the request to this Application
PurposeOrderIntakekeeps this Listener unique if the App later gains a second one (e.g. a stock lookup)

The Step 11 test then posts the canonical sample above to the shared ingress with those two headers.


7
Step Seven
Create the warehouse file port
General

Create a send port WarehousePickFile on the SFTP Caller, subscribing on {{Message.MessageType}} == "Order".

SettingValue
AdapterSFTP Caller
WayOne
AuthenticationWarehouseSftp, from Step 2
Hostthe warehouse’s SFTP server
Remote Directorytheir pick-file drop, e.g. /inbound/orders
Select the map

On the outbound flow, select OrderToPickFile, from Step 4, so the order leaves in the warehouse’s layout.

Set the filename

The Filename wraps the order number in a literal prefix and extension, binding the OrderNumber promotion from Step 1, so the sample order lands as order_SO-10472.csv:

TEMPLATE
order_{{Promoted.Order.OrderNumber}}.csv

8
Step Eight
Create the finance port
General

Create a send port FinanceRow on the SQL Caller, subscribing on {{Message.MessageType}} == "Order".

SettingValue
AdapterSQL Caller
WayOne
Auth ConfigFinanceDb, from Step 2
Command TypeStoredProcedure
Command Textdbo.BookInvoice
Select the map

On the outbound flow, select OrderToInvoice, from Step 5, so the procedure receives the invoice shape rather than the raw order.

Input Parameters

One row under Input Parameters; the mapped invoice JSON travels whole:

ParameterValue
InvoiceJson{{Message.Body}}

9
Step Nine
Create the notification port
General

Create an O365 Mail send port AccountManagerMail, subscribing on {{Message.MessageType}} == "Order". No outbound map here: the notification composes straight from the promotions defined on Order in Step 1.

Compose the mail fields
SettingValue
AuthenticationO365Ops, from Step 2
Fromnoreply@acme.example
To{{Promoted.Order.ManagerEmail}}
SubjectNew order {{Promoted.Order.OrderNumber}} for {{Promoted.Order.CustomerName}}
BodyOrder {{Promoted.Order.OrderNumber}} for {{Promoted.Order.CustomerName}}, total EUR {{Promoted.Order.TotalLine}}.

10
Step Ten
Wire the failure path
Set the Exception Message Type

On WarehousePickFile, FinanceRow, and AccountManagerMail, set Exception Message Type, on the port’s General step, to OrderDeliveryFailed, from Step 1.

Create the OpsAlert send port

Create the OpsAlert O365 Mail port subscribing on {{Message.MessageType}} == "OrderDeliveryFailed":

SettingValue
AuthenticationO365Ops, from Step 2
Fromnoreply@acme.example
Toops@acme.example
SubjectOrder delivery failed: {{Promoted.OrderDeliveryFailed.OrderNumber}}

11
Step Eleven
Start the ports and test
Start in dependency order

Everything you configure is live the moment you save it, there is nothing to deploy. Start the send ports first, then the receive ports.

Turn on tracking

Set Tracking to Enabled + Body on every port the flow touches, so you can walk each run step by step, with its message body, in tracking.

Post a test order

Post one test order. In tracking you should see one published Order and three independent deliveries.

Break it on purpose

Stop the warehouse SFTP server and post another order: file delivery suspends and alerts, the database row and the email still complete, that independence is the whole point. Replay the held delivery when the server is back.

Turn bodies off in production. Enabled + Body is the most expensive tracking level, extra processing and database space, and it records every payload including successful runs. Once the flow is proven, set the ports to Only on Error instead: failures still capture the full body for diagnosis, while healthy runs are not recorded.