Online order intake
A customer checks out on the company webshop. The moment they confirm, the order has to land in the back-office order database so the business can act on it within seconds. The webshop is one party, the order database is the other, and the job is to carry every order from the first to the second without losing any of them. The end result is one new row in the orders table for every checkout. If a save ever fails, the order is not quietly dropped: someone in operations gets an email so it can be fixed and tried again.
So the situation is simple to picture. The webshop hands over each new order, the order database needs to store it, and nothing should fall through the gap in between. The one thing to watch is a failed save, which has to raise a flag rather than disappear.
Here is how Art2link ESB builds that. Messages move across a shared backbone called the bus, and every order travels through it as a canonical Order, one agreed shape the rest of the business reads. The way in is a receive port (the entry point that takes a message in), bound to the API Listener (the adapter, the connector to a particular kind of system, here an HTTP endpoint the webshop posts to). The storefront posts its order and gets an acknowledgement as soon as the message is on the bus; the database write happens just after, on the bus's own time.
The order arrives in the webshop's own shape, classified as WebOrder (the port’s Publish Message Type Fallback labels it, since this endpoint only ever receives one thing). A map (a rule that rewrites one shape into another) then translates it into the canonical Order message type before it is published, so the storefront's field names stop at the edge, map at the edges.
The way out is a send port (a port that delivers a message to an outside system), bound to the SQL Caller and configured as a stored procedure call. Rather than being told what to deliver, the send port states which messages it wants, a subscription. Its subscription is a single predicate:
{{Message.MessageType}} == "Order"
and its parameter bindings draw on three promotions defined on Order:
| Parameter | Value |
|---|---|
| OrderNumber | {{Promoted.Order.OrderNumber}} |
| CustomerId | {{Promoted.Order.CustomerId}} |
| OrderTotal | {{Promoted.Order.OrderTotal}} |
When it fails. Set the SQL send port’s exception message type to OrderInsertFailed. If the insert exhausts its retries, the in-flight order is re-published under that type, payload intact, instead of vanishing. An O365 Mail send port subscribes to it and alerts operations, and the failed run stays visible in tracking for replay once the cause is fixed, fail loud, never drop.
{{Message.MessageType}} == "OrderInsertFailed"
Build it, step by step. The steps run in dependency order: every object is created before the object that selects it.
Under the application’s Message types, create the three types this flow routes on. A message type is a name plus a format, no schema required:
| Name | Format | Purpose |
|---|---|---|
| WebOrder | JSON | the storefront’s payload as it arrives |
| Order | JSON | the canonical shape the rest of the estate consumes |
| OrderInsertFailed | JSON | the exception type the failure path publishes under (Step 7) |
Promotions live on the message type, so add them while you are here. The send port’s parameters (Step 6) and the alert subject (Step 7) bind these values, and adapter parameters are plain strings: they bind {{…}} tokens but never evaluate a body path.
| Promotion | Path |
|---|---|
| OrderNumber | $.orderNumber |
| CustomerId | $.customer.id |
| OrderTotal | $.total |
A failed order re-publishes payload intact, but promotions are type-qualified, so the exception type needs its own:
| Promotion | Path |
|---|---|
| OrderNumber | $.orderNumber |
Three external parties, three 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 | OrderDbSql |
| Adapter | SQL Caller |
| Definition | SQL Server Connection |
| Connection String (Database Config) | the order database’s connection string, credentials included |
| 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 database, the send port (Step 6) calls the procedure. The order number is the primary key, so the MERGE is idempotent, a replayed order updates instead of duplicating.
CREATE TABLE dbo.Orders ( OrderNumber VARCHAR(40) NOT NULL PRIMARY KEY, CustomerId VARCHAR(40) NOT NULL, CustomerName NVARCHAR(200) NULL, Region VARCHAR(20) NULL, OrderTotal DECIMAL(18,2) NOT NULL, Currency CHAR(3) NOT NULL CONSTRAINT DF_Orders_Cur DEFAULT 'EUR', ReceivedAt DATETIME2 NOT NULL CONSTRAINT DF_Orders_Rcv DEFAULT SYSUTCDATETIME() );
CREATE OR ALTER PROCEDURE dbo.UpsertOrder @OrderNumber VARCHAR(40), @CustomerId VARCHAR(40), @OrderTotal DECIMAL(18,2) AS BEGIN SET NOCOUNT ON; MERGE dbo.Orders AS tgt USING (SELECT @OrderNumber AS OrderNumber) AS src ON tgt.OrderNumber = src.OrderNumber WHEN MATCHED THEN UPDATE SET CustomerId = @CustomerId, OrderTotal = @OrderTotal WHEN NOT MATCHED THEN INSERT (OrderNumber, CustomerId, OrderTotal) VALUES (@OrderNumber, @CustomerId, @OrderTotal); END;
Under the application’s Maps, create the map. A map is not a file, it is a named resource stored in the platform, defined by its name and the two message types it sits between, both created in Step 1.
| Map setting | Value |
|---|---|
| Name | WebOrderToOrder |
| Source message type | WebOrder, from Step 1 |
| Target message type | Order, from Step 1 |
A sample WebOrder as the storefront posts it, the same payload Step 8 tests with:
{
"order_no": "10472",
"ship_region": "EMEA",
"buyer": { "ref": "C-2201", "company": "Nordwind Retail" },
"account_mgr": "j.driessen@example.com",
"grand_total": 1249.50
}Maps are authored in XSLT 3.0 and run on Saxon HE 12.9. The WebOrder JSON arrives as a string, so the template matches / and parses it with parse-json(); from there fields are read with the ?key accessor, the result is built as a map{} and serialized back to JSON. The order number gets the SO- prefix every downstream tutorial expects, and the account manager’s address rides along for the order fan-out notification:
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="text"/> <!-- The WebOrder 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:value-of select="serialize( map { 'orderNumber': concat('SO-', string($in?order_no)), 'channel': 'webshop', 'region': string($in?ship_region), 'customer': map { 'id': string($in?buyer?ref), 'name': string($in?buyer?company) }, 'accountManager': map { 'email': string($in?account_mgr) }, 'total': number($in?grand_total), 'currency': 'EUR' }, map { 'method': 'json', 'indent': true() } )"/> </xsl:template> </xsl:stylesheet>
Create a receive port WebshopOrders bound to the API Listener. Everything it references already exists, so each field is a pure selection. (The dropdowns can also create types and maps inline; building the dependencies first keeps each dialog a selection.) If you need to step away mid-setup, Save Draft keeps the port under just its name; a draft does not exist to the runtime until the required fields are filled in and the port is saved properly.
| General | Value |
|---|---|
| Adapter | API Listener |
| Way | One |
Open the adapter configuration (the button beside Adapter) and pair the port with the storefront credential, so every caller must present it:
| Adapter Configuration | Value |
|---|---|
| Auth Config Type | Static |
| Auth Config | StorefrontToken, from Step 2 |
| Verb | POST |
| Headers | the two rows below |
Every API Listener in the environment shares one ingress URL, the read-only Url at the foot of the dialog, so the storefront never calls a port-specific address: the runtime decides which receive port a request belongs to from the App header, the Authentication, and any distinguishing headers (routing on a shared ingress covers the full rule). Declare both in the Headers table:
| 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) |
On the Publish step under the port’s Solicit stages, set Message Type Fallback to WebOrder, from Step 1.
On the Map step, set Map Assignment to Typed and add one row, selecting the map from Step 4:
| Source Message Type | Map | Target Message Type |
|---|---|---|
| WebOrder | WebOrderToOrder | Order |
Create a send port OrderToDatabase bound to the SQL Caller, paired with the OrderDbSql Authentication from Step 2. It subscribes on {{Message.MessageType}} == "Order".
| Setting | Value |
|---|---|
| Adapter | SQL Caller |
| Way | One |
| Command Type | Stored Procedure |
| Command Text | dbo.UpsertOrder |
The procedure from Step 3 needs three values out of the payload; its parameters go under Input Parameters, one key/value row each, binding the promotions defined on Order in Step 1:
| Parameter | Value |
|---|---|
| OrderNumber | {{Promoted.Order.OrderNumber}} |
| CustomerId | {{Promoted.Order.CustomerId}} |
| OrderTotal | {{Promoted.Order.OrderTotal}} |
On OrderToDatabase, set Exception Message Type, on the port’s General step, to OrderInsertFailed, from Step 1. If the insert exhausts its retries, the failed order re-publishes under that type, payload intact, and the promotion you defined on it carries the order number into the alert subject.
Add an O365 Mail send port OpsAlert subscribing on {{Message.MessageType}} == "OrderInsertFailed":
| Setting | Value |
|---|---|
| Authentication | O365Ops, from Step 2 |
| From | noreply@acme.example |
| To | ops@acme.example |
| Subject | Order insert failed: {{Promoted.OrderInsertFailed.OrderNumber}} |
There is nothing to deploy, everything you configured is already saved and live, because Art2link applies changes immediately. To bring the flow online you start the send ports first, then the receive port: start OrderToDatabase and OpsAlert, then start WebshopOrders. Starting the subscribers before the producer means the Listener never accepts an order before the database send port is ready to receive it. (To take the flow offline, stop the receive port first, for the same reason.)
Before you test, set Tracking to Enabled + Body on every port the flow touches, WebshopOrders, OrderToDatabase, and OpsAlert. That records each run step by step with the message body, so you can walk a single order end to end in tracking and see the payload exactly as it was mapped and inserted.
POST the Step 4 sample WebOrder to the shared ingress URL, the one base URL every API Listener in the environment is hosted under (see routing on a shared ingress), and present the three things the Listener routes on: the App: AcmeOrders header, the Purpose: OrderIntake header, and the storefront’s API Authentication credential. Verify in tracking that the run shows the map and the insert; check the row landed with order number SO-10472.
Stop the database or post an order that violates a constraint, and confirm the operations mailbox gets the OrderInsertFailed alert and the run is available for replay.