KAHIBARO
Discord Login Register

31.6. Orders

Understanding Orders in an E‑Commerce Backend

In an e‑commerce system, orders are the heart of the business. Everything else, such as products, inventory, payments, and users, exists so that customers can place and receive orders. In this chapter you will focus on how to design and implement the order part of the backend, and how it connects to the rest of the system.

You will see examples, but the exact technology stack is not important here. The ideas apply whether you use FastAPI, Django, Node.js, or any other backend framework.

Key idea: An order is a snapshot of what the customer agreed to buy, at specific prices, under specific conditions, at a specific time.

If you remember this, many design decisions become easier.


The Order Lifecycle

An order moves through a series of states from the moment the customer confirms the purchase until the order is delivered or canceled. You can think of this as a finite state machine.

Typical states:

StateDescription
PENDINGCreated but not yet paid
PAIDPayment confirmed
PROCESSINGBeing prepared for shipment
SHIPPEDHanded over to logistics / carrier
DELIVEREDConfirmed delivered to customer
CANCELLEDCanceled before shipment
REFUNDEDFully refunded (optionally after being paid or shipped)

You can start with fewer states, for example only PENDING, PAID, CANCELLED, COMPLETED, and expand later.

Example state transitions:

You typically enforce which transitions are allowed. For example, you might not allow going from DELIVERED back to PENDING.

Rule: Only allow valid state transitions and always record who or what triggered the change (user, admin, system).

You can store order events in a separate order_events table to have an audit trail:


idorder_idfrom_statusto_statuschanged_bychanged_atnote
142NULLPENDINGsystem2026-01-01 10:00order created from cart
242PENDINGPAIDsystem2026-01-01 10:01payment intent 123 success

Order vs Cart

The cart and the order are related but not the same thing.

Typical flow:

  1. User adds items to a cart.
  2. User updates quantities, applies discount codes.
  3. User fills in shipping address and chooses shipping method.
  4. User clicks "Place order".
  5. Backend:
    • Validates stock.
    • Freezes prices and discounts.
    • Creates an order.
    • Starts payment process.

After step 5, you should treat the order as an independent record, not just a reference to the cart.

Rule: Do not depend on the cart to reconstruct an order. Copy all relevant data from cart and products into the order and order items.

This means that when the customer or admin opens the order page months later, they see exactly what was purchased, even if product prices or names have changed.


Order Data Model

At minimum, you will have two main tables (or collections):

You can have more tables for shipping, payments, etc., but these two are the core.

Example `orders` table

ColumnTypeDescription
idUUID / bigint PKOrder identifier
user_idFK to usersCustomer who placed the order
statusenum / textPENDING, PAID, ...
total_items_pricenumericSum of item line totals before discounts
discount_totalnumericSum of applied discounts
shipping_pricenumericShipping cost charged
tax_totalnumericTotal tax for this order
grand_totalnumericFinal amount to pay: items + shipping + tax - discounts
currencytextE.g. USD, EUR
shipping_address_jsonjsonb / textSnapshot of shipping address
billing_address_jsonjsonb / textSnapshot of billing address
shipping_methodtextE.g. standard, express
payment_statusenum / textPENDING, PAID, FAILED, REFUNDED
created_attimestampWhen the order was created
updated_attimestampLast update time

Here, addresses are stored as JSON snapshots so that if the user changes their default address in their profile later, the order still shows the original address.

Example `order_items` table

ColumnTypeDescription
idUUID / bigint PKOrder item id
order_idFK to ordersWhich order this line belongs to
product_idFK to productsProduct reference (for internal usage and analytics)
product_nametextCopy of the name at order time
skutextStock keeping unit or variant id
unit_pricenumericPrice per unit at order time (before item-specific discount)
quantityintegerNumber of units ordered
discount_amountnumericTotal discount applied to this line
tax_amountnumericTotal tax for this line
line_totalnumericFinal line total = unit_price * quantity - discount + tax

You may also store:

So that order details can be rendered even if the product is later removed or changed.

Formula: For each order item, a typical calculation is
$$\text{line\_total} = \text{unit\_price} \times \text{quantity} - \text{discount\_amount} + \text{tax\_amount}$$

