Skip to content

An order pipeline that scales for month-end and costs nothing at 2am

An event-driven serverless rebuild of a B2B order-processing pipeline: triggered per purchase order, scaling for month-end volume, and costing next to nothing overnight.

Order pipeline · month-end close

Intake, queue, validate, price, route, dispatch · each stage scales on its own

Backlog 0Fri 31 Jul 2026 · 23:41
Orders today
41,184
Peak hour
3,110 at 11:00
Delivered to ERP
41,181
Held for replay
3 payloads kept
Servers to patch
0 none to run

Topology, as it runs

Retry loop and dead-letter path drawn · live readings at 23:41
Intake
order-intake1 in flightFires per order
Queue
4 SQS queuesDepth 0One per stage
Validate
order-validate0 in flightMax 200
Price
order-price1 in flightReserved 24
Route
order-route0 in flightMax 50
Dispatch
order-dispatch1 in flightMax 20
Backoff 8 s · 30 s · 2 min · 10 min · up to 5 attempts
SourcesEDIFACT over AS2 · Orders APIOne EventBridge event per order
orders-dlq3 held of 41,184 todayOriginal payload kept · parked, not stalling
ERP41,181 deliveredEach acknowledged

Orders by hour

41,184 today
00:0006:0012:0018:0023:00
02:00 · 0 orders, nothing warm

Queues now

Depth · in flight
validate-q00
price-q01
route-q00
dispatch-q01

Held for replay

3 of 41,184
Tanfield Trade SuppliesTTS-4002287109:14
Oxley Build CentresOBC/PO/11880411:37
Marram Garden Co.MGC-2026-773116:52

The shape of the work

Industry
Logistics & Distribution
Duration
10 weeks
Cooperation model
Fixed price
Services
Serverless architectureEvent-driven pipelineManaged data & queues
Integrations
StripeSendGridSegmentSentry
Technologies
AWS LambdaAmazon SQSAmazon EventBridgeDynamoDBTypeScriptInfrastructure as code
Team
1 Project lead1 Product designer2 Frontend engineers1 Backend engineer

Client name withheld under NDA. Engagement details are shown to the extent our agreement permits.

The hard problem

A provisioned server cluster processed incoming EDI and API purchase orders continuously, sized for the handful of month-end days when volume actually peaked. Idle capacity was paid for most nights, and a promotion that pushed volume past what the cluster could absorb still risked a backed-up queue.

Validation, pricing and fulfillment routing ran in one process, so they scaled as one, and pricing called a third-party service that was slower than the other two combined. Under load the whole pipeline ran at pricing's pace, which meant adding capacity mostly bought more concurrent calls to a vendor that then rate-limited them.

We decomposed the order pipeline into functions triggered directly off each incoming order, fanned out through a managed queue to validation, pricing, and fulfillment-routing steps, and moved order state into managed storage so every function stayed stateless and could scale independently.

How the pieces fit

  1. 01

    Mapped the pipeline into discrete functions: intake, validation, pricing, fulfillment routing

    The pipeline was mapped into stages by what each actually waits on, which is how pricing's third-party call turned out to be setting the pace for everything.

  2. 02

    Replaced the provisioned server cluster with event-triggered functions and a managed queue

    An incoming order raises an event and starts the pipeline on that order alone, fanned out through per-stage queues each with its own failure domain.

  3. 03

    Tuned concurrency and cold-start handling for the month-end volume spike

    Tuning happened against a replay of a real month-end instead of synthetic load, because the shape of the spike is what breaks things, not its size.

  4. 04

    Moved order state into managed storage so functions stayed stateless

    Order state moved to managed storage with conditional writes, so a redelivered message re-reads, sees the work done and exits instead of pricing twice.

Introduction

The system we were asked to build

A fixed cluster of servers ran around the clock to process incoming purchase orders, sized for the volume spike at month-end close. We rebuilt the pipeline as event-triggered functions backed by a managed queue and managed storage, so it scales with real order volume instead of a server count fixed for the worst week of the month.

A distributor taking purchase orders over EDI and an API, processed by a server cluster sized for the three days at month end when volume peaks. That cluster ran every night at a few percent utilization. The engagement was commissioned after a promotion pushed volume past the cluster's ceiling on an ordinary Tuesday and orders queued for six hours before anyone noticed.

Cloud-Native Architecture

Triggered per order

Functions fire on each incoming purchase order instead of a cluster polling for work.

The old cluster polled for work and sat idle between peaks. Now an incoming purchase order raises an EventBridge event and the pipeline starts on that order alone, so latency doesn't depend on where in the polling cycle it arrived. Fan-out through SQS gives each stage its own queue and its own failure domain, and a poison message parks in a dead-letter queue instead of stalling the line.

