Skip to main content

Chapter 4: Consistency Models in DynamoDB

Introduction

Consistency in distributed systems is one of the most critical concepts to understand when working with DynamoDB. As a distributed database that replicates data across multiple Availability Zones, DynamoDB must balance consistency, availability, and partition tolerance according to the CAP theorem. This chapter explores DynamoDB’s consistency models, their implications, and how to choose the right consistency level for your application.

Understanding Distributed Consistency

The CAP Theorem

The CAP theorem states that a distributed system can provide at most two of the following three guarantees:
  • Consistency: All nodes see the same data at the same time
  • Availability: Every request receives a response (success or failure)
  • Partition Tolerance: The system continues to operate despite network partitions
<svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg">
  <!-- Title -->
  <text x="400" y="30" font-size="18" font-weight="bold" text-anchor="middle" fill="#333">
    CAP Theorem: Choose Two
  </text>

  <!-- Triangle -->
  <polygon points="400,100 650,500 150,500" fill="none" stroke="#333" stroke-width="3"/>

  <!-- C vertex -->
  <circle cx="400" cy="100" r="60" fill="#1976d2" stroke="#0d47a1" stroke-width="2"/>
  <text x="400" y="95" font-size="16" font-weight="bold" text-anchor="middle" fill="#fff">
    Consistency
  </text>
  <text x="400" y="115" font-size="12" text-anchor="middle" fill="#fff">
    (C)
  </text>

  <!-- A vertex -->
  <circle cx="150" cy="500" r="60" fill="#4caf50" stroke="#2e7d32" stroke-width="2"/>
  <text x="150" y="495" font-size="16" font-weight="bold" text-anchor="middle" fill="#fff">
    Availability
  </text>
  <text x="150" y="515" font-size="12" text-anchor="middle" fill="#fff">
    (A)
  </text>

  <!-- P vertex -->
  <circle cx="650" cy="500" r="60" fill="#ff9800" stroke="#f57c00" stroke-width="2"/>
  <text x="650" y="490" font-size="14" font-weight="bold" text-anchor="middle" fill="#fff">
    Partition
  </text>
  <text x="650" y="505" font-size="14" font-weight="bold" text-anchor="middle" fill="#fff">
    Tolerance
  </text>
  <text x="650" y="520" font-size="12" text-anchor="middle" fill="#fff">
    (P)
  </text>

  <!-- CA edge -->
  <rect x="230" y="280" width="140" height="60" fill="#e3f2fd" stroke="#1976d2" stroke-width="2" rx="5"/>
  <text x="300" y="305" font-size="13" font-weight="bold" text-anchor="middle" fill="#1976d2">
    CA System
  </text>
  <text x="300" y="325" font-size="11" text-anchor="middle" fill="#333">
    Traditional
  </text>
  <text x="300" y="340" font-size="11" text-anchor="middle" fill="#333">
    RDBMS
  </text>

  <!-- CP edge -->
  <rect x="450" y="280" width="140" height="60" fill="#fff3e0" stroke="#ff9800" stroke-width="2" rx="5"/>
  <text x="520" y="305" font-size="13" font-weight="bold" text-anchor="middle" fill="#f57c00">
    CP System
  </text>
  <text x="520" y="325" font-size="11" text-anchor="middle" fill="#333">
    MongoDB
  </text>
  <text x="520" y="340" font-size="11" text-anchor="middle" fill="#333">
    HBase
  </text>

  <!-- AP edge -->
  <rect x="340" y="470" width="140" height="60" fill="#e8f5e9" stroke="#4caf50" stroke-width="2" rx="5"/>
  <text x="410" y="495" font-size="13" font-weight="bold" text-anchor="middle" fill="#4caf50">
    AP System
  </text>
  <text x="410" y="515" font-size="11" text-anchor="middle" fill="#333">
    DynamoDB
  </text>
  <text x="410" y="530" font-size="11" text-anchor="middle" fill="#333">
    Cassandra
  </text>

  <!-- Note -->
  <rect x="200" y="560" width="400" height="30" fill="#fff9c4" stroke="#fbc02d" stroke-width="1" rx="3"/>
  <text x="400" y="580" font-size="11" text-anchor="middle" fill="#333">
    In practice, partition tolerance is mandatory for distributed systems
  </text>
</svg>
DynamoDB chooses Availability and Partition Tolerance (AP), offering eventual consistency by default with an option for strong consistency on reads.

Replication in DynamoDB

