Skip to content
← All patterns

field-service

Work Orders

Which jobs need attention?

Several query dimensions packed into one sort key, with no index.

Field-service orders that need filtering by status and by date. Rather than adding an index for the combination, both dimensions go into the sort key in the order they are narrowed - status first, then date - so one Query answers what a secondary index would otherwise be built for.

The model

WorkOrder

One job, keyed by the dimensions it is searched on.

pk
REGION#<regionId>
sk
STATUS#<status>#<scheduledOn>#<orderId>

Attributes: customer (S), engineer (S), scheduledAt (N)

Access patterns

  • QueryOrders in a region

    A region's jobs, grouped by status and ordered by date within each.

  • DeleteItemClose an order

    Move a job to DONE, which means writing it at a new key and removing the old one.

  • GetItemFetch one order

    A single work order by its full key.

Design notes

The order of the dimensions is the designwhy

Status comes before date because that is the order the reads narrow: always one status, sometimes a date range within it. Reversed, a date-first key could not answer "open jobs" without reading every status on every day. A composite sort key only serves reads that narrow left to right, and choosing that order is the whole decision.

A date range needs a bound above every idtrade-off

The key ends in an order id, so STATUS#OPEN#<date>#w-2314 sorts after the bare STATUS#OPEN#<date>. A range closing on that date therefore has to reach past it - BETWEEN "STATUS#OPEN#<from>" AND "STATUS#OPEN#<to>#\uFFFF" - or the last day silently returns nothing. Appending a discriminator to a sort key is what makes rows unique; it is also what makes the upper bound of every range awkward.

This is the index you did not buildtrade-off

A secondary index on status plus date would answer the same question and cost a second copy of every item, written on every update. The composite key gets there with one copy and no index, and the price is that the key can only be read in that one order.

What it costs when a job closestrade-off

Status is in the key, so closing a job is not an update - it is a delete and a put at a new key, because a primary key cannot be changed in place. Two writes, and between them the job sits under neither status prefix: a dispatch board refreshing at that moment sees it in no list at all, unless the pair is bound in one transaction. That is the bill for this design, and it is why a status that changes constantly belongs in a sparse index instead, where only the index entry moves. The console cannot run either write, so this page prices the design rather than demonstrating it.

Read the sort-key columntrade-off

The console reads the whole region, since the resolver handles only the partition half of a key condition. Even so the grouping is visible: DONE sorts before OPEN, and within each the dates run in order. A begins_with on STATUS#OPEN# would return exactly the second block, and nothing before it would be read.

Taught in the course

work-ordersDynamoDB workbench
Ready to run
Explore an access patternSelect to load & run

A region's jobs, grouped by status and ordered by date within each.

Request
Execute against the local Dynoxide engine
ReturnedFiltered outChanged
PK(pk)
SK(sk)
customer
engineer
scheduledAt
REGION#north
STATUS#DONE#2026-09-13#w-2201S
Halden MillS
samS
1789257600N
REGION#north
STATUS#DONE#2026-09-19#w-2288S
Ashby DepotS
meiS
1789776000N
REGION#north
STATUS#OPEN#2026-09-23#w-2310S
Calder WorksS
samS
1790121600N
REGION#north
STATUS#OPEN#2026-09-24#w-2314S
Bramley FarmS
—
1790208000N
REGION#north
STATUS#OPEN#2026-09-27#w-2331S
Halden MillS
meiS
1790467200N
REGION#south
STATUS#DONE#2026-09-17#w-2250S
Peel LogisticsS
rajS
1789603200N
REGION#south
STATUS#OPEN#2026-09-25#w-2322S
Vale Cold StoreS
rajS
1790294400N
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.