Skip to content

Example: Rich Updates

A product catalog example demonstrating every update operation beyond simple set(). DynamoDB supports several update expression types — SET, ADD, REMOVE, and DELETE — and effect-dynamodb exposes each as a composable combinator that you can mix freely in a single update call.

What you’ll learn:

  • Entity.add() for atomic numeric increments
  • Entity.subtract() for atomic numeric decrements
  • Entity.append() for appending to list attributes
  • Entity.remove() for deleting attributes entirely
  • Entity.deleteFromSet() for removing elements from DynamoDB Set types
  • Composing multiple update types in one operation
  • Combining rich updates with optimistic locking via expectedVersion()

A pure domain model for a product — no DynamoDB concepts:

models.ts
class Product extends Schema.Class<Product>("Product")({
productId: Schema.String,
name: Schema.NonEmptyString,
description: Schema.optional(Schema.String),
price: Schema.Number,
stock: Schema.Number,
viewCount: Schema.Number,
tags: Schema.Array(Schema.String),
category: Schema.String,
}) {}
const ProductModel = DynamoModel.configure(Product, {
category: { immutable: true },
})

DynamoModel.configure marks category as immutable — it can be set on creation but cannot be changed by updates.


entities.ts
const AppSchema = DynamoSchema.make({ name: "updates-demo", version: 1 })
const Products = Entity.make({
model: ProductModel,
entityType: "Product",
primaryKey: {
pk: { field: "pk", composite: ["productId"] },
sk: { field: "sk", composite: [] },
},
timestamps: true,
versioned: true,
})
const MainTable = Table.make({ schema: AppSchema, entities: { Products } })

versioned: true enables optimistic locking — every write increments a version field, and expectedVersion() can enforce concurrency safety.


const product = yield* db.entities.Products.put({
productId: "p-1",
name: "Wireless Mouse",
description: "Ergonomic wireless mouse",
price: 29.99,
stock: 100,
viewCount: 0,
tags: ["electronics", "accessories"],
category: "peripherals",
})

Step 4: Atomic Increment with Entity.add()

Section titled “Step 4: Atomic Increment with Entity.add()”

ADD atomically increments a numeric attribute. If the attribute does not exist, DynamoDB initializes it to the provided value. This is thread-safe — no read-modify-write cycle needed.

const afterAdd = yield* db.entities.Products.update({ productId: "p-1" }).add({ viewCount: 1 })
// Multiple adds compose
const afterMultiAdd = yield* db.entities.Products.update({ productId: "p-1" }).add({
viewCount: 5,
stock: 50,
})

Step 5: Atomic Decrement with Entity.subtract()

Section titled “Step 5: Atomic Decrement with Entity.subtract()”

DynamoDB has no native SUBTRACT action. Entity.subtract() synthesizes SET #field = #field - :val for you:

const afterSub = yield* db.entities.Products.update({ productId: "p-1" }).subtract({ stock: 3 })

Appends elements to the end of a list attribute using DynamoDB’s list_append function:

const afterAppend = yield* db.entities.Products.update({ productId: "p-1" }).append({
tags: ["on-sale", "featured"],
})

The value must be an array — each element is appended to the existing list.


Step 7: Remove Attributes with Entity.remove()

Section titled “Step 7: Remove Attributes with Entity.remove()”

REMOVE deletes attributes entirely from the item. The attribute no longer exists after removal (it becomes undefined, not null):

const afterRemove = yield* db.entities.Products.update({ productId: "p-1" }).remove([
"description",
])

Step 8: Delete from Set with Entity.deleteFromSet()

Section titled “Step 8: Delete from Set with Entity.deleteFromSet()”

DELETE removes specific elements from a DynamoDB Set attribute (SS, NS, or BS). This operates on native DynamoDB Set types:

// For models with Set-type fields:
Entity.deleteFromSet({ fieldName: new Set(["value-to-remove"]) })
// → Produces DynamoDB DELETE clause for the Set attribute

All update combinators chain as methods on the BoundUpdate builder. They are merged into one UpdateItem call to DynamoDB:

const composed = yield* db.entities.Products.update({ productId: "p-1" })
.set({ name: "Premium Wireless Mouse", price: 39.99 })
.add({ viewCount: 10 })
.subtract({ stock: 5 })
.append({ tags: ["premium"] })

One DynamoDB call produces: name: "Premium Wireless Mouse", price: 39.99, viewCount: 16 (was 6, added 10), stock: 142 (was 147, subtracted 5), tags: ["electronics", "accessories", "on-sale", "featured", "premium"].

This is a single atomic DynamoDB operation — all changes apply together or none do.


Step 10: Combining with Optimistic Locking

Section titled “Step 10: Combining with Optimistic Locking”

Rich updates compose with Entity.expectedVersion() for concurrency control:

const locked = yield* db.entities.Products.update({ productId: "p-1" })
.add({ viewCount: 1 })
.expectedVersion(6)
// Wrong version → OptimisticLockError
const lockFail = yield* db.entities.Products.update({ productId: "p-1" })
.add({ viewCount: 1 })
.expectedVersion(1)
.asEffect()
.pipe(
Effect.map(() => "updated"),
Effect.catchTag("OptimisticLockError", (e) =>
Effect.succeed(`OptimisticLockError: expected v${e.expectedVersion}`),
),
)

When the version in DynamoDB does not match the expected version, the update is rejected with an OptimisticLockError. This prevents lost updates in concurrent scenarios.


The complete runnable example is at examples/updates.ts in the repository.

Terminal window
docker run -d -p 8000:8000 amazon/dynamodb-local
Terminal window
npx tsx examples/updates.ts
main.ts
const AppLayer = Layer.mergeAll(
DynamoClient.layer({
region: "us-east-1",
endpoint: "http://localhost:8000",
credentials: { accessKeyId: "local", secretAccessKey: "local" },
}),
MainTable.layer({ name: "updates-demo-table" }),
)
const main = program.pipe(Effect.provide(AppLayer))
Effect.runPromise(main).then(
() => console.log("\nDone."),
(err) => console.error("\nFailed:", err),
)

ConceptHow it’s used
Entity.add()Atomic numeric increment via DynamoDB ADD — no read-modify-write needed
Entity.subtract()Synthesized SET #f = #f - :v for atomic decrement
Entity.append()list_append for adding elements to list attributes
Entity.remove()REMOVE to delete attributes entirely from an item
Entity.deleteFromSet()DELETE to remove elements from DynamoDB Set types
CompositionAll combinators chain as methods on BoundUpdate — compiled into one atomic UpdateItem call
Optimistic locking.expectedVersion(n) chains with any other combinator on the builder for concurrency safety