DynamoDB automatically replicates data across three Availability Zones within a region:
<svg viewBox="0 0 900 600" xmlns="http://www.w3.org/2000/svg">
  <!-- Title -->
  <text x="450" y="30" font-size="18" font-weight="bold" text-anchor="middle" fill="#333">
    DynamoDB Cross-AZ Replication
  </text>

  <!-- Region Box -->
  <rect x="50" y="60" width="800" height="520" fill="#f5f5f5" stroke="#666" stroke-width="2" stroke-dasharray="5,5" rx="5"/>
  <text x="450" y="90" font-size="16" font-weight="bold" text-anchor="middle" fill="#666">
    AWS Region (e.g., us-east-1)
  </text>

  <!-- AZ 1 -->
  <rect x="80" y="120" width="220" height="420" fill="#e3f2fd" stroke="#1976d2" stroke-width="2" rx="5"/>
  <text x="190" y="150" font-size="14" font-weight="bold" text-anchor="middle" fill="#1976d2">
    Availability Zone 1
  </text>

  <!-- Storage Node 1 -->
  <rect x="120" y="180" width="140" height="100" fill="#fff" stroke="#1976d2" stroke-width="2" rx="3"/>
  <text x="190" y="205" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Storage Node
  </text>
  <rect x="130" y="220" width="120" height="25" fill="#bbdefb" stroke="#1976d2" stroke-width="1" rx="2"/>
  <text x="190" y="237" font-size="11" text-anchor="middle" fill="#333">
    Partition: A-M
  </text>
  <rect x="130" y="250" width="120" height="20" fill="#e3f2fd" stroke="#1976d2" stroke-width="1" rx="2"/>
  <text x="190" y="264" font-size="10" text-anchor="middle" fill="#333">
    Replica: Leader
  </text>

  <!-- Storage Node 2 -->
  <rect x="120" y="310" width="140" height="100" fill="#fff" stroke="#1976d2" stroke-width="2" rx="3"/>
  <text x="190" y="335" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Storage Node
  </text>
  <rect x="130" y="350" width="120" height="25" fill="#bbdefb" stroke="#1976d2" stroke-width="1" rx="2"/>
  <text x="190" y="367" font-size="11" text-anchor="middle" fill="#333">
    Partition: N-Z
  </text>
  <rect x="130" y="380" width="120" height="20" fill="#e3f2fd" stroke="#1976d2" stroke-width="1" rx="2"/>
  <text x="190" y="394" font-size="10" text-anchor="middle" fill="#333">
    Replica: Follower
  </text>

  <!-- AZ 2 -->
  <rect x="340" y="120" width="220" height="420" fill="#e8f5e9" stroke="#4caf50" stroke-width="2" rx="5"/>
  <text x="450" y="150" font-size="14" font-weight="bold" text-anchor="middle" fill="#4caf50">
    Availability Zone 2
  </text>

  <!-- Storage Node 3 -->
  <rect x="380" y="180" width="140" height="100" fill="#fff" stroke="#4caf50" stroke-width="2" rx="3"/>
  <text x="450" y="205" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Storage Node
  </text>
  <rect x="390" y="220" width="120" height="25" fill="#c8e6c9" stroke="#4caf50" stroke-width="1" rx="2"/>
  <text x="450" y="237" font-size="11" text-anchor="middle" fill="#333">
    Partition: A-M
  </text>
  <rect x="390" y="250" width="120" height="20" fill="#e8f5e9" stroke="#4caf50" stroke-width="1" rx="2"/>
  <text x="450" y="264" font-size="10" text-anchor="middle" fill="#333">
    Replica: Follower
  </text>

  <!-- Storage Node 4 -->
  <rect x="380" y="310" width="140" height="100" fill="#fff" stroke="#4caf50" stroke-width="2" rx="3"/>
  <text x="450" y="335" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Storage Node
  </text>
  <rect x="390" y="350" width="120" height="25" fill="#c8e6c9" stroke="#4caf50" stroke-width="1" rx="2"/>
  <text x="450" y="367" font-size="11" text-anchor="middle" fill="#333">
    Partition: N-Z
  </text>
  <rect x="390" y="380" width="120" height="20" fill="#e8f5e9" stroke="#4caf50" stroke-width="1" rx="2"/>
  <text x="450" y="394" font-size="10" text-anchor="middle" fill="#333">
    Replica: Leader
  </text>

  <!-- AZ 3 -->
  <rect x="600" y="120" width="220" height="420" fill="#fff3e0" stroke="#ff9800" stroke-width="2" rx="5"/>
  <text x="710" y="150" font-size="14" font-weight="bold" text-anchor="middle" fill="#ff9800">
    Availability Zone 3
  </text>

  <!-- Storage Node 5 -->
  <rect x="640" y="180" width="140" height="100" fill="#fff" stroke="#ff9800" stroke-width="2" rx="3"/>
  <text x="710" y="205" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Storage Node
  </text>
  <rect x="650" y="220" width="120" height="25" fill="#ffe0b2" stroke="#ff9800" stroke-width="1" rx="2"/>
  <text x="710" y="237" font-size="11" text-anchor="middle" fill="#333">
    Partition: A-M
  </text>
  <rect x="650" y="250" width="120" height="20" fill="#fff3e0" stroke="#ff9800" stroke-width="1" rx="2"/>
  <text x="710" y="264" font-size="10" text-anchor="middle" fill="#333">
    Replica: Follower
  </text>

  <!-- Storage Node 6 -->
  <rect x="640" y="310" width="140" height="100" fill="#fff" stroke="#ff9800" stroke-width="2" rx="3"/>
  <text x="710" y="335" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Storage Node
  </text>
  <rect x="650" y="350" width="120" height="25" fill="#ffe0b2" stroke="#ff9800" stroke-width="1" rx="2"/>
  <text x="710" y="367" font-size="11" text-anchor="middle" fill="#333">
    Partition: N-Z
  </text>
  <rect x="650" y="380" width="120" height="20" fill="#fff3e0" stroke="#ff9800" stroke-width="1" rx="2"/>
  <text x="710" y="394" font-size="10" text-anchor="middle" fill="#333">
    Replica: Follower
  </text>

  <!-- Replication Arrows -->
  <path d="M 260 230 L 380 230" stroke="#1976d2" stroke-width="2" fill="none" marker-end="url(#arrowblue)" stroke-dasharray="5,5"/>
  <path d="M 520 230 L 640 230" stroke="#4caf50" stroke-width="2" fill="none" marker-end="url(#arrowgreen)" stroke-dasharray="5,5"/>

  <path d="M 260 360 L 380 360" stroke="#1976d2" stroke-width="2" fill="none" marker-end="url(#arrowblue)" stroke-dasharray="5,5"/>
  <path d="M 520 360 L 640 360" stroke="#4caf50" stroke-width="2" fill="none" marker-end="url(#arrowgreen)" stroke-dasharray="5,5"/>

  <!-- Info Box -->
  <rect x="150" y="460" width="600" height="60" fill="#fff" stroke="#666" stroke-width="2" rx="5"/>
  <text x="450" y="485" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Automatic 3-Way Replication
  </text>
  <text x="450" y="505" font-size="11" text-anchor="middle" fill="#666">
    Each partition is replicated to 3 storage nodes across 3 AZs
  </text>

  <!-- Arrow markers -->
  <defs>
    <marker id="arrowblue" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
      <polygon points="0 0, 10 3, 0 6" fill="#1976d2"/>
    </marker>
    <marker id="arrowgreen" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
      <polygon points="0 0, 10 3, 0 6" fill="#4caf50"/>
    </marker>
  </defs>
</svg>

DynamoDB Consistency Models

Eventual Consistency (Default)

With eventually consistent reads, responses might not reflect recently completed writes. However, consistency is typically achieved within one second. Characteristics:
  • Best performance: Lower latency and higher throughput
  • Cost-efficient: Consumes half the RCUs of strongly consistent reads
  • Eventually consistent: Reads may return stale data temporarily
  • Default option: Used unless you specify otherwise
// Eventually consistent read (default)
const params = {
  TableName: 'Users',
  Key: {
    userId: '12345'
  }
  // ConsistentRead: false is the default
};

const result = await dynamodb.get(params).promise();
Timeline Example:
Time: T0    T1    T2    T3    T4
      |     |     |     |     |
AZ1:  [Write v2] ------------> v2
      |     |     |     |     |
AZ2:  v1 -----> v2 ----------> v2
      |     |     |     |     |
AZ3:  v1 ----------> v2 -----> v2
      |     |     |     |     |
Read: [v1 or v2] [v1 or v2] [v2]

- Write acknowledged at T0 (written to AZ1)
- Read at T1 might return v1 (from AZ2 or AZ3)
- Read at T3 will likely return v2 (all replicas updated)

Strong Consistency

Strongly consistent reads return a result that reflects all successful writes prior to the read. Characteristics:
  • Strong guarantees: Always returns the most recent data
  • Higher cost: Consumes twice the RCUs of eventual consistency
  • Slightly higher latency: Must coordinate across replicas
  • Opt-in: Must explicitly request