At the order level you usually have:

$$\text{grand\_total} = \text{total\_items\_price} - \text{discount\_total} + \text{shipping\_price} + \text{tax\_total}$$


Creating an Order from a Cart

When the user clicks "Place order", you typically have:

A simplified flow in pseudocode:

python
def create_order_from_cart(user_id: int, cart_id: int) -> Order:
    cart = get_cart_for_user(user_id, cart_id)
    if not cart.items:
        raise EmptyCartError()
    # Re-validate data to avoid tampering
    products = load_products([item.product_id for item in cart.items])
    # Begin database transaction
    with db.transaction():
        order = create_order_record(
            user_id=user_id,
            status="PENDING",
            shipping_address=cart.shipping_address,
            billing_address=cart.billing_address,
            shipping_method=cart.shipping_method,
            currency="USD",
        )
        total_items_price = 0
        total_tax = 0
        for cart_item in cart.items:
            product = products[cart_item.product_id]
            # Use backend trusted price
            unit_price = product.price
            # Example: simple tax calculation (10 %)
            tax_rate = 0.10
            line_subtotal = unit_price * cart_item.quantity
            line_tax = line_subtotal * tax_rate
            create_order_item(
                order_id=order.id,
                product_id=product.id,
                product_name=product.name,
                sku=product.sku,
                unit_price=unit_price,
                quantity=cart_item.quantity,
                tax_amount=line_tax,
                line_total=line_subtotal + line_tax,
            )
            total_items_price += line_subtotal
            total_tax += line_tax
        # Apply discounts, coupons, shipping, etc.
        discount_total = calculate_discounts(order, cart, total_items_price)
        shipping_price = calculate_shipping(order, cart)
        grand_total = total_items_price - discount_total + shipping_price + total_tax
        update_order_totals(
            order_id=order.id,
            total_items_price=total_items_price,
            discount_total=discount_total,
            shipping_price=shipping_price,
            tax_total=total_tax,
            grand_total=grand_total,
        )
        # Optionally clear the cart
        clear_cart(cart.id)
        return get_order(order.id)

Important checks during creation:

Orders and Inventory

Orders and inventory are tightly connected. Inventory is described in a separate chapter, so here focus only on how orders should interact with it.

There are two common moments when you adjust stock:

  1. When the order is placed (reservation).
  2. When the order is shipped (final deduction).

A simple beginner friendly approach is:

Example logic:

python
def mark_order_as_paid(order_id: int):
    order = get_order(order_id)
    if order.status != "PENDING":
        raise InvalidState("Order must be pending to be paid")
    with db.transaction():
        # Update status
        update_order_status(order_id, "PAID")
        # Decrease inventory
        for item in order.items:
            decrease_stock(item.sku, item.quantity)

If you want to support preorders or reservations, you might:

When canceling an unpaid order you might release reserved stock. When refunding a paid order you might increase stock only if the items are returned.

Rule: Always update inventory in the same transaction when you update the order status that affects stock.

This helps you avoid inconsistent states where an order says "PAID" but inventory was not updated.


Orders and Payments

Payments are complex and have their own chapter, but here is how orders usually connect to them.

You normally have a payments table that references the orders table.

Example payments table:

ColumnTypeDescription
idUUID / bigintPayment id
order_idFK to ordersWhich order this payment belongs to
providertextE.g. stripe, paypal
provider_idtextProvider specific payment identifier
amountnumericAmount that the provider is charging
currencytextShould match order currency
statustextPENDING, SUCCEEDED, FAILED, REFUNDED
created_attimestampWhen the payment was initiated
updated_attimestampLast update

Basic flow:

  1. Create order with status PENDING.
  2. Create payment record with PENDING.
  3. Redirect user or use payment widget.
  4. Payment provider sends webhook:
    • If success: mark payment SUCCEEDED and order PAID.
    • If failure: mark payment FAILED and maybe cancel order after some time.

Example webhook handler:

