“Add an Order Service” sounds like an architectural decision. Until someone defines what that service owns and how callers interact with it, it is only a label.
This lesson turns an online store into explicit responsibilities and contracts. You will design a checkout interface, decide who may change each kind of data, and see why those boundaries can live inside a modular monolith.
In Define System Boundaries with an Online Store, Fieldnote promised that accepted orders are paid, stored, and allocated stock. If you are starting here, that promise is the only prerequisite beyond basic HTTP and transaction concepts. The full route is Online Store Architecture: From Boundaries to Reliable Orders.
Give each responsibility an owner
Fieldnote has one backend application and one relational database. Its code is organized into modules:
| Module | Owns | Contract other modules use |
|---|---|---|
| Catalog and Cart | Products, current prices, cart contents and version | Validate a versioned cart and produce trusted purchase inputs |
| Inventory | Available quantities and reservations | Hold, commit, or release stock |
| Orders | Order record and permitted state transitions | Create pending order, accept it, or cancel it |
| Payments | Provider mapping and payment-attempt identity | Execute or resolve one logical payment attempt |
| Checkout | Coordination and retry identity | Prepare and complete a purchase attempt |
| Fulfillment | Packing tasks | Create a task for an accepted order |
| Notifications | Receipt jobs and delivery progress | Schedule and send the receipt |
These are services in the sense of capabilities exposed through interfaces. They are not seven network processes. A call from Checkout to Inventory can be an ordinary function call.
Give modules control over their writes even when tables share a database. Checkout asks Inventory to reserve notebooks; it does not update a stock counter directly. That rule puts stock invariants in one place. A shared local transaction can still coordinate several module operations through an application unit of work.
This view describes components inside the backend. It should not present each module as a separate deployable container. The C4 component diagram guidance distinguishes that closer view from the system and container views.
Live model · Architecture view
Read the contracts inside one backend
Catalog and Cart, Checkout, Inventory, Orders, Payments, Fulfillment, and Notifications are modules inside the same backend codebase. Checkout coordinates them through local interfaces. Inventory owns stock writes, and a shared relational database supports local transactions without requiring a network service per module.
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.
Make a contract carry meaning
A function named reserve() needs more than a parameter list. For Fieldnote, define:
holdStock(orderId, items) -> reservationId | OutOfStock
Its meaning is: hold every requested item or hold none; held units are unavailable to competing checkouts; repeating the operation for the same order does not hold them again. The caller must supply positive quantities, and Inventory must enforce the rules under concurrent requests.
For two shoppers racing to buy the final notebook, a “read availability, then subtract later” implementation is insufficient. Fieldnote will coordinate the check and reservation in a local database transaction. The mechanism is separate from the business contract; lesson three will show where it belongs.
Define failure meaning too. OutOfStock is a known business outcome. “Database unavailable” is an operational failure. Treating both as an empty result would make support and recovery much harder.
Design the checkout API around one intent
The browser submits this illustrative request:
POST /checkouts
Idempotency-Key: checkout-key-104
Content-Type: application/json
{
"cartId": "cart_17",
"cartVersion": 7,
"paymentMethodRef": "method_example"
}
The authenticated caller is part of the operation's scope. The cart version covers the quoted quantities and prices; a change requires the shopper to review the updated cart. The backend verifies that version, computes the trusted amount, and freezes it on a pending order. The browser does not choose the charge amount.
On confirmed completion, return 201 with orderId, status: ACCEPTED, and an authorized status URL. If a durable attempt exists but payment remains unresolved, return 202 with checkoutId, orderId, status: PENDING_PAYMENT, and a status URL. 202 communicates incomplete processing, consistent with HTTP semantics; it is not an acceptance receipt for the business order.
Fieldnote uses 409 CART_CHANGED or 409 OUT_OF_STOCK for purchase conflicts, and 422 PAYMENT_DECLINED for a definitive payment decline. These application mappings are choices, not universal payment API conventions.
The same key and equivalent input identify the same attempt. A changed input with that key produces 409 IDEMPOTENCY_CONFLICT. Matching retries read or resume that attempt, while GET /checkout-attempts/{checkoutId} reports current status. An unresolved attempt can later become accepted; the status resource exists to show that change.
The retry key is useful only if it is enforced durably. We will develop its concurrency and retention rules in Handle Checkout Failures with Idempotency and Explicit Trade-offs.
Live model · Sequence view
See the contracts meet in one checkout
The shared checkout sequence connects the contracts discussed above. Preparation commits before the remote payment request. The second local transaction accepts a paid, allocated order; decline and unknown payment outcomes follow different branches.
Open this viewer on its own ↗ if the frame is unavailable. The explanation remains readable without the viewer.
Choose deployment boundaries separately
A modular monolith gives Fieldnote inexpensive calls and a local transaction across order preparation and stock reservation. The cost is a shared release and weaker runtime isolation: a faulty module can affect the whole process.
Separate deployments may become justified if a capability needs independent releases, a distinct security boundary, or substantially different scaling. They also add network failures, operational work, and cross-service consistency problems. Martin Fowler's Monolith First describes the cost of choosing distributed boundaries before those boundaries are understood; it is an experience-based argument, not a proof that every product should begin the same way.
For this shop, keep the modules together. Record an extraction trigger, such as payment-adapter isolation becoming an explicit operational requirement, instead of presenting future growth as evidence that every module needs its own service today. An Adapter can isolate a provider contract inside the existing process before a separate deployment is justified.
Try it in Unograph
Open the Fieldnote architecture at the backend level. Add the seven modules inside one application boundary and one shared database outside that application boundary but inside the owned system. Label Checkout's calls with the contracts they use. Add the stock invariant as a note.
Your review question is concrete: can Orders change inventory without calling Inventory's interface? If the diagram suggests yes, clarify ownership before adding more boxes.
Next, Trace a Synchronous Checkout with a Sequence Diagram puts these contracts in time order. The previous lesson, Define System Boundaries with an Online Store, explains the outer scope; the route hub connects all five views.