KAHIBARO
Discord Login Register

31.5. Shopping Cart

Overview

In an e‑commerce backend, the shopping cart connects browsing to buying. It temporarily stores what the user wants, lets them adjust quantities, and feeds data into pricing, discounts, inventory checks, and order creation.

This chapter focuses on how to design and implement the backend side of a shopping cart for a typical REST API, assuming other chapters will cover authentication, products, and orders in detail.

Core Responsibilities of a Shopping Cart

At minimum, your cart must:

You will often expose it through endpoints such as:

Later, the order creation process will typically use the current cart contents to build an order.

Cart Data Model

Choosing a Cart Ownership Model

There are two common ownership models:

ModelDescriptionProsCons
User‑based cartCart belongs to an authenticated userSimple, persists across devices, easy to linkGuests need a temporary user or token
Anonymous / session cartCart belongs to a session or device identifierWorks for guests, no signup neededHarder to merge carts when user later logs in

A practical approach:

Basic Relational Schema

A typical relational model uses two tables: carts and cart_items.

sql
CREATE TABLE carts (
    id             UUID PRIMARY KEY,
    user_id        UUID UNIQUE,           -- nullable if guest
    guest_token    VARCHAR(255) UNIQUE,   -- nullable if user
    currency       VARCHAR(3) NOT NULL,   -- e.g. 'USD'
    created_at     TIMESTAMP NOT NULL,
    updated_at     TIMESTAMP NOT NULL
);
CREATE TABLE cart_items (
    id             UUID PRIMARY KEY,
    cart_id        UUID NOT NULL REFERENCES carts(id) ON DELETE CASCADE,
    product_id     UUID NOT NULL,
    quantity       INTEGER NOT NULL,
    unit_price     NUMERIC(10, 2) NOT NULL,
    currency       VARCHAR(3) NOT NULL,
    added_at       TIMESTAMP NOT NULL
);

Key points:

Important rule: Never trust client‑side prices. Always calculate totals on the server, using prices from the database or trusted service.

Capturing More Business Data

Depending on your business rules, you may extend the model:

Do not prematurely add every field you can imagine. Start with the essentials and extend as needed.

Cart Operations

Adding Items to the Cart

A typical add‑item request:

http
POST /cart/items
Content-Type: application/json
{
  "product_id": "c0b7a50c-...",
  "quantity": 2
}

Backend workflow:

  1. Identify the cart (by authenticated user or guest token).
  2. Validate product:
    • Product exists and is active.
    • Product is purchasable (not deleted or out of catalog).
  3. Validate quantity:
    • quantity is a positive integer.
    • Optional: respects per‑product max/min.
  4. Check inventory (depending on when you reserve stock).
  5. If the product is already in the cart, either:
    • Increase existing quantity, or
    • Replace quantity with the requested one, depending on API contract.
  6. Calculate and store current unit_price.
  7. Recalculate cart totals if you store aggregate fields.

Example business logic in Python‑style pseudocode:

python
def add_item_to_cart(cart, product_id, quantity):
    if quantity <= 0:
        raise ValueError("Quantity must be positive")
    product = get_product(product_id)
    if not product or not product.is_active:
        raise ValueError("Product not available")
    # Optional inventory check
    if product.stock is not None and quantity > product.stock:
        raise ValueError("Not enough stock")
    item = find_cart_item(cart.id, product_id)
    if item:
        item.quantity += quantity
        item.unit_price = product.price  # update to current price or keep old
        save_cart_item(item)
    else:
        item = CartItem(
            cart_id=cart.id,
            product_id=product_id,
            quantity=quantity,
            unit_price=product.price,
            currency=cart.currency
        )
        save_cart_item(item)
    update_cart_totals(cart.id)
    return item

Decide and document whether adding the same product again increments or overwrites quantity.

Updating Item Quantities

Example request:

http
PATCH /cart/items/{item_id}
Content-Type: application/json
{
  "quantity": 3
}

Backend workflow:

  1. Load item by item_id and cart.
  2. If quantity == 0, either:
    • Remove the item, or
    • Reject the request. Choose one behavior and document it.
  3. Validate quantity, product state, and inventory.
  4. Update the item and recalculate totals.