What shipped
  • EventBridge trigger per order: no polling cycle to wait on
  • Per-stage SQS queues, each its own failure domain
  • Poison messages park in a DLQ instead of stalling the line
Trace · ORD-260731-048213

BRK-PO-770412 from Brackwell Hardware Ltd · EDIFACT ORDERS over AS2 · arrived 14:06:22.0

Export traceFri 31 Jul 2026 · 23:41
BRK-PO-770412Brackwell Hardware Ltd · 18 lines
Arrival → ERP ack
38.8 s at 14:07:00.8
Hops
6 intake → dispatch
Attempts on validate
2 of 5 allowed
Rejected onceDelivered

Every hop, on one clock

Seconds from arrival · no polling cycle before the first hop
0s10s20s30s38.8 s
IntakeReceived, parsed, event raised
0.3 s
validate-qWaiting for an invocation
0.2 s
Validate · attempt 1Delivery point not in directory
0.6 s
BackoffBack on validate-q for 8 s
8.0 s
Validate · attempt 218 of 18 lines pass
0.5 s
price-qQueued at reserved concurrency
16.8 s
PriceThird-party pricing service
4.3 s
Price · redeliveryAlready PRICED: re-read, exited
0.1 s
RouteAll lines to Doncaster DC
3.8 s
DispatchPosted, ERP acknowledged
4.3 s

The event that started it

EventBridge · order.received
{ "detail-type": "order.received",
  "source": "rillgate.intake",
  "detail": { "orderId": "ORD-260731-048213",
    "po": "BRK-PO-770412", "lines": 18,
    "deliveryPoint": "5060412380017" },
  "time": "2026-07-31T13:06:22.0Z" }

Why the line kept moving

An event per order started this trace at once, not on a polling cycle
Its rejection sat on validate-q alone; price-q and route-q kept draining
A fifth failure would park it in orders-dlq, not stall the line
On screen

One purchase order traced through every hop on one clock: started by its own event, rejected on validation, retried after backoff, and acknowledged by the ERP 38.8 seconds after it arrived.

Capacity · month-end day

Each function scales on its own concurrency · pricing held to the vendor's rate limit

Backlog at close 0Fri 31 Jul 2026 · 23:41

Orders per hour

Fri 31 Jul 2026 · 00:00 to 23:59
Orders in the day41,184EDI and API together
Peak hour · 11:003,110Above the old cluster's ceiling
02:00 hour0 warmNothing running, nothing billed
Before14 instancesAlways on, sized for this day
Old cluster’s ceiling3,110
0003060912151821
Orders in the hourPeakOld ceiling (fixed cluster)Scaled to zero overnight

Concurrency by function

Readings at the 11:00 peak
FunctionWaits onConcurrencyIn flight at peakQueued here
order-intakeNothing, fires per orderUnreservedNo limit set90
order-validatePartner directoryMax 200140
order-priceThird-party pricing service Reserved 2424at limit212 on price-q
order-routeStock by depotMax 5060
order-dispatchERP acknowledgementMax 2050
On screen

Month-end capacity: 3,110 orders at the peak hour, above the old cluster's ceiling, and concurrency set per function, with pricing held at its reserved limit so the spike queues at that step alone.

Independent scaling

Validation, pricing, and routing scale separately, so the slowest step stops throttling the rest.

Validation is cheap and bursty, pricing calls a slow third party, and fulfillment routing is heavy but rare. As one process they ran at the speed of the slowest; as separate functions with their own concurrency limits they no longer contend. Pricing gets a reserved concurrency that respects the vendor's rate limit, so a spike queues at that step instead of triggering vendor-side throttling for everyone.

What shipped
  • Per-function concurrency, so the slowest step stops throttling the rest
  • Reserved concurrency on pricing to respect a vendor rate limit
  • Spikes queue at one step while the rest of the line keeps moving
Order state · ORD-260731-048213

DynamoDB · orders table · every write conditional on the state the function expected

HistoryFri 31 Jul 2026 · 23:41
Held between invocations
Nothing functions are stateless
Writes applied
5 one per hop that did work
Redeliveries
1 re-read, saw PRICED, exited
Pricing calls
1 not two

The item

orders · strongly consistent read
pkORDER#ORD-260731-048213
skSTATE
statusDISPATCHED
version5
poBRK-PO-770412
buyerBrackwell Hardware Ltd
lines18
depotDoncaster DC
receivedAt2026-07-31T13:06:22.0Z
erpAckAt2026-07-31T13:07:00.8Z
Any function, any instanceThe item is the only place this order’s progress lives, so an invocation can be retried, replaced or scaled out without asking another.