// Strongly consistent read
const params = {
  TableName: 'Users',
  Key: {
    userId: '12345'
  },
  ConsistentRead: true  // Request strong consistency
};

const result = await dynamodb.get(params).promise();
Timeline Example:
Time: T0    T1    T2    T3
      |     |     |     |
AZ1:  [Write v2] ------> v2
      |     |     |     |
AZ2:  v1 -----> v2 ----> v2
      |     |     |     |
AZ3:  v1 -----> v2 ----> v2
      |     |     |     |
Read:       [Wait...] [v2]

- Write initiated at T0
- Strong read waits for quorum before returning
- Read at T2 returns v2 (guaranteed latest)

Read-After-Write Consistency

DynamoDB provides read-after-write consistency for specific scenarios: Guaranteed Read-After-Write Consistency:
  1. GetItem with same key immediately after PutItem/UpdateItem
  2. Query/Scan of newly written items (within same session)
  3. Transactional reads (TransactGetItems)
// Write-then-read scenario
await dynamodb.put({
  TableName: 'Users',
  Item: {
    userId: '12345',
    name: 'John Doe'
  }
}).promise();

// This read will see the write (read-after-write consistency)
const result = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: '12345' }
}).promise();

// Returns: { userId: '12345', name: 'John Doe' }

Consistency Across Operations

GetItem and Query Operations

// GetItem - supports both consistency models
const eventualResult = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: '12345' },
  ConsistentRead: false  // Default: eventual consistency
}).promise();

const strongResult = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: '12345' },
  ConsistentRead: true   // Strong consistency
}).promise();

// Query - supports both consistency models
const queryResult = await dynamodb.query({
  TableName: 'Orders',
  KeyConditionExpression: 'userId = :uid',
  ExpressionAttributeValues: {
    ':uid': '12345'
  },
  ConsistentRead: true  // Optional: strong consistency
}).promise();

Scan Operations

// Scan - supports both consistency models
const scanResult = await dynamodb.scan({
  TableName: 'Products',
  FilterExpression: 'price < :maxPrice',
  ExpressionAttributeValues: {
    ':maxPrice': 100
  },
  ConsistentRead: true  // Optional: strong consistency
}).promise();

// Note: Strongly consistent scans are expensive!
// Only use when absolutely necessary

BatchGetItem

// BatchGetItem - eventual consistency only
const batchResult = await dynamodb.batchGet({
  RequestItems: {
    'Users': {
      Keys: [
        { userId: '12345' },
        { userId: '67890' }
      ]
      // ConsistentRead not supported for BatchGetItem!
    }
  }
}).promise();

// Workaround: Use parallel GetItem calls for strong consistency
const strongBatchResults = await Promise.all([
  dynamodb.get({
    TableName: 'Users',
    Key: { userId: '12345' },
    ConsistentRead: true
  }).promise(),
  dynamodb.get({
    TableName: 'Users',
    Key: { userId: '67890' },
    ConsistentRead: true
  }).promise()
]);

Global Secondary Indexes

Important: GSIs only support eventual consistency.
// Query GSI - eventual consistency only
const gsiResult = await dynamodb.query({
  TableName: 'Users',
  IndexName: 'EmailIndex',
  KeyConditionExpression: 'email = :email',
  ExpressionAttributeValues: {
    ':email': '[email protected]'
  }
  // ConsistentRead: true NOT SUPPORTED for GSI!
}).promise();

// GSI propagation timeline:
// T0: Write to base table
// T1: Write acknowledged
// T2: GSI updated (typically milliseconds)
// T3: GSI read reflects write

// Possible scenarios:
// - Read at T1.5: Old value (GSI not yet updated)
// - Read at T3: New value (GSI updated)

Transactions

DynamoDB transactions provide ACID guarantees with serializable isolation.
// TransactWriteItems - immediately consistent
await dynamodb.transactWrite({
  TransactItems: [
    {
      Put: {
        TableName: 'Accounts',
        Item: { accountId: 'A123', balance: 1000 }
      }
    },
    {
      Update: {
        TableName: 'Accounts',
        Key: { accountId: 'B456' },
        UpdateExpression: 'SET balance = balance - :amount',
        ExpressionAttributeValues: { ':amount': 100 }
      }
    }
  ]
}).promise();

// TransactGetItems - serializable snapshot
const transactResult = await dynamodb.transactGet({
  TransactItems: [
    {
      Get: {
        TableName: 'Accounts',
        Key: { accountId: 'A123' }
      }
    },
    {
      Get: {
        TableName: 'Accounts',
        Key: { accountId: 'B456' }
      }
    }
  ]
}).promise();

// Returns consistent snapshot across all items

Consistency Trade-offs

