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 incrementsEntity.subtract()for atomic numeric decrementsEntity.append()for appending to list attributesEntity.remove()for deleting attributes entirelyEntity.deleteFromSet()for removing elements from DynamoDB Set types- Composing multiple update types in one operation
- Combining rich updates with optimistic locking via
expectedVersion()
Step 1: Model
Section titled “Step 1: Model”A pure domain model for a product — no DynamoDB concepts:
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.
Step 2: Schema, Table, and Entity
Section titled “Step 2: Schema, Table, and Entity”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.
Step 3: Create a Product
Section titled “Step 3: Create a Product”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 })Step 6: List Append with Entity.append()
Section titled “Step 6: List Append with Entity.append()”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 attributeStep 9: Composing Multiple Update Types
Section titled “Step 9: Composing Multiple Update Types”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.
Running the Example
Section titled “Running the Example”The complete runnable example is at examples/updates.ts in the repository.
docker run -d -p 8000:8000 amazon/dynamodb-localnpx tsx examples/updates.tsLayer Setup
Section titled “Layer Setup”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),)Key Takeaways
Section titled “Key Takeaways”| Concept | How 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 |
| Composition | All 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 |
What’s Next?
Section titled “What’s Next?”- Modeling Guide — Deep dive into models, schemas, tables, and entities
- Queries — Query combinators and pagination
- Example: Batch Operations — Batch get and write across entities
- Example: Human Resources — Full single-table design with collections, transactions, and soft delete