You can also support partial updates like:

http
PATCH /cart/items/{item_id}
{
  "increment_by": 1
}

But that requires a carefully defined API, especially in concurrent environments.

Removing Items

Example request:

http
DELETE /cart/items/{item_id}

Steps:

  1. Ensure the item belongs to the current cart.
  2. Delete the item.
  3. Update cart totals or mark cart as empty if no items remain.

You can also support:

http
DELETE /cart/items

To clear the entire cart.

Retrieving the Cart

Typical endpoint:

http
GET /cart

Example response:

json
{
  "id": "9d6c0d21-...",
  "currency": "USD",
  "items": [
    {
      "id": "item-1",
      "product_id": "prod-1",
      "name": "Red T‑Shirt",
      "quantity": 2,
      "unit_price": 19.99,
      "line_total": 39.98
    },
    {
      "id": "item-2",
      "product_id": "prod-2",
      "name": "Blue Jeans",
      "quantity": 1,
      "unit_price": 49.50,
      "line_total": 49.50
    }
  ],
  "subtotal": 89.48,
  "discounts": 5.00,
  "tax": 7.56,
  "shipping": 0.00,
  "total": 92.04
}

Separation of concerns:

Calculating Totals

Basic Price Calculations

The simplest model:

Key pricing formula:
$$total = \left(\sum_i quantity_i \times unit\_price_i\right) - discounts + tax + shipping$$
Always compute this on the server.

A small example:

ItemQuantityUnit priceLine total
Red T‑Shirt219.99$2 \times 19.99 = 39.98$
Blue Jeans149.50$1 \times 49.50 = 49.50$

Subtotal:

$$subtotal = 39.98 + 49.50 = 89.48$$

If discount is $5.00, tax is $7.56, shipping is $0.00:

$$total = 89.48 - 5.00 + 7.56 + 0 = 92.04$$

When to Recalculate

You can recalculate totals:

Most applications recalculate automatically with every write operation so GET /cart always returns up‑to‑date totals.

Handling Price Changes

Two typical strategies when product prices change:

StrategyBehaviorProsCons
Sticky cart pricesKeep unit_price as stored in cart_itemsPredictable for userMay sell at outdated price
Dynamic cart pricesRefresh unit_price from product table periodicallyAlways uses current priceUser total can change unexpectedly

A common compromise:

Implementation idea:

  1. Store unit_price and price_checked_at per item.
  2. When user goes to checkout, compare with current product prices.
  3. If difference is above a threshold, require user confirmation.

Inventory and Availability

When to Reserve Inventory

You must decide when to reduce stock:

Reserving on add to cart can lead to "stock locked in carts" that never turn into orders. Most systems:

Example soft check on add:

python
if product.stock is not None and quantity > product.stock:
    # Limit the item quantity to available stock
    quantity = product.stock

When user proceeds to checkout, do a final inventory validation.

Handling Out‑of‑Stock Items in Cart

Several strategies if an item becomes unavailable:

Data model addition:

sql
ALTER TABLE cart_items
ADD COLUMN is_available BOOLEAN NOT NULL DEFAULT TRUE;

During cart retrieval:

python
def refresh_cart_item_availability(cart):
    for item in cart.items:
        product = get_product(item.product_id)
        item.is_available = bool(product and product.is_active and product.stock > 0)

Frontends can then disable checkout for unavailable items.

Guest Carts and Merging

Identifying Guest Carts

Typical approach:

Example cookie:

http
Set-Cookie: cart_token=bd4c...; Path=/; Max-Age=2592000; HttpOnly; SameSite=Lax

Backend flow:

  1. Check authenticated user.
  2. If no user, look for cart_token.
  3. If none, create a new cart and return a token.

Merging Carts on Login

When a guest with a cart logs in and they also have an existing user cart, you must decide how to merge.

Strategies:

Example merge logic:

python
def merge_carts(user_cart, guest_cart):
    for guest_item in guest_cart.items:
        user_item = find_cart_item(user_cart.id, guest_item.product_id)
        if user_item:
            user_item.quantity += guest_item.quantity
            save_cart_item(user_item)
        else:
            move_item_to_cart(guest_item, user_cart.id)
    delete_cart(guest_cart.id)
    update_cart_totals(user_cart.id)