<svg viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg">
  <!-- Title -->
  <text x="400" y="30" font-size="18" font-weight="bold" text-anchor="middle" fill="#333">
    Consistency Model Trade-offs
  </text>

  <!-- Table Header -->
  <rect x="50" y="60" width="700" height="50" fill="#1976d2" stroke="#0d47a1" stroke-width="2"/>
  <text x="150" y="90" font-size="14" font-weight="bold" text-anchor="middle" fill="#fff">Aspect</text>
  <text x="350" y="90" font-size="14" font-weight="bold" text-anchor="middle" fill="#fff">Eventual Consistency</text>
  <text x="600" y="90" font-size="14" font-weight="bold" text-anchor="middle" fill="#fff">Strong Consistency</text>

  <!-- Row 1: Latency -->
  <rect x="50" y="110" width="700" height="60" fill="#e3f2fd" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="145" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">Latency</text>
  <text x="350" y="135" font-size="12" text-anchor="middle" fill="#333">Lower</text>
  <text x="350" y="155" font-size="11" text-anchor="middle" fill="#666">(~5-10ms)</text>
  <text x="600" y="135" font-size="12" text-anchor="middle" fill="#333">Higher</text>
  <text x="600" y="155" font-size="11" text-anchor="middle" fill="#666">(~10-20ms)</text>

  <!-- Row 2: Cost -->
  <rect x="50" y="170" width="700" height="60" fill="#fff" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="205" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">Cost (RCUs)</text>
  <text x="350" y="195" font-size="12" text-anchor="middle" fill="#333">0.5 RCU per 4KB</text>
  <text x="350" y="215" font-size="11" text-anchor="middle" fill="#4caf50">50% cheaper</text>
  <text x="600" y="195" font-size="12" text-anchor="middle" fill="#333">1 RCU per 4KB</text>
  <text x="600" y="215" font-size="11" text-anchor="middle" fill="#ff9800">More expensive</text>

  <!-- Row 3: Throughput -->
  <rect x="50" y="230" width="700" height="60" fill="#e3f2fd" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="265" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">Throughput</text>
  <text x="350" y="265" font-size="12" text-anchor="middle" fill="#333">Higher</text>
  <text x="600" y="265" font-size="12" text-anchor="middle" fill="#333">Lower</text>

  <!-- Row 4: Freshness -->
  <rect x="50" y="290" width="700" height="60" fill="#fff" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="325" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">Data Freshness</text>
  <text x="350" y="315" font-size="12" text-anchor="middle" fill="#333">May be stale</text>
  <text x="350" y="335" font-size="11" text-anchor="middle" fill="#666">(< 1 second)</text>
  <text x="600" y="315" font-size="12" text-anchor="middle" fill="#333">Always fresh</text>
  <text x="600" y="335" font-size="11" text-anchor="middle" fill="#666">(Latest value)</text>

  <!-- Row 5: Availability -->
  <rect x="50" y="350" width="700" height="60" fill="#e3f2fd" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="385" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">Availability</text>
  <text x="350" y="385" font-size="12" text-anchor="middle" fill="#333">Higher (AP)</text>
  <text x="600" y="385" font-size="12" text-anchor="middle" fill="#333">Lower (CP)</text>

  <!-- Row 6: GSI Support -->
  <rect x="50" y="410" width="700" height="60" fill="#fff" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="445" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">GSI Support</text>
  <text x="350" y="445" font-size="12" text-anchor="middle" fill="#4caf50">✓ Yes</text>
  <text x="600" y="445" font-size="12" text-anchor="middle" fill="#f44336">✗ No</text>

  <!-- Row 7: Use Cases -->
  <rect x="50" y="470" width="700" height="80" fill="#e3f2fd" stroke="#90caf9" stroke-width="1"/>
  <text x="150" y="515" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">Best For</text>
  <text x="350" y="500" font-size="11" text-anchor="middle" fill="#333">• Analytics</text>
  <text x="350" y="518" font-size="11" text-anchor="middle" fill="#333">• Caching</text>
  <text x="350" y="536" font-size="11" text-anchor="middle" fill="#333">• High-throughput reads</text>
  <text x="600" y="500" font-size="11" text-anchor="middle" fill="#333">• Financial transactions</text>
  <text x="600" y="518" font-size="11" text-anchor="middle" fill="#333">• Inventory management</text>
  <text x="600" y="536" font-size="11" text-anchor="middle" fill="#333">• Critical data accuracy</text>
</svg>

When to Use Each Consistency Model

Use Eventual Consistency When:

  1. High throughput is priority
// Social media feed - eventual consistency acceptable
const feedPosts = await dynamodb.query({
  TableName: 'Posts',
  IndexName: 'UserFeedIndex',
  KeyConditionExpression: 'userId = :uid',
  ExpressionAttributeValues: {
    ':uid': '12345'
  },
  ConsistentRead: false,  // Eventual consistency
  Limit: 50
}).promise();
  1. Cost optimization
// Analytics queries - eventual consistency for cost savings
const metrics = await dynamodb.query({
  TableName: 'AnalyticsEvents',
  KeyConditionExpression: 'eventType = :type AND #ts BETWEEN :start AND :end',
  ExpressionAttributeNames: {
    '#ts': 'timestamp'
  },
  ExpressionAttributeValues: {
    ':type': 'PAGE_VIEW',
    ':start': '2024-01-01',
    ':end': '2024-01-31'
  },
  ConsistentRead: false
}).promise();
  1. Reading from GSIs
// GSI reads - only eventual consistency available
const usersByEmail = await dynamodb.query({
  TableName: 'Users',
  IndexName: 'EmailIndex',
  KeyConditionExpression: 'email = :email',
  ExpressionAttributeValues: {
    ':email': '[email protected]'
  }
  // ConsistentRead not supported on GSIs
}).promise();
  1. Caching scenarios
// Product catalog - slight staleness acceptable
const products = await dynamodb.query({
  TableName: 'Products',
  IndexName: 'CategoryIndex',
  KeyConditionExpression: 'category = :cat',
  ExpressionAttributeValues: {
    ':cat': 'Electronics'
  },
  ConsistentRead: false
}).promise();

Use Strong Consistency When:

  1. Financial transactions
// Account balance - must be accurate
const account = await dynamodb.get({
  TableName: 'Accounts',
  Key: { accountId: 'A12345' },
  ConsistentRead: true  // Strong consistency required
}).promise();

// Ensure latest balance before debit
if (account.Item.balance >= withdrawalAmount) {
  await processWithdrawal();
}
  1. Inventory management
// Stock levels - prevent overselling
const product = await dynamodb.get({
  TableName: 'Inventory',
  Key: { productId: 'PROD-789' },
  ConsistentRead: true
}).promise();

if (product.Item.quantity >= orderQuantity) {
  await reserveInventory();
}
  1. Read-after-write scenarios
// Create order then immediately display
await dynamodb.put({
  TableName: 'Orders',
  Item: { orderId: 'ORD-001', status: 'PENDING' }
}).promise();

// Read immediately with strong consistency
const order = await dynamodb.get({
  TableName: 'Orders',
  Key: { orderId: 'ORD-001' },
  ConsistentRead: true
}).promise();

return order.Item;  // Guaranteed to show 'PENDING' status
  1. Critical data verification
// User authentication - must have latest credentials
const user = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: '12345' },
  ConsistentRead: true
}).promise();

// Verify password against latest hash
const isValid = await verifyPassword(
  inputPassword,
  user.Item.passwordHash
);

Consistency in Distributed Scenarios

Multi-Region Considerations

When using DynamoDB Global Tables, additional consistency considerations apply:
// Global Tables provide eventual consistency across regions
// Writes in us-east-1
await dynamodbUSEast.put({
  TableName: 'GlobalUsers',
  Item: { userId: '12345', region: 'us-east-1', lastUpdate: Date.now() }
}).promise();

// Read in eu-west-1 (may not immediately see write)
const user = await dynamodbEUWest.get({
  TableName: 'GlobalUsers',
  Key: { userId: '12345' }
}).promise();

