Skip to content

Core operations

This guide covers the four fundamental CRUD operations in DynamoDB: Get, Put, Update, and Delete. These operations form the foundation of all DynamoDB interactions.

Overview

The TableClient provides simple, type-safe methods for all core operations:

  • Get - Retrieve a single item by its key
  • Put - Create or replace an item
  • Update - Modify specific attributes of an item
  • Delete - Remove an item from the table

All operations support: - Conditional expressions for safe concurrent updates - Projection expressions to retrieve only needed attributes - Automatic retry with exponential backoff - Statistics collection for monitoring

Operation flow

sequenceDiagram
    participant App as Application
    participant Client as TableClient
    participant DDB as DynamoDB

    App->>Client: get(key)
    Client->>DDB: GetItem
    DDB-->>Client: Item
    Client-->>App: Item | null

    App->>Client: put(item)
    Client->>DDB: PutItem
    DDB-->>Client: Success
    Client-->>App: void

    App->>Client: update(key, updates)
    Client->>DDB: UpdateItem
    DDB-->>Client: Updated Item
    Client-->>App: Item

    App->>Client: delete(key)
    Client->>DDB: DeleteItem
    DDB-->>Client: Success
    Client-->>App: void

Get operation

Retrieve a single item by its partition key (and sort key if applicable).

Basic get

import { TableClient } from '@ddb-lib/client'
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'

const client = new TableClient({
  tableName: 'my-table',
  client: new DynamoDBClient({ region: 'us-east-1' })
})

// Get an item by key
const item = await client.get({
  pk: 'USER#123',
  sk: 'PROFILE'
})

if (item) {
  console.log('Found user:', item)
} else {
  console.log('User not found')
}

Get with projection

Retrieve only specific attributes to reduce data transfer and costs:

// Get only name and email fields
const item = await client.get(
  { pk: 'USER#123', sk: 'PROFILE' },
  {
    projectionExpression: ['name', 'email', 'status']
  }
)

// Item will only contain: { pk, sk, name, email, status }

Consistent read

By default, DynamoDB uses eventually consistent reads. For strongly consistent reads:

const item = await client.get(
  { pk: 'USER#123', sk: 'PROFILE' },
  {
    consistentRead: true  // Ensures latest data
  }
)

When to use consistent reads: - ✅ After a write when you need to read the latest value immediately - ✅ When data consistency is critical (financial transactions, inventory) - ❌ For most read operations (eventually consistent is faster and cheaper)

Get with pattern helpers

Use pattern helpers to construct keys safely:

import { PatternHelpers } from '@ddb-lib/core'

const userId = '123'
const key = {
  pk: PatternHelpers.entityKey('USER', userId),
  sk: 'PROFILE'
}

const user = await client.get(key)

Put operation

Create a new item or completely replace an existing item.

Basic put

interface User {
  pk: string
  sk: string
  userId: string
  name: string
  email: string
  status: 'ACTIVE' | 'INACTIVE'
  createdAt: string
}

const client = new TableClient<User>({
  tableName: 'my-table',
  client: new DynamoDBClient({ region: 'us-east-1' })
})

// Create a new user
await client.put({
  pk: 'USER#123',
  sk: 'PROFILE',
  userId: '123',
  name: 'Alice Johnson',
  email: 'alice@example.com',
  status: 'ACTIVE',
  createdAt: new Date().toISOString()
})

Conditional put (create if not exists)

Prevent overwriting existing items:

try {
  await client.put(
    {
      pk: 'USER#123',
      sk: 'PROFILE',
      userId: '123',
      name: 'Alice Johnson',
      email: 'alice@example.com',
      status: 'ACTIVE',
      createdAt: new Date().toISOString()
    },
    {
      // Only create if pk doesn't exist
      condition: {
        pk: { attributeNotExists: true }
      }
    }
  )
  console.log('User created successfully')
} catch (error) {
  if (error.name === 'ConditionalCheckFailedException') {
    console.log('User already exists')
  }
}

Put with multiple conditions

Combine multiple conditions for complex logic:

await client.put(
  {
    pk: 'ORDER#456',
    sk: 'DETAILS',
    orderId: '456',
    status: 'PENDING',
    total: 99.99,
    updatedAt: new Date().toISOString()
  },
  {
    condition: {
      // Create if doesn't exist OR if status is DRAFT
      or: [
        { pk: { attributeNotExists: true } },
        { status: { eq: 'DRAFT' } }
      ]
    }
  }
)

Put with return values

Get the old item back when replacing:

const result = await client.put(
  {
    pk: 'CONFIG#app',
    sk: 'SETTINGS',
    theme: 'dark',
    language: 'en'
  },
  {
    returnValues: 'ALL_OLD'  // Returns the previous item
  }
)

// result contains the old item (if it existed)

Update operation

Modify specific attributes without replacing the entire item.

Basic update

// Update specific fields
const updated = await client.update(
  { pk: 'USER#123', sk: 'PROFILE' },
  {
    email: 'alice.new@example.com',
    updatedAt: new Date().toISOString()
  }
)

console.log('Updated user:', updated)

Update vs Put: - Update - Modifies only specified attributes, keeps others unchanged - Put - Replaces the entire item, removes unspecified attributes

Update with conditions

Ensure safe concurrent updates:

// Update only if current status is PENDING
await client.update(
  { pk: 'ORDER#456', sk: 'DETAILS' },
  {
    status: 'PROCESSING',
    processedAt: new Date().toISOString()
  },
  {
    condition: {
      status: { eq: 'PENDING' }
    }
  }
)