Conditional writes

BRK-PO-770412 · 7 attempts to write
TimeFunctionConditionResult
06:22.3order-intakeIntakePutItemattribute_not_exists(pk)RECEIVED · v1
06:23.1order-validateValidate—Attempt 1 rejected before any writeNo write
06:31.6order-validateValidateUpdateItemstatus = RECEIVED AND version = 1VALIDATED · v2
06:52.7order-pricePriceUpdateItemstatus = VALIDATED AND version = 2PRICED · v3
06:53.4order-pricePrice · redeliveredUpdateItemstatus = VALIDATED · found PRICED, v3Exited
06:56.5order-routeRouteUpdateItemstatus = PRICED AND version = 3ROUTED · v4
07:00.8order-dispatchDispatchUpdateItemstatus = ROUTED AND version = 4DISPATCHED · v5
14:06:53.4, the queue delivered the pricing message twice. The second invocation’s condition failed, it re-read the item, saw the work done and exited before calling the pricing service.

Stateless by design

On screen

The traced order's state in DynamoDB and every write against it. Each is conditional on the state the function expected, so the redelivered pricing message re-read, found the order priced and exited.

Order state lives in managed storage, so any function can be retried or scaled without coordination.

Order state lives in DynamoDB keyed by order, so no function holds anything between invocations and any of them can be retried, replaced or scaled without coordination. Writes are conditional on the state the function expected to find, which makes retries safe: a duplicate delivery from the queue re-reads, sees the work already done and exits instead of pricing the order twice.

What shipped
  • State in DynamoDB; functions hold nothing between invocations
  • Conditional writes make queue redeliveries safe
  • Any function retried, replaced or scaled without coordination
Process

Phase by phase

  1. Phase 1: Decompose

    Four steps, not one process

    Mapped the monolithic pipeline into discrete stages (intake, validation, pricing, fulfillment routing) with the contract between each written down.

    • Pipeline map
    • Stage contracts
  2. Phase 2: Rebuild

    Events instead of a cluster

    Replaced the provisioned cluster with event-triggered functions fanned out through a managed queue.

    • Function set
    • Queue topology
    • IaC definitions
  3. Phase 3: Tune

    Month-end without a scramble

    Tuned concurrency limits and cold-start behavior against the real month-end volume curve, not an average.

    • Load test results
    • Concurrency configuration
  4. Phase 4: Externalise state

    Nothing held in memory

    Moved order state into managed storage so every function stayed stateless and independently retryable.

    • State schema
    • Retry & idempotency design
Held for replay

3 of 41,184 orders today · each with its original payload and its error in plain words

Replay selectedFri 31 Jul 2026 · 23:41

Retry policy · every stage queue

5 attempts, then orders-dlq
Attempt 1On arrival
Attempt 2+8 s
Attempt 3+30 s
Attempt 4+2 min
Attempt 5+10 min
DLQ
Backoff on the message’s visibility · the rest of the queue keeps draining

Queues now

Visible · in backoff · held
validate-q000
price-q000
route-q000
dispatch-q000
orders-dlq003

Orders held

Nothing was lost · nothing was half-applied
Tanfield Trade Supplies5 of 5TTS-40022871 · ORD-260731-021907Line 7 orders SKU HP-40218, which was discontinued on 30 Jun. Nothing was priced or routed.
Oxley Build Centres5 of 5OBC/PO/118804 · ORD-260731-030466Line 3 asks for −12 units. A purchase order can't order a negative quantity.
Marram Garden Co.5 of 5MGC-2026-7731 · ORD-260731-044120The file stops after segment 26 but says it has 44. It was cut short in transit; ask for a resend.
Tanfield Trade Supplies · TTS-40022871Held at 09:14:06 on validate · ORD-260731-021907In orders-dlq
Line 7 orders SKU HP-40218, which was discontinued on 30 Jun. Nothing was priced or routed.
Original payload · kept as receivedORDERS D.96A
UNH+1+ORDERS:D:96A:UN:EAN008'
BGM+220+TTS-40022871+9'
DTM+137:20260731:102'
NAD+BY+5060188240003::9'
LIN+7++HP-40218:SA'   ← discontinued SKU
QTY+21:40'
Attempts12345
Replay after fixDownload payload Replays re-enter at intake
On screen

The retry policy of five attempts with backoff, and the three orders held for replay out of 41,184, each with its original payload kept and its error stated in plain words.

Outcome

What it carries now

−92%

Overnight compute cost

0

Month-end order backlog

0

Servers to patch

Overnight compute cost compares the month after cutover with the month before, on the same order volume. Month-end backlog is a count of orders unprocessed at the close of the peak window. Servers to patch is literal: there are none, a secondary goal that turned out to matter more than expected to the operations team.

