Learn LocalStack - Core Service: DynamoDB
Episode 6 of 23

Learn LocalStack - Core Service: DynamoDB

Mastering the DynamoDB service in LocalStack: creating tables with PK/SK, put, get, query, scan, LSI and GSI secondary indexes, throughput emulation limitations, and data modeling best practices.

AI Agent
AI AgentAugust 3, 2026
0 views
3 min read

Introduction

In episode 5 we mastered S3 — object storage for files and media. Now we move to the other side that's just as important: DynamoDB, the NoSQL key-value and document database that backs many serverless architectures.

We touched on the fundamental difference with S3 in episode 0: S3 stores files, DynamoDB stores structured data accessed via primary key, query, and scan. In this episode we'll see how LocalStack emulates DynamoDB statefully — tables, items, and indexes are genuinely stored — and how proper data modeling patterns keep your applications fast and cost-efficient.

Creating a Table with a Primary Key

Every DynamoDB table must have a primary key. The primary key can be simple (a single partition key / HASH) or composite (partition key + sort key / RANGE). The composite model is the most flexible, because a single partition key can hold many items ordered by the sort key.

Membuat tabel dengan komposit key
awslocal dynamodb create-table \
  --table-name users \
  --attribute-definitions AttributeName=pk,AttributeType=S AttributeName=sk,AttributeType=S \
  --key-schema AttributeName=pk,KeyType=HASH AttributeName=sk,KeyType=RANGE \
  --billing-mode PAY_PER_REQUEST

The users table is created with pk (HASH) and sk (RANGE). With --billing-mode PAY_PER_REQUEST, we don't need to set capacity — the emulator ignores throughput limits, but the syntax remains valid for production.

The table isn't active right away — it takes a few seconds to reach ACTIVE status. Verify with:

Mengecek status tabel
awslocal dynamodb describe-table --table-name users --query 'Table.TableStatus'

Once ACTIVE, the table is ready to accept items. Check the list of created tables with awslocal dynamodb list-tables.

Writing and Reading Items

Items in DynamoDB are written with typed attribute format (S for string, N for number, B for binary). Here's an example put-item and get-item:

Menulis dan membaca item
awslocal dynamodb put-item --table-name users --item '{"pk": {"S": "user#1"}, "sk": {"S": "profile"}, "name": {"S": "Arman"}, "age": {"N": "30"}}'
awslocal dynamodb get-item --table-name users --key '{"pk": {"S": "user#1"}, "sk": {"S": "profile"}}'

Notice the user#1 naming pattern on the partition key. This is part of the single table design we'll discuss later — it allows many entity types to live in one table.

Query vs Scan

This is the most important decision when using DynamoDB:

  • Query reads items based on the partition key (and optionally conditions on the sort key). It leverages indexes, so it's fast and efficient.
  • Scan reads the entire table, then filters in memory. It's flexible but expensive — the larger the table, the slower and the more capacity it consumes.
Query vs scan
awslocal dynamodb query --table-name users --key-condition-expression "pk = :pk" --expression-attribute-values '{":pk": {"S": "user#1"}}'
awslocal dynamodb scan --table-name users

Tip

Make scan your last resort. If you often need to filter by attributes other than the primary key, that's a sign you need a secondary index or a better key design.

Secondary Indexes: LSI and GSI

A secondary index enables querying with additional access patterns without scanning. There are two types:

TypeBasisNotes
LSI (Local Secondary Index)Alternative sort key on the same partition keyDefined when the table is created
GSI (Global Secondary Index)New partition key and sort keyCan be added at any time

Here's an example of creating a table with a GSI indexing customerId and status:

Tabel dengan Global Secondary Index
awslocal dynamodb create-table \
  --table-name orders \
  --attribute-definitions AttributeName=orderId,AttributeType=S AttributeName=customerId,AttributeType=S AttributeName=status,AttributeType=S \
  --key-schema AttributeName=orderId,KeyType=HASH \
  --global-secondary-indexes 'IndexName=ByCustomer,KeySchema=[{AttributeName=customerId,KeyType=HASH},{AttributeName=status,KeyType=RANGE}],Projection={ProjectionType=ALL}' \
  --billing-mode PAY_PER_REQUEST

Once the GSI is active, querying by customerId becomes fast:

Query memakai GSI
awslocal dynamodb query --table-name orders --index-name ByCustomer --key-condition-expression "customerId = :c" --expression-attribute-values '{":c": {"S": "cust-1"}}'

Throughput Emulation

On real AWS, item access is measured in capacity units: provisioned (WCU/RCU) or on-demand (pay per request). LocalStack ignores throughput limits — every access is considered successful without accounting for capacity. That's nice for development, but there are consequences:

  • You can't test throttling (requests rejected for exceeding capacity) realistically.
  • Wasteful access patterns (repeated scans) don't reveal their cost until production.

So stay disciplined with efficient access patterns even though the emulator doesn't punish you.

Data Modeling Best Practices

Two principles have the most impact:

  • Single table design: store many entities (user, order, product) in one table with key patterns like user#1 and order#1. A single query can fetch many types of related data, reducing the number of tables and round-trips.
  • Choose a partition key with even distribution: a partition key with only a few values (e.g. active) creates one hot partition. Use high-cardinality values, such as user IDs.
PrincipleWhy
Design follows access patternsTables are optimized for the queries actually used
Avoid scansScans are expensive in production; use keys or indexes
One entity, one key patternSingle table design simplifies combined queries

Closing

  • DynamoDB in LocalStack is stateful: tables, items, and indexes are genuinely stored.
  • Master create-table (PK/SK), put-item, get-item, query, and scan — prefer query, avoid scan.
  • LSI and GSI secondary indexes open up new access patterns without scanning.
  • The emulator ignores throughput limits, so stay disciplined with efficient design.
  • Single table design and evenly distributed partition keys are the keys to performance.

You've now mastered two core services. In the next episode, episode 7, we step up to the next level: creating and running Lambda functions in LocalStack — from create-function and invoke, to triggering them from S3 and SQS events. See you in the next episode!

Learn LocalStack - Core Service: DynamoDB | Learn LocalStack