Optimistic locking with versions

Prevent lost updates in concurrent scenarios:

import { PatternHelpers } from '@ddb-lib/core'

// Read current item
const item = await client.get({ pk: 'ACCOUNT#789', sk: 'BALANCE' })

if (!item) {
  throw new Error('Account not found')
}

// Update with version check
try {
  await client.update(
    { pk: 'ACCOUNT#789', sk: 'BALANCE' },
    {
      balance: item.balance + 100,
      version: PatternHelpers.incrementVersion(item.version)
    },
    {
      // Only update if version hasn't changed
      condition: {
        version: { eq: item.version }
      }
    }
  )
  console.log('Balance updated successfully')
} catch (error) {
  if (error.name === 'ConditionalCheckFailedException') {
    console.log('Version conflict - item was modified by another process')
    // Retry the operation
  }
}

Update return values

Control what data is returned:

// Return all attributes after update (default)
const updated = await client.update(
  { pk: 'USER#123', sk: 'PROFILE' },
  { lastLogin: new Date().toISOString() },
  { returnValues: 'ALL_NEW' }
)

// Return only updated attributes
const updated = await client.update(
  { pk: 'USER#123', sk: 'PROFILE' },
  { lastLogin: new Date().toISOString() },
  { returnValues: 'UPDATED_NEW' }
)

// Return old values before update
const old = await client.update(
  { pk: 'USER#123', sk: 'PROFILE' },
  { status: 'INACTIVE' },
  { returnValues: 'ALL_OLD' }
)

Delete operation

Remove an item from the table.

Basic delete

// Delete an item
await client.delete({
  pk: 'USER#123',
  sk: 'PROFILE'
})

console.log('User deleted')

Conditional delete

Only delete if certain conditions are met:

try {
  await client.delete(
    { pk: 'USER#123', sk: 'PROFILE' },
    {
      // Only delete if status is INACTIVE
      condition: {
        status: { eq: 'INACTIVE' }
      }
    }
  )
  console.log('Inactive user deleted')
} catch (error) {
  if (error.name === 'ConditionalCheckFailedException') {
    console.log('User is not inactive, cannot delete')
  }
}

Delete with return values

Get the deleted item back:

const deleted = await client.delete(
  { pk: 'USER#123', sk: 'PROFILE' },
  {
    returnValues: 'ALL_OLD'
  }
)

if (deleted) {
  console.log('Deleted user:', deleted)
  // Archive or log the deleted data
}

Safe delete pattern

Verify item exists before deleting:

// Delete only if item exists
await client.delete(
  { pk: 'USER#123', sk: 'PROFILE' },
  {
    condition: {
      pk: { attributeExists: true }
    }
  }
)

Error handling

All operations can throw errors that should be handled:

import { ConditionalCheckError, ValidationError } from '@ddb-lib/client'

try {
  await client.put(item, { condition: { pk: { attributeNotExists: true } } })
} catch (error) {
  if (error instanceof ConditionalCheckError) {
    console.log('Condition failed:', error.message)
    console.log('Failed condition:', error.condition)
  } else if (error instanceof ValidationError) {
    console.log('Validation failed:', error.message)
  } else if (error.name === 'ProvisionedThroughputExceededException') {
    console.log('Throttled - retry with backoff')
  } else {
    console.error('Unexpected error:', error)
  }
}

Performance considerations

Projection expressions

Always use projection expressions when you don't need all attributes:

// ❌ Bad: Retrieves entire item (wastes bandwidth and RCU)
const item = await client.get({ pk: 'USER#123', sk: 'PROFILE' })
const name = item.name

// ✅ Good: Retrieves only needed attribute
const item = await client.get(
  { pk: 'USER#123', sk: 'PROFILE' },
  { projectionExpression: ['name'] }
)

Batch operations

For multiple items, use batch operations instead of individual operations:

// ❌ Bad: Multiple individual operations
for (const key of keys) {
  await client.get(key)  // Slow and expensive
}

// ✅ Good: Single batch operation
const items = await client.batchGet(keys)  // Fast and efficient

See the Batch Operations Guide for details.

Update vs put

Use update when modifying a few attributes:

// ❌ Bad: Get entire item, modify, then put back
const item = await client.get(key)
item.email = 'new@example.com'
await client.put(item)  // Replaces entire item

// ✅ Good: Update only the changed attribute
await client.update(key, { email: 'new@example.com' })

Common patterns

Upsert (create or update)

// Put without condition = upsert
await client.put({
  pk: 'CONFIG#app',
  sk: 'SETTINGS',
  theme: 'dark',
  updatedAt: new Date().toISOString()
})
// Creates if doesn't exist, replaces if exists

Increment counter

// Atomic counter increment
await client.update(
  { pk: 'STATS#views', sk: 'PAGE#home' },
  {
    count: (item.count || 0) + 1,
    lastViewed: new Date().toISOString()
  }
)

Soft delete

// Mark as deleted instead of removing
await client.update(
  { pk: 'USER#123', sk: 'PROFILE' },
  {
    status: 'DELETED',
    deletedAt: new Date().toISOString()
  }
)

TTL (time to live)

import { PatternHelpers } from '@ddb-lib/core'

// Item expires in 30 days
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 30)

await client.put({
  pk: 'SESSION#abc123',
  sk: 'DATA',
  userId: '123',
  ttl: PatternHelpers.ttlTimestamp(expiresAt)
})

Next steps