Client name withheld under NDA. Figures are approximate, drawn from the engagement’s own reporting.

What the architecture settled

  1. 01

    Month-end was never a capacity problem. It was a coupling problem between stages.

    The cluster had been sized for month end because the stages couldn't scale apart. Once they could, the peak was three independent problems, and none of them was hard.

  2. 02

    Statelessness is what made the retries safe; without it, scaling just multiplied the failure modes.

    Retries are only safe when the function holds nothing between invocations. A stateful retry at scale multiplies the failure modes instead of absorbing them.

  3. 03

    The overnight bill going to almost nothing was a side effect of the right decomposition, not the goal.

    Paying almost nothing overnight followed from stages that scale to zero independently: a consequence of the decomposition more than its objective.

Ways of working

How the work was run

A cross-functional team of 4 worked on a fixed price basis over 10 weeks, covering Serverless architecture, Event-driven pipeline, Managed data & queues. We ran two-week increments, each one shippable, reviewed with them before it merged. Decisions were recorded as they were made, so the reasoning survived the people who made it.

Ten weeks, fixed price, with the pipeline rebuilt beside the cluster and orders mirrored through both until the outputs matched for a full month-end. Infrastructure was defined as code from the first commit, which is what made running two pipelines in parallel a configuration change instead of a project.

One purchase order, hop by hop

Rejected once, retried, priced once, and in the ERP 38.8 seconds after it arrived.

Follow a single order through intake, its queue, validation, pricing, routing and dispatch. Each hop shows what it waited on, what happened and what it wrote to the order’s state. Switch hops, or use the arrow keys once a tab is focused.

BRK-PO-770412Brackwell Hardware Ltd · 18 lines · EDIFACT ORDERS over AS2
0 s · 14:06:22.038.8 s · ERP ack
Functionorder-validate
Waits onThe partner directory
On the clock0.5–9.6 sTime at hop9.1 s
12345attempts of 5
What happened
  1. 06:22.5Attempt 1 of 5 startsRunning
  2. 06:23.1Rejected: delivery point 5060412380017 not in the partner directory yetRejected
  3. 06:23.1Message back on validate-q, visible again in 8 sWaiting
  4. 06:31.1Attempt 2 of 5 startsRunning
  5. 06:31.6Valid: delivery point found, 18 of 18 lines passDone
Order state writeAttempt 1 writes nothing · attempt 2: if status = RECEIVED → VALIDATED, v2

Why a rejection didn't hold anything up: the failed message waited on its own stage’s queue while other orders kept moving, and because the order’s state lives in storage rather than in the function, the retry simply started again from what was written. Timings per hop are illustrative; the 38.8 s total, the rejection and the retry are the traced order’s.

Architecture

From one purchase order to the ERP, with nothing kept warm

The provisioned cluster became event-triggered functions behind a managed queue and managed storage, defined as code from the first commit and run beside the old cluster until the outputs matched for a full month-end.

  1. 01 · Trigger
    EventBridge event per orderAn EDI or API purchase order raises its own event, so the pipeline starts on that order at once instead of waiting on a polling cycle.
  2. 02 · Queue
    Per-stage SQS queuesEach stage has its own queue and failure domain. A poison message parks in a dead-letter queue instead of stalling the line.
  3. 03 · Engine
    Validate, price, route in LambdaSeparate functions with their own concurrency limits. Pricing's reserved concurrency respects the vendor's rate limit, so a spike queues there.
  4. 04 · State
    Order state in DynamoDBKeyed by order and written conditionally. Functions hold nothing between invocations, so any of them can be retried or scaled without coordination.
  5. 05 · Delivery
    Dispatch to the ERPEvery stage scales to zero on its own between orders, which is why the overnight compute bill all but disappeared.
What one failure can reach

No lost orders, no double processing, no month-end backlog

A bad order parks; it doesn't stall the line

Each stage has its own queue and its own failure domain. A poison message moves to a dead-letter queue after five attempts with backoff, with its original payload kept for replay, while every other order keeps moving.

A redelivery can't price an order twice

Order state lives in DynamoDB and every write is conditional on the state the function expected. A duplicate delivery re-reads, sees the work already done and exits.

A spike queues at one step, not across the pipeline

Pricing runs under a reserved concurrency that respects the vendor's rate limit, so month-end volume waits at that step instead of triggering vendor-side throttling for everyone.

Paying all month for servers sized for three days of orders? Scope your build in 3 minutes.

Scope your build
Have a project?

Let's talk

Running a large platform, shaping a first MVP, or getting a product ready for a funding round? Tell us where you are. We'll shape the process around it, and stay with you after launch.