Skip to content
← All patterns

commerce

Commerce Capstone

How does the whole shop fit together?

Four entity types, one table, no joins and no scans.

The destination the rest of the library builds towards: customers, orders, line items and products in a single table, with a generic key carrying entity prefixes and one overloaded index answering the cross-cutting reads. Every technique here has appeared alone somewhere else.

The model

Order

An order header, in its customer's partition.

pk
CUSTOMER#<customerId>
sk
ORDER#<placedOn>#<orderId>

Attributes: status (S), totalPence (N), placedAt (N), GSI1PK (S), GSI1SK (S)

LineItem

One line of an order, in the same partition as its header.

pk
CUSTOMER#<customerId>
sk
ORDER#<placedOn>#<orderId>#LINE#<lineNo>

Attributes: sku (S), quantity (N), pricePence (N)

Customer

The customer header, sorting ahead of their orders.

pk
CUSTOMER#<customerId>
sk
CUSTOMER#<customerId>

Attributes: name (S), email (S)

Product

A product, in its own partition and in the shared index by category.

pk
PRODUCT#<sku>
sk
PRODUCT#<sku>

Attributes: title (S), pricePence (N), GSI1PK (S), GSI1SK (S)

Access patterns

  • QueryEverything about a customer

    The customer, their orders and every line, in one read.

  • Query · GSI1Orders awaiting dispatch

    Every order in one status, across all customers, oldest first.

  • GetItemRead an order header

    One order, without its lines.

Design notes

The key is generic so the items do not have to bewhy

PK and SK carry no domain meaning at all - they hold CUSTOMER#, ORDER#, PRODUCT# and whatever else the design needs later. That is what allows four entity types in one table: the key describes position, and the prefix describes what lives there. A table named per entity cannot do this, and needs a join to answer the first access pattern here.

One partition holds an entire order historywhy

The customer header, every order and every line share CUSTOMER#<id>, and the sort key nests lines under their order. So the order-history page is one Query: no join, no second round trip, and the rows arrive already grouped because an order sorts immediately before its own lines. The date sits in the key ahead of the order id, exactly as the Customer Orders model has it, so the collection is also in chronological order.

Sparse and overloaded at the same timewhy

Of the orders, only those awaiting dispatch carry GSI1PK, so the picking list is a sparse index - a delivered order leaves it by having its index keys removed. That same index also holds products under CATEGORY#, so one index serves the warehouse and the storefront. Both techniques appear alone elsewhere in this library; here they are the same index. The console runs one index read per model, so the category question is in the table to be seen rather than pressed.

What this costs to work withtrade-off

Nothing about this table is self-describing. A row's meaning lives in its prefix, a query has to know the key convention, and adding an access pattern usually means changing what is written rather than adding an index. That is the trade single-table design makes: the reads get cheap and fixed, and the design gets harder to change than a schema you can add a column to.

Start here lasttrade-off

Every technique on this page is one of the simpler models in this library, combined: item collections, a type-discriminating sort key, a sparse index, an overloaded index. Working out the access patterns first and the key design second is what makes the combination tractable - done the other way round, this table is impossible to get right.

Taught in the course

commerce-capstoneDynamoDB workbench
Ready to run
Explore an access patternSelect to load & run

The customer, their orders and every line, in one read.

Request
Execute against the local Dynoxide engine
ReturnedFiltered outChanged
PK(pk)
SK(sk)
GSI1PK
GSI1SK
placedAt
status
totalPence
pricePence
quantity
sku
email
name
title
CUSTOMER#c-9001
CUSTOMER#c-9001S
—
—
—
—
—
—
—
—
isla@example.comS
Isla BeckettS
—
CUSTOMER#c-9001
ORDER#2026-08-27#o-69880S
—
—
1787788800N
DELIVEREDS
12900N
—
—
—
—
—
—
CUSTOMER#c-9001
ORDER#2026-09-21#o-70112S
STATUS#AWAITING_DISPATCHS
2026-09-21T00:00:00Z#o-70112S
1789948800N
AWAITING_DISPATCHS
4890N
—
—
—
—
—
—
CUSTOMER#c-9001
ORDER#2026-09-21#o-70112#LINE#01S
—
—
—
—
—
1745N
2N
sku-1180S
—
—
—
CUSTOMER#c-9001
ORDER#2026-09-21#o-70112#LINE#02S
—
—
—
—
—
1400N
1N
sku-4127S
—
—
—
CUSTOMER#c-9002
CUSTOMER#c-9002S
—
—
—
—
—
—
—
—
owen@example.comS
Owen TraskS
—
CUSTOMER#c-9002
ORDER#2026-09-21#o-70140S
STATUS#AWAITING_DISPATCHS
2026-09-21T14:24:00Z#o-70140S
1790000640N
AWAITING_DISPATCHS
23400N
—
—
—
—
—
—
CUSTOMER#c-9002
ORDER#2026-09-21#o-70140#LINE#01S
—
—
—
—
—
1400N
6N
sku-4127S
—
—
—
PRODUCT#sku-1180
PRODUCT#sku-1180S
CATEGORY#kitchenS
Enamel MugS
—
—
—
1745N
—
—
—
—
Enamel MugS
PRODUCT#sku-4127
PRODUCT#sku-4127S
CATEGORY#kitchenS
CafetiereS
—
—
—
1400N
—
—
—
—
CafetiereS
Awaiting request
Your next query starts here.

Choose an access pattern above, or build your own request. See what comes back and what it costs.

Write transactions, streams, tags and TTL are among the operations this browser build leaves out. dynoxide's native build has them.