// Typical propagation: < 1 second
// During network partitions: may take longer
Global Table Consistency Timeline:
<svg viewBox="0 0 900 400" xmlns="http://www.w3.org/2000/svg">
  <!-- Title -->
  <text x="450" y="30" font-size="18" font-weight="bold" text-anchor="middle" fill="#333">
    Global Tables Cross-Region Replication
  </text>

  <!-- Timeline -->
  <line x1="100" y1="200" x2="800" y2="200" stroke="#333" stroke-width="2"/>

  <!-- Time markers -->
  <text x="100" y="230" font-size="12" text-anchor="middle" fill="#666">T0</text>
  <text x="250" y="230" font-size="12" text-anchor="middle" fill="#666">T1</text>
  <text x="400" y="230" font-size="12" text-anchor="middle" fill="#666">T2</text>
  <text x="550" y="230" font-size="12" text-anchor="middle" fill="#666">T3</text>
  <text x="700" y="230" font-size="12" text-anchor="middle" fill="#666">T4</text>

  <!-- US East Region -->
  <rect x="50" y="60" width="200" height="100" fill="#e3f2fd" stroke="#1976d2" stroke-width="2" rx="5"/>
  <text x="150" y="85" font-size="14" font-weight="bold" text-anchor="middle" fill="#1976d2">
    us-east-1
  </text>
  <text x="150" y="110" font-size="12" text-anchor="middle" fill="#333">
    Write: balance = 500
  </text>
  <text x="150" y="130" font-size="11" text-anchor="middle" fill="#666">
    T0: Write initiated
  </text>
  <text x="150" y="145" font-size="11" text-anchor="middle" fill="#666">
    T1: Write acknowledged
  </text>

  <!-- EU West Region -->
  <rect x="300" y="60" width="200" height="100" fill="#fff3e0" stroke="#ff9800" stroke-width="2" rx="5"/>
  <text x="400" y="85" font-size="14" font-weight="bold" text-anchor="middle" fill="#ff9800">
    eu-west-1
  </text>
  <text x="400" y="110" font-size="12" text-anchor="middle" fill="#333">
    Read: balance = ?
  </text>
  <text x="400" y="130" font-size="11" text-anchor="middle" fill="#666">
    T1: Old value (300)
  </text>
  <text x="400" y="145" font-size="11" text-anchor="middle" fill="#666">
    T3: New value (500)
  </text>

  <!-- AP South Region -->
  <rect x="550" y="60" width="200" height="100" fill="#e8f5e9" stroke="#4caf50" stroke-width="2" rx="5"/>
  <text x="650" y="85" font-size="14" font-weight="bold" text-anchor="middle" fill="#4caf50">
    ap-south-1
  </text>
  <text x="650" y="110" font-size="12" text-anchor="middle" fill="#333">
    Read: balance = ?
  </text>
  <text x="650" y="130" font-size="11" text-anchor="middle" fill="#666">
    T2: Old value (300)
  </text>
  <text x="650" y="145" font-size="11" text-anchor="middle" fill="#666">
    T4: New value (500)
  </text>

  <!-- Replication arrows -->
  <path d="M 250 120 L 300 120" stroke="#1976d2" stroke-width="2" fill="none" marker-end="url(#arrowhead)" stroke-dasharray="5,5"/>
  <path d="M 250 140 L 550 140" stroke="#1976d2" stroke-width="2" fill="none" marker-end="url(#arrowhead)" stroke-dasharray="5,5"/>

  <!-- Info Box -->
  <rect x="200" y="280" width="500" height="80" fill="#fff9c4" stroke="#fbc02d" stroke-width="2" rx="5"/>
  <text x="450" y="310" font-size="13" font-weight="bold" text-anchor="middle" fill="#333">
    Cross-Region Replication Latency
  </text>
  <text x="450" y="335" font-size="12" text-anchor="middle" fill="#666">
    Typical: 0.5-1 second | During partition: Several seconds
  </text>
  <text x="450" y="355" font-size="11" text-anchor="middle" fill="#666">
    Last-writer-wins conflict resolution with timestamps
  </text>

  <!-- Arrow marker -->
  <defs>
    <marker id="arrowhead" markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto">
      <polygon points="0 0, 10 3, 0 6" fill="#1976d2"/>
    </marker>
  </defs>
</svg>

Conflict Resolution

In multi-region setups, DynamoDB uses last-writer-wins based on timestamps:
// Concurrent writes to same item in different regions

// us-east-1 at T0
await dynamodbUSEast.update({
  TableName: 'GlobalUsers',
  Key: { userId: '12345' },
  UpdateExpression: 'SET #name = :name',
  ExpressionAttributeNames: { '#name': 'name' },
  ExpressionAttributeValues: { ':name': 'Alice' }
}).promise();

// eu-west-1 at T0 + 50ms
await dynamodbEUWest.update({
  TableName: 'GlobalUsers',
  Key: { userId: '12345' },
  UpdateExpression: 'SET #name = :name',
  ExpressionAttributeNames: { '#name': 'name' },
  ExpressionAttributeValues: { ':name': 'Bob' }
}).promise();

// Result after replication:
// Last write wins (T0 + 50ms): name = 'Bob'

// To handle conflicts, use version numbers:
await dynamodbUSEast.update({
  TableName: 'GlobalUsers',
  Key: { userId: '12345' },
  UpdateExpression: 'SET #name = :name, #version = :version',
  ConditionExpression: '#version = :currentVersion',
  ExpressionAttributeNames: {
    '#name': 'name',
    '#version': 'version'
  },
  ExpressionAttributeValues: {
    ':name': 'Alice',
    ':version': 2,
    ':currentVersion': 1
  }
}).promise();

Application-Level Consistency Patterns

Pattern 1: Optimistic Locking

Use version numbers to prevent lost updates:
// Read current version
const user = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: '12345' },
  ConsistentRead: true
}).promise();

const currentVersion = user.Item.version;

// Update with version check
try {
  await dynamodb.update({
    TableName: 'Users',
    Key: { userId: '12345' },
    UpdateExpression: 'SET #name = :name, #version = :newVersion',
    ConditionExpression: '#version = :currentVersion',
    ExpressionAttributeNames: {
      '#name': 'name',
      '#version': 'version'
    },
    ExpressionAttributeValues: {
      ':name': 'Updated Name',
      ':newVersion': currentVersion + 1,
      ':currentVersion': currentVersion
    }
  }).promise();

  console.log('Update successful');
} catch (error) {
  if (error.code === 'ConditionalCheckFailedException') {
    console.log('Version conflict - retry');
    // Retry with fresh read
  }
}

Pattern 2: Idempotent Writes

Use client tokens to prevent duplicate writes:
// Idempotent payment processing
const processPayment = async (paymentId, amount) => {
  try {
    await dynamodb.put({
      TableName: 'Payments',
      Item: {
        paymentId: paymentId,  // Unique ID from client
        amount: amount,
        status: 'COMPLETED',
        timestamp: new Date().toISOString()
      },
      ConditionExpression: 'attribute_not_exists(paymentId)'
    }).promise();

    return { success: true, message: 'Payment processed' };
  } catch (error) {
    if (error.code === 'ConditionalCheckFailedException') {
      // Payment already exists - idempotent success
      return { success: true, message: 'Payment already processed' };
    }
    throw error;
  }
};

// Client can safely retry
await processPayment('PAY-12345', 99.99);  // First call
await processPayment('PAY-12345', 99.99);  // Retry - no duplicate charge

