Intermediate5 min read

DynamoDB Update Expressions

An update expression tells UpdateItem how to mutate a single item: which to write, increment, delete, or fold into a set. There's no UPDATE … SET … WHERE — you name the item by its key and describe the change with four clause keywords.

How do DynamoDB update expressions work?

A DynamoDB update expression tells UpdateItem how to mutate one item using four clauses. SET writes or overwrites an . ADD atomically increments a number or unions into a set. REMOVE deletes an attribute or one list element. DELETE removes specific members from a set. One call can carry all four at once.

  • SET writes or overwrites an attribute — scalars, documents, and the function idioms if_not_exists and list_append.
  • ADD does an atomic number increment or a set union, in one round trip, with no read-first.
  • REMOVE deletes an attribute outright (or a single list element by index).
  • DELETE removes specific members from a set — and only from a set.

Coming from SQL, the trap is reaching for SET for everything. ADD and DELETE exist because read-modify-write on a counter or a set is a race you'll lose under concurrency.

Pick the clause by what you're changing

One UpdateItem call can carry all four clauses at once, in the order SET … REMOVE … ADD … DELETE. Each keyword appears at most once and takes a comma-separated list of actions.

ClauseWorks onUse it to
SETAny attributeWrite/overwrite a value or document field
ADDNumber or Set onlyAtomically increment, or union into a set
REMOVEAny attribute or list elementDelete an attribute; drop one list index
DELETESet onlyRemove specific members from a set

ADD on a string and DELETE on a scalar are validation errors, not no-ops — DynamoDB rejects the whole call. Per the AWS update-expression reference, ADD is restricted to numbers and sets, and DELETE to sets.

The worked example: a shopping cart

One item per cart, keyed by CartPK = "CART#c-9f21" and CartSK = "SUMMARY". It tracks a running OrderTotal, a LineItems list, a PromoCodes string set, and an ItemCount.

SET — write the scalars and documents

SET overwrites whatever was there. Add a line item to the list and bump the total in the same call:

SET OrderTotal = :total,
LineItems = list_append(LineItems, :newItem),
UpdatedAt = :now

list_append(LineItems, :newItem) appends to the tail; flip the arguments — list_append(:newItem, LineItems) — to prepend. The order of arguments is the order of concatenation, nothing more.

There's a footgun in that first call: if the cart is brand new, LineItems doesn't exist yet, and list_append on a missing attribute fails. Guard it with if_not_exists:

SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem)

if_not_exists(LineItems, :empty) returns the current list if present, else the fallback :empty (an empty list []). That makes the first add and every later add use the same expression — a real reason these idioms exist.

ADD — increment the count, atomically

To bump ItemCount, do not read it, add one in your code, and SET it back. That's a lost-update race: two concurrent adds both read 3, both write 4, and you've dropped one. ADD does the arithmetic server-side:

ADD ItemCount :one

With :one = 1, this is an atomic counter. Concurrent calls serialize on the item, so two adds land as +2. Pass a negative number to decrement. If ItemCount is absent, ADD treats it as 0 first — so you never need to seed the counter.

You can build this exact expression — names, typed values, and the marshalled request — in the DynamoDB expression builder without hand-escaping a single #name or :value placeholder.

Edit the preset below — a SET plus an atomic ADD — and watch the UpdateExpression rebuild as you go:

Build your request
Generated code
new UpdateItemCommand({
  "TableName": "AuditLog",
  "Key": {
    "pk": {
      "S": "TENANT#acme"
    },
    "sk": {
      "S": "CONFIG"
    }
  },
  "UpdateExpression": "SET #upd0 = :updValue0 ADD #upd1 :updValue1",
  "ExpressionAttributeNames": {
    "#upd0": "plan",
    "#upd1": "seats"
  },
  "ExpressionAttributeValues": {
    ":updValue0": {
      "S": "pro"
    },
    ":updValue1": {
      "N": "5"
    }
  }
})

REMOVE — drop an attribute or one line item

REMOVE is how you delete an attribute entirely (there's no "set it to null" — that just writes a NULL type). Clear an applied discount and drop the third line item in one call:

REMOVE AppliedDiscount, LineItems[2]

LineItems[2] removes the element at index 2 and shifts everything after it down — index 3 becomes 2, and so on. If you REMOVE two indices in one expression, both are evaluated against the original list, so removing [2] and [3] together drops the third and fourth elements as you'd expect.

DELETE — remove set members

PromoCodes is a string set, so a customer pulling one code uses DELETE, not REMOVE. REMOVE PromoCodes would nuke the whole set; DELETE subtracts the named members:

DELETE PromoCodes :pulled

With :pulled = the set {"SAVE10"}, only that member goes. Two rules bite here: a set can never be empty, so deleting the last member removes the PromoCodes attribute outright; and the value must be a set type matching the attribute — a bare string is a type error.

Put it together

A "add item, apply a promo, bump the count" update is one call across three clauses:

SET LineItems = list_append(if_not_exists(LineItems, :empty), :newItem),
OrderTotal = OrderTotal + :price
ADD ItemCount :one
DELETE PromoCodes :expiredCode

Note OrderTotal = OrderTotal + :price — arithmetic inside SET works on the existing value. It's not atomic the way ADD is for race-safety, but it reads the current total server-side rather than round-tripping it through your code.

Pitfalls to avoid

  • SET-ing a counter you read first. Use ADD — read-modify-write loses updates under concurrency. This is the most common cart/inventory bug.
  • list_append on a missing list. Wrap the target in if_not_exists or the first write fails.
  • Confusing REMOVE and DELETE. REMOVE drops the attribute; DELETE subtracts members from a set. Mixing them deletes more than you meant.
  • Forgetting UpdateItem is an upsert. If the key doesn't exist, it creates the item. Use a ConditionExpression (attribute_exists(CartPK)) when you mean "update only".

For modeling the keys these expressions run against, see single-table design; to decide how you'll read the cart back, see query vs scan.

Build and copy any of these in the expression builder, then try DynoTable to run them against your own tables and watch the item change live.

Updated