Be explicit about how you handle:

Currency and Tax Considerations

Single Currency vs Multi‑Currency Carts

Simplest approach: one currency per store and cart. The currency field is fixed.

For multi‑currency:

If a user changes currency, often you create a new cart in the new currency and discard or convert the old one.

Basic Tax Handling

Tax rules are complex and vary across regions, so only simple logic should be inside the cart service. For example:

$$tax = subtotal \times 0.10$$

The cart might delegate tax calculation:

python
tax = tax_service.calculate_tax(
    items=cart.items,
    shipping_address=user.shipping_address
)

You then store tax on the cart or compute it on the fly.

API Design for Shopping Cart

Example REST Endpoints

A basic RESTful API design:

MethodPathDescription
GET/cartGet current cart
POST/cart/itemsAdd item to cart
PATCH/cart/items/{id}Update item quantity or options
DELETE/cart/items/{id}Remove item
DELETE/cart/itemsClear cart

Sample OpenAPI‑style schema for a cart item:

yaml
CartItem:
  type: object
  properties:
    id:
      type: string
      format: uuid
    product_id:
      type: string
      format: uuid
    quantity:
      type: integer
      minimum: 1
    unit_price:
      type: number
      format: float
    currency:
      type: string
      minLength: 3
      maxLength: 3
    line_total:
      type: number
      format: float

Example FastAPI‑Style Models

You might implement these models in Python with Pydantic:

python
from pydantic import BaseModel, conint, constr
from typing import List
class CartItemCreate(BaseModel):
    product_id: str
    quantity: conint(gt=0)
class CartItemRead(BaseModel):
    id: str
    product_id: str
    quantity: int
    unit_price: float
    currency: constr(min_length=3, max_length=3)
    line_total: float
class CartRead(BaseModel):
    id: str
    currency: constr(min_length=3, max_length=3)
    items: List[CartItemRead]
    subtotal: float
    discounts: float
    tax: float
    shipping: float
    total: float

And endpoints:

python
from fastapi import APIRouter, Depends
router = APIRouter(prefix="/cart")
@router.get("", response_model=CartRead)
def get_cart(current_cart = Depends(get_current_cart)):
    return build_cart_response(current_cart)
@router.post("/items", response_model=CartItemRead)
def add_cart_item(payload: CartItemCreate,
                  current_cart = Depends(get_or_create_cart)):
    item = add_item_to_cart(current_cart, payload.product_id, payload.quantity)
    return build_cart_item_response(item)

Consistency and Concurrency

Dealing with Concurrent Updates

Examples of conflicts:

Common safeguards:

Simple optimistic locking schema:

sql
ALTER TABLE carts
ADD COLUMN version INTEGER NOT NULL DEFAULT 0;

Every update:

  1. Read current version.
  2. Update with WHERE id = ? AND version = ?.
  3. If no rows are updated, someone else changed the cart. Client must reload.

Cart Expiration

Carts can grow without limit if you never remove them. Simple approach:

Example field:

sql
ALTER TABLE carts
ADD COLUMN expires_at TIMESTAMP;

Expiration strategy:

Putting It All Together

A typical cart lifecycle in your e‑commerce backend:

  1. User visits the site.
    • Backend looks for user or cart_token.
    • Returns existing cart or creates new one.
  2. User adds items.
    • Backend validates product and quantity.
    • Saves item, recalculates totals.
  3. User updates or removes items.
    • Backend applies changes atomically.
    • Prices and availability are rechecked as needed.
  4. User logs in (if previously a guest).
    • Backend merges guest cart into user cart.
  5. User proceeds to checkout.
    • Backend validates prices, stock, discounts.
    • Cart contents are transformed into an order.

Throughout this process, the cart acts as a temporary, consistent, server‑trusted representation of the user’s purchase intent, ready to be turned into an order in the next steps of your e‑commerce backend.

Views: 8

Comments

Please login to add a comment.

Don't have an account? Register now!