Pattern 3: Event Sourcing

Store events immutably for eventual consistency:
// Append-only event log
const addEvent = async (aggregateId, event) => {
  await dynamodb.put({
    TableName: 'EventStore',
    Item: {
      aggregateId: aggregateId,
      eventId: `${Date.now()}-${randomUUID()}`,
      eventType: event.type,
      eventData: event.data,
      timestamp: new Date().toISOString()
    }
  }).promise();
};

// Rebuild state from events
const getAggregateState = async (aggregateId) => {
  const events = await dynamodb.query({
    TableName: 'EventStore',
    KeyConditionExpression: 'aggregateId = :id',
    ExpressionAttributeValues: {
      ':id': aggregateId
    },
    ConsistentRead: false  // Eventual consistency acceptable
  }).promise();

  // Apply events to rebuild state
  return events.Items.reduce((state, event) => {
    return applyEvent(state, event);
  }, initialState);
};

Pattern 4: CQRS (Command Query Responsibility Segregation)

Separate read and write models:
// Write model - strong consistency
const createOrder = async (order) => {
  await dynamodb.put({
    TableName: 'Orders',
    Item: {
      orderId: order.id,
      userId: order.userId,
      status: 'PENDING',
      items: order.items,
      total: order.total,
      createdAt: new Date().toISOString()
    }
  }).promise();

  // Emit event for read model update
  await eventBus.publish({
    type: 'ORDER_CREATED',
    data: order
  });
};

// Read model - eventual consistency (optimized for queries)
const getOrderHistory = async (userId) => {
  return await dynamodb.query({
    TableName: 'OrderHistory',  // Separate read-optimized table
    IndexName: 'UserOrdersIndex',
    KeyConditionExpression: 'userId = :userId',
    ExpressionAttributeValues: {
      ':userId': userId
    },
    ConsistentRead: false,  // Eventual consistency for performance
    ScanIndexForward: false
  }).promise();
};

// Event handler updates read model
const handleOrderCreated = async (event) => {
  await dynamodb.put({
    TableName: 'OrderHistory',
    Item: {
      userId: event.data.userId,
      orderId: event.data.id,
      orderDate: event.data.createdAt,
      total: event.data.total,
      status: event.data.status
    }
  }).promise();
};

Consistency Monitoring

Detecting Consistency Issues

// Monitor consistency lag
const monitorConsistency = async (itemId) => {
  const writeTimestamp = Date.now();

  // Write with timestamp
  await dynamodb.put({
    TableName: 'TestItems',
    Item: {
      itemId: itemId,
      writeTimestamp: writeTimestamp,
      value: 'test-value'
    }
  }).promise();

  // Immediate eventual read
  const eventualRead = await dynamodb.get({
    TableName: 'TestItems',
    Key: { itemId: itemId },
    ConsistentRead: false
  }).promise();

  const eventualLag = Date.now() - (eventualRead.Item?.writeTimestamp || writeTimestamp);

  // Strong read
  const strongRead = await dynamodb.get({
    TableName: 'TestItems',
    Key: { itemId: itemId },
    ConsistentRead: true
  }).promise();

  const strongLag = Date.now() - strongRead.Item.writeTimestamp;

  return {
    eventualConsistencyLag: eventualLag,
    strongConsistencyLag: strongLag,
    eventualFoundData: !!eventualRead.Item,
    strongFoundData: !!strongRead.Item
  };
};

// Run monitoring
const metrics = await monitorConsistency('test-123');
console.log('Eventual lag:', metrics.eventualConsistencyLag, 'ms');
console.log('Strong lag:', metrics.strongConsistencyLag, 'ms');

CloudWatch Metrics

// Monitor read consistency choices
const AWS = require('aws-sdk');
const cloudwatch = new AWS.CloudWatch();

const trackConsistencyMetrics = async (operation, consistencyType) => {
  await cloudwatch.putMetricData({
    Namespace: 'DynamoDB/Consistency',
    MetricData: [
      {
        MetricName: 'ReadOperations',
        Dimensions: [
          { Name: 'Operation', Value: operation },
          { Name: 'ConsistencyType', Value: consistencyType }
        ],
        Value: 1,
        Unit: 'Count',
        Timestamp: new Date()
      }
    ]
  }).promise();
};

// Track in your application
const getUser = async (userId, strongConsistency = false) => {
  await trackConsistencyMetrics('GetUser', strongConsistency ? 'Strong' : 'Eventual');

  return await dynamodb.get({
    TableName: 'Users',
    Key: { userId: userId },
    ConsistentRead: strongConsistency
  }).promise();
};

Interview Questions and Answers

Question 1: Explain the difference between eventual consistency and strong consistency in DynamoDB.

Answer: Eventual Consistency (default):
  • Reads may return stale data immediately after writes
  • Data becomes consistent typically within one second
  • Reads from any of the three replicas
  • Consumes half the RCUs (0.5 RCU per 4KB read)
  • Best for high throughput and cost optimization
  • Example: Social media feeds, analytics
Strong Consistency:
  • Always returns the most recent data
  • Reads coordinate across replicas to ensure latest value
  • Consumes double the RCUs (1 RCU per 4KB read)
  • Slightly higher latency due to coordination
  • Best for critical data accuracy
  • Example: Financial transactions, inventory
Key trade-off: Performance and cost vs. data freshness

Question 2: Can you use strong consistency with Global Secondary Indexes?

Answer: No. GSIs only support eventually consistent reads. Reason: GSIs are asynchronously updated from the base table. The propagation from base table to GSI typically takes milliseconds but is not instantaneous. Workaround:
// If you need strong consistency for a GSI query pattern
// Option 1: Query base table with strong consistency if possible
const result = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: userId },  // Base table key
  ConsistentRead: true
}).promise();

// Option 2: Accept eventual consistency and design for it
const gsiResult = await dynamodb.query({
  TableName: 'Users',
  IndexName: 'EmailIndex',
  KeyConditionExpression: 'email = :email',
  ExpressionAttributeValues: { ':email': email }
  // ConsistentRead not available
}).promise();

// Option 3: Use optimistic locking with version numbers
// to detect stale reads

Question 3: How does DynamoDB handle consistency in multi-region Global Tables?

Answer: Consistency Model: Global Tables provide eventual consistency across regions with last-writer-wins conflict resolution. How it works:
  1. Writes succeed in the local region immediately
  2. Changes replicate asynchronously to other regions
  3. Typical replication latency: < 1 second
  4. During conflicts, timestamp determines winner
Example:
// us-east-1: Write at T0
write(userId: '123', name: 'Alice', timestamp: T0)

// eu-west-1: Write at T0 + 50ms
write(userId: '123', name: 'Bob', timestamp: T0 + 50ms)

