One Request Becomes a Stored Order
Assemble validation, catalogue pricing, persistence and retrieval into the first complete local order API.
The pieces are ready to meet. The client sends product identifiers and quantities. The catalogue supplies prices. The flow calculates an order, writes its representation to the database and lets a second request retrieve that representation.
Checkpoint 14 is the first complete local order API, and it is still a teaching service — it binds to loopback, uses a fixed synthetic retailer and keeps H2 data only for the runtime’s lifetime. Those limits are what let us finish one coherent operation before strengthening its durability and identity model.

Open this checkpoint in ACB
Stop the previous local application, then use File → Open Folder to open book/checkpoints/14-local-order-api. Open src/main/mule/app.xml and use Flow List to select the named flow for each example. Start the controlled dependency in a separate terminal from the companion root with python3 book/stubs/server.py. Choose Run and Debug → Run Mule Application and wait for deployment. Save canvas edits and use Save and Hot-deploy to Local Runtime before repeating a request.
Run python3 book/run.py verify 14 from the companion root to exercise this running checkpoint with its synthetic fixtures. The verifier supplies requests and checks results; it does not start the ACB application. Keep the editor on this checkpoint while reading a failure so an old deployment cannot supply a misleading answer.
Use the command, not the calculation fixture
Two fixtures look almost alike and only one of them belongs here. Start the dependency service, deploy checkpoint 14 and call /lab/setup to prepare its isolated table. Then post book/fixtures/order-command.json to /api/orders. Do not send order-calculation.json — its supplied prices belong to the earlier arithmetic exercises and are deliberately rejected by this API.
The catalogue returns the three prices already familiar from Dana’s order: PEN-01 is 2.5, PAD-22 is 6 and CLP-08 is 1. The following subflow turns the command into the internal priced representation:
Example 036 — Price the accepted command
The complete source is in checkpoints/14-local-order-api/src/main/mule/app.xml.
Choose Flow List → price-order. Select Set Variable: Name command, Value expression payload. Select Request: Connection Config Dependency_HTTP, Method GET, Path /catalogue, and Target Variable catalogue.
Open the following Transform Message → Payload → Inline Script. The script keeps the priced lines in a DataWeave variable so they can supply both the item list and the total:
%dw 2.0
output application/java
var pricedLines = vars.command.items map (item) -> {
sku: item.sku,
qty: item.qty,
unitPrice: vars.catalogue[item.sku],
lineTotal: vars.catalogue[item.sku] * item.qty
}
---
{
orderId: vars.command.orderId,
tenantId: vars.tenant,
currency: "USD",
items: pricedLines,
total: sum(pricedLines map (item) -> item.lineTotal),
state: "ACCEPTED"
}
The command is kept in a variable before Request returns catalogue data, and the Request target keeps the price lookup out of the payload — without both, the command would be gone by the time the transform needed it. Inside the transform, pricedLines names the mapped array so the response can include it and calculate its total from the same values.
vars.catalogue[item.sku] selects a price with a key supplied by the current item. The earlier catalogue fixture is an object whose keys are SKUs. This dynamic selection is appropriate because RAML has already restricted the allowed SKUs to those known keys.
The internal transform uses application/java because the next Mule components consume an in-process value — the storage and response boundaries serialize it as JSON at their own edges. Giving the writer a purpose makes the representation decision easier to follow than treating every intermediate value as a JSON string.
The state ACCEPTED here describes successful acceptance by this local API — its in-memory database cannot honor a promise to survive process termination. The recovery edition will make that persistence boundary explicit before using restart tests.
Store the calculated representation
Example 037 — Create and save the local order
The complete source is in checkpoints/14-local-order-api/src/main/mule/app.xml.
Choose Flow List → create-order. Follow these components in canvas order:
| Component | Configuration to inspect |
|---|---|
| Set Variable | Name tenant; Value expression 'retailer-a' |
| Flow Reference | Name price-order |
| Set Variable | Name order; Value expression payload |
| Database Select | Connection Orders_DB; Target Variable existing |
| Choice | Condition not isEmpty(vars.existing); matching route raises APP:CONFLICT |
| Database Insert | Connection Orders_DB; Target Variable insertResult |
| Set Variable | Name httpStatus; numeric Value expression 201 |
| Transform Message | JSON output; body expression vars.order |
Select Select to inspect its SQL and parameters:
SELECT order_id FROM accepted_orders
WHERE tenant_id = :tenant AND order_id = :id
Its Input Parameters expression is {tenant: vars.tenant, id: vars.order.orderId}. Then select Insert:
INSERT INTO accepted_orders(tenant_id,order_id,response)
VALUES (:tenant,:id,:response)
Its Input Parameters expression is {tenant: vars.tenant, id: vars.order.orderId, response: write(vars.order,"application/json")}. Keep these parameter objects in the component’s expression field, separate from the SQL statement.
The fixed retailer-a value is a synthetic caller context — it is not read from a client-controlled tenant header, and it is not authentication. Chapter 23 will separate trusted identity from the application’s ownership check.
After pricing, vars.order retains the internal order. The lookup checks for an existing order under the same tenant and identity. A sequential repeat raises APP:CONFLICT, which the entry handler maps to 409. The insert writes a JSON representation using bound parameters, and the response returns that same calculated order with status 201.
This first duplicate check is intentionally limited — a Select followed by an Insert does not prevent two concurrent requests from both observing absence. The database primary key still protects row identity, but a friendly concurrent replay policy is later work. Chapter 18 replaces this arrangement with a transactionally recorded result and explicit race tests.
Retrieve the order that was actually written
Example 038 — Retrieve the saved order
The complete source is in checkpoints/14-local-order-api/src/main/mule/app.xml.
Choose Flow List → get-order. The first Set Variable supplies the synthetic tenant. Select Select, using Orders_DB, and inspect:
SELECT response AS "response" FROM accepted_orders
WHERE tenant_id = :tenant AND order_id = :id
Its Input Parameters expression is {tenant: vars.tenant, id: attributes.uriParams.orderId}. The following Choice uses isEmpty(payload) to select a Raise Error of Type APP:NOT_FOUND.
Open the final Transform Message and inspect its JSON payload script:
%dw 2.0
output application/json
---
read(payload[0].response as String, "application/json")
The query includes both the configured tenant and order identity. Select returns an array. If it is empty, the flow raises the public not-found error. Otherwise the stored JSON representation is read and returned as JSON content.
The creation response and the subsequent GET should agree on order identity, items, prices, currency and total. For the canonical command they describe the same three lines and total 32 USD. Compare numeric values and object structure; insignificant JSON spacing does not change the business result.
Run the checkpoint’s verification command. It checks a create, read-back, duplicate conflict and absent order, as well as the malformed request cases from the contract lesson. The tests do not supply an amount to a separate database endpoint. They follow one request through validation, pricing and persistence, then observe the saved result independently.
Inspect the failure boundaries
If the command is malformed, APIkit rejects it before pricing. If the catalogue cannot respond, the API reports a dependency failure instead of saving an invented price. If the database operation fails, the service must not return the successful creation envelope.
A client can still lose the HTTP response after the database write succeeds. Repeating the command currently produces a conflict rather than recovering the prior successful response — a concrete limitation we can now improve, because there is finally an actual persisted representation to recover.
The same is true of notification. This checkpoint does not send a fulfillment event and does not claim that it has. A later outbox will store the intent to publish beside the order. Adding that mechanism will change the acceptance operation in one named checkpoint, with its own failure tests.
The checkpoint also tightens the RAML response to the actual accepted-order fields, numeric totals and error body. Chapter 13 used a broad preview object so routing could be examined first. APIkit request validation alone does not prove these output values; the independent HTTP checks compare the complete response.
Try it
1. Explain the source of 32. Which input does the API trust for prices?
Show answer
The controlled catalogue supplies the unit prices. The command supplies only SKUs and quantities. The mapped line totals are 10, 12 and 10, and their sum is 32. A command containing a price is rejected by the declared request shape.
2. Lose the response mentally. The row exists but the client did not receive 201. What does this checkpoint do on a repeated POST?
Show answer
Its sequential duplicate check returns 409. It has not yet implemented successful-result replay. That behavior is explicit, and chapter 18 replaces it with a recorded-result policy.
3. Identify the milestone. What does this checkpoint prove that the old separate summary and insert probes could not?
Show answer
One HTTP command travels through the API boundary, pricing, calculation and persistence, and a later GET retrieves the same result. Separate probes establish individual mechanisms but do not establish their integration into that operation.
You now have a complete, bounded local service. The next chapters add other ways to receive and process work before we strengthen what acceptance promises after a failure.
Comments