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.
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.)
{{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.
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.
{{Message.MessageType}} == "OrderDeliveryFailed"
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:
{
"orderNumber": "SO-10472",
"region": "EMEA",
"customer": { "id": "C-2201", "name": "Nordwind Retail" },
"accountManager": { "email": "j.driessen@example.com" },
"total": 1249.50,
"currency": "EUR"
}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:
| Name | Format | Purpose |
|---|---|---|
| Order | JSON | the canonical order all three deliveries consume |
| OrderDeliveryFailed | JSON | the 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.
| Promotion | Path |
|---|---|
| OrderNumber | $.orderNumber |
| CustomerName | $.customer.name |
| ManagerEmail | $.accountManager.email |
| TotalLine | $.total |
A failed delivery re-publishes the order payload intact, but promotions are type-qualified, so the exception type needs its own:
| Promotion | Path |
|---|---|
| OrderNumber | $.orderNumber |
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.
| Setting | Value |
|---|---|
| Name | StorefrontToken |
| Adapter | API Listener |
| Definition | API Listener Token |
| Token (Partner Config) | the secret the storefront will present on every call |
| Setting | Value |
|---|---|
| Name | WarehouseSftp |
| Adapter | SFTP Caller |
| Definition | SFTP Password Authentication |
| Username / Password | the warehouse’s SFTP account |
| Host Key Fingerprint | optional; when set, the connection is refused unless the server’s key matches |
| Setting | Value |
|---|---|
| Name | FinanceDb |
| Adapter | SQL Caller |
| Definition | SQL Server Connection |
| Connection String (Database Config) | the invoicing database’s connection string, credentials included |
Both mail ports (Steps 9 and 10) send through it.
| Setting | Value |
|---|---|
| Name | O365Ops |
| Adapter | O365 Mail Sender |
| Definition | Microsoft Graph |
| Tenant Id / Client Id / Client Secret (Graph AuthConfig) | an app registration with Mail.Send granted |
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.
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;
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 setting | Value |
|---|---|
| Name | OrderToPickFile |
| Source message type | Order, from Step 1 |
| Target | the warehouse’s pick-file layout (CSV) |
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:
<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 </xsl:text> <xsl:value-of select="string-join(( $in?orderNumber, $in?customer?name, $in?region, string($in?total)), ';')"/> <xsl:text> </xsl:text> </xsl:template> </xsl:stylesheet>
The finance delivery gets its own map, which reshapes the canonical Order into the invoice shape the booking procedure (Step 3) expects:
| Map setting | Value |
|---|---|
| Name | OrderToInvoice |
| Source message type | Order, from Step 1 |
| Target | the finance system’s invoice shape (JSON) |
<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>
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.)
| Setting | Value |
|---|---|
| Adapter | API Listener |
| Way | One |
| Authentication | StorefrontToken, from Step 2 |
| Message Type Fallback | Order, from Step 1 |
Declare the two matching headers that keep this Listener unique on the shared ingress (routing on a shared ingress covers the full rule):
| Header | Value | Why |
|---|---|---|
| App | AcmeOrders | the Application’s namespace, narrows the request to this Application |
| Purpose | OrderIntake | keeps 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.
Create a send port WarehousePickFile on the SFTP Caller, subscribing on {{Message.MessageType}} == "Order".
| Setting | Value |
|---|---|
| Adapter | SFTP Caller |
| Way | One |
| Authentication | WarehouseSftp, from Step 2 |
| Host | the warehouse’s SFTP server |
| Remote Directory | their pick-file drop, e.g. /inbound/orders |
On the outbound flow, select OrderToPickFile, from Step 4, so the order leaves in the warehouse’s layout.
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:
order_{{Promoted.Order.OrderNumber}}.csvCreate a send port FinanceRow on the SQL Caller, subscribing on {{Message.MessageType}} == "Order".
| Setting | Value |
|---|---|
| Adapter | SQL Caller |
| Way | One |
| Auth Config | FinanceDb, from Step 2 |
| Command Type | StoredProcedure |
| Command Text | dbo.BookInvoice |
On the outbound flow, select OrderToInvoice, from Step 5, so the procedure receives the invoice shape rather than the raw order.
One row under Input Parameters; the mapped invoice JSON travels whole:
| Parameter | Value |
|---|---|
| InvoiceJson | {{Message.Body}} |
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.
| Setting | Value |
|---|---|
| Authentication | O365Ops, from Step 2 |
| From | noreply@acme.example |
| To | {{Promoted.Order.ManagerEmail}} |
| Subject | New order {{Promoted.Order.OrderNumber}} for {{Promoted.Order.CustomerName}} |
| Body | Order {{Promoted.Order.OrderNumber}} for {{Promoted.Order.CustomerName}}, total EUR {{Promoted.Order.TotalLine}}. |
On WarehousePickFile, FinanceRow, and AccountManagerMail, set Exception Message Type, on the port’s General step, to OrderDeliveryFailed, from Step 1.
Create the OpsAlert O365 Mail port subscribing on {{Message.MessageType}} == "OrderDeliveryFailed":
| Setting | Value |
|---|---|
| Authentication | O365Ops, from Step 2 |
| From | noreply@acme.example |
| To | ops@acme.example |
| Subject | Order delivery failed: {{Promoted.OrderDeliveryFailed.OrderNumber}} |
Everything you configure is live the moment you save it, there is nothing to deploy. Start the send ports first, then the receive ports.
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 one test order. In tracking you should see one published Order and three independent deliveries.
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.