// Final state after replication:
// name = 'Bob' (later timestamp wins)
Handling conflicts:
  • Use version numbers for optimistic locking
  • Design idempotent operations
  • Implement application-level conflict resolution
  • Consider region affinity for related writes

Question 4: What is read-after-write consistency and when does DynamoDB provide it?

Answer: Read-after-write consistency guarantees that a read immediately following a write will see that write. DynamoDB provides this for:
  1. GetItem/Query on the same partition key immediately after PutItem/UpdateItem
  2. Within the same request thread/session
  3. TransactGetItems following TransactWriteItems
Example:
// Write
await dynamodb.put({
  TableName: 'Users',
  Item: { userId: '123', name: 'Alice' }
}).promise();

// Immediate read (read-after-write consistency)
const user = await dynamodb.get({
  TableName: 'Users',
  Key: { userId: '123' }
}).promise();
// Guaranteed to return name: 'Alice'
Not provided for:
  • GSI reads (eventual only)
  • Different client sessions
  • BatchGetItem operations

Question 5: How would you implement a bank account system requiring strong consistency?

Answer:
class BankAccountService {
  async transfer(fromAccount, toAccount, amount) {
    // Use transactions for atomicity and consistency
    try {
      await dynamodb.transactWrite({
        TransactItems: [
          {
            // Debit from account
            Update: {
              TableName: 'Accounts',
              Key: { accountId: fromAccount },
              UpdateExpression: 'SET balance = balance - :amount, version = version + :inc',
              ConditionExpression: 'balance >= :amount AND version = :currentVersion',
              ExpressionAttributeValues: {
                ':amount': amount,
                ':inc': 1,
                ':currentVersion': await this.getVersion(fromAccount)
              }
            }
          },
          {
            // Credit to account
            Update: {
              TableName: 'Accounts',
              Key: { accountId: toAccount },
              UpdateExpression: 'SET balance = balance + :amount, version = version + :inc',
              ExpressionAttributeValues: {
                ':amount': amount,
                ':inc': 1
              }
            }
          }
        ]
      }).promise();

      return { success: true };
    } catch (error) {
      if (error.code === 'TransactionCanceledException') {
        return { success: false, reason: 'Insufficient funds or version conflict' };
      }
      throw error;
    }
  }

  async getBalance(accountId) {
    // Always use strong consistency for balance reads
    const result = await dynamodb.get({
      TableName: 'Accounts',
      Key: { accountId: accountId },
      ConsistentRead: true  // Critical for accurate balance
    }).promise();

    return result.Item.balance;
  }

  async getVersion(accountId) {
    const result = await dynamodb.get({
      TableName: 'Accounts',
      Key: { accountId: accountId },
      ProjectionExpression: 'version',
      ConsistentRead: true
    }).promise();

    return result.Item.version;
  }
}
Key points:
  • Use transactions for atomic multi-item updates
  • Always use strong consistency for balance reads
  • Implement version numbers for optimistic locking
  • Use conditional expressions to prevent invalid states

Question 6: What are the cost implications of using strong consistency?

Answer: Read Capacity Units (RCUs):
  • Eventual consistency: 0.5 RCU per 4KB read
  • Strong consistency: 1 RCU per 4KB read
  • Cost difference: 2x more expensive
Example calculation:
// Scenario: 1 million reads per day, 1KB average item size

// Eventual consistency
const eventualRCUs = 1_000_000 * 0.5 * (1/4) = 125,000 RCUs
const eventualCost = (125_000 / 1_000_000) * $0.25 = $0.03 per day

// Strong consistency
const strongRCUs = 1_000_000 * 1 * (1/4) = 250,000 RCUs
const strongCost = (250_000 / 1_000_000) * $0.25 = $0.06 per day

// Monthly difference: ~$0.90 for this workload
Recommendations:
  • Use eventual consistency by default
  • Reserve strong consistency for critical operations
  • Monitor your consistency usage patterns
  • Consider caching for frequently read data

Question 7: How do you test consistency behavior in your application?

Answer:
const testConsistencyBehavior = async () => {
  const testId = `test-${Date.now()}`;

  // Test 1: Write and immediate eventual read
  await dynamodb.put({
    TableName: 'TestTable',
    Item: { id: testId, value: 'initial', timestamp: Date.now() }
  }).promise();

  const eventualRead = await dynamodb.get({
    TableName: 'TestTable',
    Key: { id: testId },
    ConsistentRead: false
  }).promise();

  console.log('Eventual read found data:', !!eventualRead.Item);

  // Test 2: Write and immediate strong read
  const strongRead = await dynamodb.get({
    TableName: 'TestTable',
    Key: { id: testId },
    ConsistentRead: true
  }).promise();

  console.log('Strong read found data:', !!strongRead.Item);

  // Test 3: Update and concurrent reads
  await dynamodb.update({
    TableName: 'TestTable',
    Key: { id: testId },
    UpdateExpression: 'SET #value = :value',
    ExpressionAttributeNames: { '#value': 'value' },
    ExpressionAttributeValues: { ':value': 'updated' }
  }).promise();

  // Race condition test
  const [eventual1, eventual2, strong] = await Promise.all([
    dynamodb.get({ TableName: 'TestTable', Key: { id: testId }, ConsistentRead: false }).promise(),
    dynamodb.get({ TableName: 'TestTable', Key: { id: testId }, ConsistentRead: false }).promise(),
    dynamodb.get({ TableName: 'TestTable', Key: { id: testId }, ConsistentRead: true }).promise()
  ]);

  console.log({
    eventual1: eventual1.Item?.value,
    eventual2: eventual2.Item?.value,
    strong: strong.Item?.value
  });

  // Test 4: GSI consistency lag
  const writeTime = Date.now();
  await dynamodb.put({
    TableName: 'TestTable',
    Item: {
      id: testId,
      value: 'test',
      gsiKey: 'indexed-value',
      timestamp: writeTime
    }
  }).promise();

  let gsiFound = false;
  let attempts = 0;
  const maxAttempts = 10;

  while (!gsiFound && attempts < maxAttempts) {
    const gsiResult = await dynamodb.query({
      TableName: 'TestTable',
      IndexName: 'GSI1',
      KeyConditionExpression: 'gsiKey = :key',
      ExpressionAttributeValues: { ':key': 'indexed-value' }
    }).promise();

    gsiFound = gsiResult.Items.length > 0;
    if (!gsiFound) {
      await new Promise(resolve => setTimeout(resolve, 100));
    }
    attempts++;
  }

  console.log('GSI propagation took', attempts * 100, 'ms');
};

Question 8: Explain the CAP theorem and how DynamoDB fits into it.