python
def handle_payment_webhook(payload: dict):
    provider_id = payload["payment_intent_id"]
    status = payload["status"]
    payment = get_payment_by_provider_id(provider_id)
    order = get_order(payment.order_id)
    with db.transaction():
        update_payment_status(payment.id, status)
        if status == "SUCCEEDED" and order.status == "PENDING":
            update_order_status(order.id, "PAID")
            decrease_stock_for_order(order.id)
        elif status == "FAILED" and order.status == "PENDING":
            update_order_status(order.id, "CANCELLED")

You should always verify webhook signatures and not trust client side reports of success.

Rule: Order payment status must be updated only by trusted server side events, typically payment provider webhooks, not by client side calls.


Order APIs

For the e‑commerce backend project, you will typically implement API endpoints to:

Example public endpoints

MethodPathDescription
POST/ordersCreate an order from current cart
GET/ordersList current user’s orders
GET/orders/{id}Get details of a specific order
POST/orders/{id}/cancelRequest cancellation

You must ensure that users can only see and modify their own orders.

Example authorization check in pseudocode:

python
def get_order_for_user(order_id: int, user_id: int) -> Order:
    order = get_order(order_id)
    if order.user_id != user_id:
        raise ForbiddenError()
    return order

Example admin endpoints

MethodPathDescription
GET/admin/ordersList all orders (with filters)
GET/admin/orders/{id}Get any order details
POST/admin/orders/{id}/statusChange order status (processing, shipped, etc.)

Admin endpoints should be protected by role based authorization.


Canceling and Refunding Orders

Cancellation and refunds affect:

You need clear rules. For example:

Example cancellation logic:

python
def cancel_order_by_user(order_id: int, user_id: int):
    order = get_order_for_user(order_id, user_id)
    if order.status not in ["PENDING", "PAID"]:
        raise InvalidState("Order cannot be canceled")
    with db.transaction():
        update_order_status(order.id, "CANCELLED")
        if order.status == "PAID":
            # Start refund flow in payment provider asynchronously
            enqueue_refund_job(order.id)
            # Optionally adjust stock
            increase_stock_for_order(order.id)

You can also support partial refunds where only some items are refunded. In that case you might add a refunded_quantity and refunded_amount fields in order_items and adjust totals accordingly.


Order History, Tracking, and Notifications

Customers expect to track order progress. You can implement this with:

Example order_events table:

ColumnDescription
idEvent id
order_idOrder reference
typeE.g. status_changed, note_added
dataJSON with details
created_byUser id or system
created_atTimestamp

When status changes from PAID to SHIPPED:

  1. Insert event status_changed with from=PAID, to=SHIPPED.
  2. Send email: "Your order has been shipped".
  3. Possibly include tracking number in the event data and email.

On the front end, "Order History" page can show a timeline built from order_events.


Performance, Pagination, and Filtering

Orders can grow to large numbers over time, so you need to think about:

Common queries:

You should:

Example listing endpoint for current user's orders:

GET /orders?limit=20&offset=0

Query example in SQL:

sql
SELECT *
FROM orders
WHERE user_id = :user_id
ORDER BY created_at DESC
LIMIT :limit OFFSET :offset;

For admin:

GET /admin/orders?status=PAID&from=2026-01-01&to=2026-01-31

And then filter by these parameters in the query.


Putting It Together in the E‑Commerce Project

Within the context of the full e‑commerce backend project:

Typical sequence when a customer checks out:

  1. User logs in and fills cart.
  2. User goes to checkout, provides shipping address, chooses shipping method.
  3. Backend:
    • Creates orders and order_items from cart in PENDING state.
    • Starts payment and returns payment session info.
  4. User pays on the provider page or in widget.
  5. Provider sends webhook:
    • Backend marks payment as SUCCEEDED.
    • Backend marks order as PAID, updates inventory.
    • Background job sends "Order confirmation" email.
  6. Warehouse processes paid orders:
    • Admin system or worker moves status to PROCESSING and then SHIPPED.
    • When shipped, tracking email is sent.
  7. After delivery, order is marked DELIVERED.

By keeping the concepts in this chapter in mind, you can build an order system that is reliable, auditable, and easy to extend.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!