A structural diagram can show that Checkout calls Payments. It cannot show whether the application holds a stock lock while waiting for the payment provider, or what happens when payment succeeds just before the connection breaks.
This lesson traces one online-store checkout from request to response. You will place database commits, distinguish local calls from a remote call, and identify the point at which Fieldnote can truthfully say “order accepted.”
Design Services and Contracts Without Splitting the Monolith defines the modules and API. To read this lesson independently, assume one modular backend, one relational database, and an external payment provider. An accepted order must be paid, saved, and allocated stock. The wider route is Online Store Architecture: From Boundaries to Reliable Orders.
Choose one concrete trace
The shopper submits cart cart_17, version 7, with two FN-A5 notebooks. The server computes a frozen illustrative total of 2400 minor units in USD. Tax and shipping computation are outside this example.
The browser supplies checkout-key-104 as the retry identity. The backend will create checkout_104, order_104, reservation res_104, and payment attempt pay_104.
Put these lifelines in the sequence diagram: Browser, Checkout, Catalog and Cart, Inventory, Orders, Payments, Store DB, and Payment Provider. The module lifelines are local code in the same backend. Only the provider call crosses that backend's external network boundary.
The main trace assumes the provider returns an immediately successful payment. Real payment flows may require customer action or remain in processing, as illustrated by Stripe's payment lifecycle. Those need additional product states and interaction paths; they are outside this trace. A response from a provider is not automatically a completed payment.
Prepare the purchase in a short local transaction
After authentication and basic validation, Checkout begins transaction A:
- Claim the caller's checkout key and store a fingerprint of its input. A uniqueness constraint resolves competing requests for that identity.
- Validate the cart version and calculate the trusted purchase snapshot.
- Ask Inventory to hold the two notebooks, checking available stock atomically.
- Ask Orders to create
order_104asPENDING_PAYMENT. - Persist
pay_104and its stable provider operation key,fieldnote/pay_104. - Commit the preparation, including the checkout record and reservation.
If any local step fails, the transaction rolls back. No charge has been requested. A stale cart or unavailable notebook can therefore return a known conflict without needing to undo a payment.
For a relational implementation, locking the relevant stock rows is one way to coordinate competing reservations. PostgreSQL's row-lock documentation explains how conflicting operations wait until the transaction ends. Acquire multiple SKU rows in a consistent order and handle transaction failures; a box labeled “transaction” does not prevent every deadlock.
The reservation is durable after commit. It reduces available stock while payment is in flight. It does not require keeping a database lock open during the remote call.
Make the remote call after commit
Checkout asks Payments to execute pay_104. Payments sends the frozen amount and the persisted operation key to the provider. The database transaction has already ended.
This ordering avoids making other shoppers wait on a database lock while a remote dependency is slow. Its cost is an intermediate state: an order and stock hold exist before payment is resolved. That state must be recoverable after a crash.
Persisting the provider key before the call gives recovery a durable identity to investigate or safely resume. The actual provider must support suitable idempotency or lookup behavior. A made-up adapter interface cannot supply a guarantee the provider does not offer.
Accept the order in a second transaction
When Payments obtains authoritative success, Checkout begins transaction B. It records that success, commits the stock allocation through Inventory, moves Orders to ACCEPTED, inserts an OrderAccepted event in the database outbox, and saves the terminal checkout result. Then it commits.
Use a state guard so an API response handler, reconciler, and verified provider notification cannot each accept the same order independently. The first valid transition performs the work. Later attempts observe the completed result. Use controlled interleavings as acceptance checks when asking a coding agent to implement that guard.
Only after transaction B commits does the browser receive 201 with status: ACCEPTED. Payment success alone is insufficient: before the local commit, Fieldnote has not yet durably recorded the complete acceptance promise.
The outbox entry matters because the application can crash immediately after that commit. Lesson four will use it to recover packing and receipt work without delaying the response.
Live model · Sequence view
Trace preparation, payment, and acceptance
Transaction A saves the pending order, stock hold, checkout identity, and payment-attempt key. The provider call occurs after that commit, with no database lock held across the network. Verified payment success enables transaction B to record acceptance, allocate stock, and append the outbox event before the browser receives success.
Explore scenarios in this model
- Architecture view
- Checkout: preparation, payment and acceptance
- Accepted order: durable packing and receipt work
- Payment timeout: same identity, authoritative recovery
These links open the same public model in a selected view. They do not change its source.
Open this viewer on its own ↗ if the frame is unavailable. The explanation remains readable without the viewer.
Exploration is local to your viewer. Reload or Reset view returns to the source layout.
Add the two branches that change the meaning
For a definitive decline, a local transaction records the decline, releases the hold, cancels the pending order, and stores the terminal checkout result. No OrderAccepted event is created.
For a timeout or unknown provider outcome, keep the order pending. The provider may have performed the payment even though Fieldnote did not receive the response. AWS's retry guidance explicitly warns that a failed or timed-out call can already have produced side effects.
If Fieldnote can respond, return 202 with the checkout status URL and arrange reconciliation. If the backend crashes first, the browser may get no response at all; its matching retry still identifies the saved attempt. Do not release the stock merely because a local timer expired, and do not allocate a new payment key to “try again.”
Try it in Unograph
Create the sequence above and mark the two local transactions as separate groups. Place the provider request between them. Add alternative branches for decline and unknown outcome. Label returns with meaningful results, including pending status rather than a generic error.
Trace a crash after provider success but before transaction B. A good diagram leaves a durable pending order, reservation, and payment identity from which a reconciler can recover. If your diagram has no such evidence, move preparation before the remote call.
Next, Process Orders Asynchronously Without Losing Work follows the committed acceptance event. The previous services and contracts lesson explains the lifelines, and the route hub connects the complete journey.