Answer: CAP Theorem: In a distributed system, you can guarantee at most two of:
  • Consistency: All nodes see the same data
  • Availability: Every request gets a response
  • Partition tolerance: System works despite network failures
DynamoDB’s choice: AP (Availability + Partition Tolerance) Why:
  1. Availability is critical for most applications
  2. Network partitions are inevitable in distributed systems
  3. Partition tolerance is non-negotiable
How DynamoDB handles it:
  • Prioritizes availability during network partitions
  • Offers eventual consistency by default (favors A over C)
  • Provides strong consistency as an option (temporarily favors C)
  • Uses quorum-based replication (write to 2 of 3 replicas)
Example during partition:
Normal operation:
Write → [AZ1, AZ2, AZ3] → Success (2 of 3 ACKs required)

Network partition (AZ3 isolated):
Write → [AZ1, AZ2] → Success (still 2 of 3)
Read from AZ1/AZ2 → Consistent
Read from AZ3 → Potentially stale (eventual consistency)

Strong consistency read → Routes to AZ1 or AZ2 (majority)
Trade-off: Slight staleness (< 1s) for high availability

Question 9: How would you design a system that needs both high throughput and strong consistency?

Answer: Strategy: Hybrid approach using CQRS pattern
// Write path - Strong consistency where needed
class WriteService {
  async createOrder(order) {
    // Critical write with transaction
    await dynamodb.transactWrite({
      TransactItems: [
        {
          // Decrement inventory
          Update: {
            TableName: 'Inventory',
            Key: { productId: order.productId },
            UpdateExpression: 'SET quantity = quantity - :qty',
            ConditionExpression: 'quantity >= :qty',
            ExpressionAttributeValues: { ':qty': order.quantity }
          }
        },
        {
          // Create order
          Put: {
            TableName: 'Orders',
            Item: {
              orderId: order.id,
              userId: order.userId,
              status: 'PENDING',
              createdAt: new Date().toISOString()
            }
          }
        }
      ]
    }).promise();

    // Async: Update read-optimized denormalized view
    await eventBus.publish({
      type: 'ORDER_CREATED',
      data: order
    });

    return order;
  }
}

// Read path - Eventual consistency for performance
class ReadService {
  async getOrderHistory(userId) {
    // High-throughput eventual consistent reads
    return await dynamodb.query({
      TableName: 'OrderHistory',  // Denormalized table
      KeyConditionExpression: 'userId = :userId',
      ExpressionAttributeValues: { ':userId': userId },
      ConsistentRead: false,  // Eventual for performance
      Limit: 100
    }).promise();
  }

  async getOrderDetails(orderId) {
    // Strong consistency for critical views
    return await dynamodb.get({
      TableName: 'Orders',
      Key: { orderId: orderId },
      ConsistentRead: true  // Strong for accuracy
    }).promise();
  }
}

// Caching layer for ultra-high throughput
class CachedReadService {
  constructor() {
    this.cache = new Redis();
  }

  async getProduct(productId) {
    // Check cache first
    const cached = await this.cache.get(`product:${productId}`);
    if (cached) {
      return JSON.parse(cached);
    }

    // Cache miss - read from DynamoDB
    const product = await dynamodb.get({
      TableName: 'Products',
      Key: { productId: productId },
      ConsistentRead: false  // Eventual for caching
    }).promise();

    // Cache for 5 minutes
    await this.cache.setex(
      `product:${productId}`,
      300,
      JSON.stringify(product.Item)
    );

    return product.Item;
  }
}
Key techniques:
  1. Use strong consistency only where critical (orders, inventory)
  2. Use eventual consistency for high-throughput reads (history, analytics)
  3. Implement caching for frequently accessed data
  4. Separate read and write models (CQRS)
  5. Use denormalization to reduce need for strong consistency

Question 10: What happens if you write to DynamoDB and immediately read from a GSI?

Answer: Short answer: The read might not see the write immediately. Detailed explanation: Write to base table:
// T0: Write to base table
await dynamodb.put({
  TableName: 'Users',
  Item: {
    userId: '123',
    email: '[email protected]',
    name: 'Alice'
  }
}).promise();
// T1: Write acknowledged

// T1: Immediately query GSI
const result = await dynamodb.query({
  TableName: 'Users',
  IndexName: 'EmailIndex',
  KeyConditionExpression: 'email = :email',
  ExpressionAttributeValues: {
    ':email': '[email protected]'
  }
}).promise();

// Result: Might be empty! GSI not yet updated
Timeline:
T0: Base table write initiated
T1: Base table write acknowledged
T2: GSI asynchronously updated (typically < 100ms)
T3: GSI read sees the data

Query at T1.5: Empty (GSI not updated)
Query at T3: Returns data (GSI updated)
Solutions:
  1. Design for eventual consistency:
const createUser = async (user) => {
  await dynamodb.put({ TableName: 'Users', Item: user }).promise();

  // Return user directly, don't query GSI
  return user;
};
  1. Use base table for immediate reads:
const createUser = async (user) => {
  await dynamodb.put({ TableName: 'Users', Item: user }).promise();

  // Read from base table with strong consistency
  return await dynamodb.get({
    TableName: 'Users',
    Key: { userId: user.userId },
    ConsistentRead: true
  }).promise();
};
  1. Retry with exponential backoff:
const findUserByEmail = async (email, maxRetries = 3) => {
  for (let i = 0; i < maxRetries; i++) {
    const result = await dynamodb.query({
      TableName: 'Users',
      IndexName: 'EmailIndex',
      KeyConditionExpression: 'email = :email',
      ExpressionAttributeValues: { ':email': email }
    }).promise();

    if (result.Items.length > 0) {
      return result.Items[0];
    }

    // Wait before retry
    await new Promise(resolve => setTimeout(resolve, 100 * Math.pow(2, i)));
  }

  throw new Error('User not found after retries');
};

Summary

Key Takeaways:
  1. Consistency Models:
    • Eventual consistency: Default, cheaper, higher throughput
    • Strong consistency: Opt-in, more expensive, guaranteed fresh data
  2. When to Use What:
    • Eventual: Analytics, caching, high-throughput scenarios
    • Strong: Financial data, inventory, critical operations
  3. Limitations:
    • GSIs only support eventual consistency
    • Global Tables use eventual consistency across regions
    • BatchGetItem doesn’t support strong consistency
  4. Best Practices:
    • Use eventual consistency by default
    • Reserve strong consistency for critical paths
    • Implement application-level consistency (versioning, idempotency)
    • Monitor consistency metrics
  5. Patterns:
    • Optimistic locking for conflict detection
    • CQRS for separating read/write models
    • Event sourcing for audit trails
    • Caching for ultra-high throughput
Understanding consistency models is crucial for building correct, performant, and cost-effective DynamoDB applications.