Skip to content

12. Operations and the ecosystem

There are no servers to patch, no replicas to promote and no vacuum to schedule. What replaces all of that is access control, client configuration and the tooling around the table — which is where DynamoDB operations actually live.

{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query", "dynamodb:PutItem",
"dynamodb:UpdateItem"],
"Resource": [
"arn:aws:dynamodb:eu-west-1:123456789012:table/AppData",
"arn:aws:dynamodb:eu-west-1:123456789012:table/AppData/index/*"
]
}
NOTE THE SECOND ARN. Index access is a SEPARATE
resource. A policy that grants Query on the table
but not on /index/* fails at runtime with
AccessDeniedException — and it is the single most
common IAM mistake with this service.

IAM conditions can restrict a caller to their own items, enforced by the service rather than by your code:

{
"Effect": "Allow",
"Action": ["dynamodb:GetItem", "dynamodb:Query"],
"Resource": "arn:aws:dynamodb:eu-west-1:…:table/AppData",
"Condition": {
"ForAllValues:StringEquals": {
"dynamodb:LeadingKeys": ["USER#${cognito-identity.amazonaws.com:sub}"],
"dynamodb:Attributes": ["PK", "SK", "name", "email"]
}
}
}
dynamodb:LeadingKeys restricts which PARTITION
KEYS this principal may touch
dynamodb:Attributes restricts which attributes
may be read or written
dynamodb:Select forbid SELECT ALL, forcing
projections

This is how mobile and browser clients can talk to DynamoDB directly without a backend — and it only works if your partition keys were designed around identity, which is one more argument for chapter 8’s method.

AT REST always on. choose the key:
AWS owned (default, free)
AWS managed (aws/dynamodb)
CUSTOMER MANAGED (your KMS key —
auditable, revocable, and the one
compliance teams ask for)
IN TRANSIT TLS, always.
NETWORK a VPC GATEWAY ENDPOINT keeps traffic off
the public internet and removes NAT
gateway data charges — which is
occasionally a larger line item than
the database itself.
DynamoDB Accelerator (DAX)
• a write-through cache in front of the table
• microsecond reads instead of milliseconds
• API-compatible: swap the client, keep the code
• an actual CLUSTER of nodes you pay for by the
hour — it is not serverless
USE IT WHEN DO NOT USE IT WHEN
the same items are read every read is unique
repeatedly, hot (cache hit rate ≈ 0)
reads vastly outnumber writes dominate
writes
sub-millisecond matters 5–10 ms is fine —
which is usually true

Before reaching for DAX, check whether eventually consistent reads (half price, chapter 6) or an application-level cache in something you already run — Redis, or a process-local LRU — solves the same problem for less.

Terminal window
docker run -p 8000:8000 amazon/dynamodb-local
aws dynamodb list-tables --endpoint-url http://localhost:8000
DYNAMODB LOCAL IS
✓ API-compatible: create tables, run queries,
test your access patterns
✓ ideal for unit and integration tests
DYNAMODB LOCAL IS NOT
✗ a performance simulator
✗ a capacity simulator — it does not throttle
the way the service does
✗ identical in every edge case, especially
around limits and index behaviour

Treat it as a correctness harness. Load tests belong against a real table, in a real region.

NoSQL Workbench is the other tool worth knowing: it models single-table designs visually, generates sample data, and produces the key structures from chapter 8 without the usual spreadsheet.

resource "aws_dynamodb_table" "app" {
name = "AppData"
billing_mode = "PAY_PER_REQUEST"
hash_key = "PK"
range_key = "SK"
attribute { name = "PK" type = "S" }
attribute { name = "SK" type = "S" }
attribute { name = "GSI1PK" type = "S" }
attribute { name = "GSI1SK" type = "S" }
global_secondary_index {
name = "GSI1"
hash_key = "GSI1PK"
range_key = "GSI1SK"
projection_type = "ALL"
}
ttl { attribute_name = "expiresAt" enabled = true }
point_in_time_recovery { enabled = true }
stream_enabled = true
stream_view_type = "NEW_AND_OLD_IMAGES"
deletion_protection_enabled = true
}

That resource carries most of this guide’s operational advice in one place — PITR on, deletion protection on, TTL configured, streams enabled. The Terraform guide covers the surrounding practice; CDK and CloudFormation express the same thing.

WATCH, PER TABLE AND PER INDEX
ConsumedRead/WriteCapacityUnits
ThrottledRequests, Read/WriteThrottleEvents
SuccessfulRequestLatency (p50, p99, by operation)
SystemErrors, UserErrors
ConditionalCheckFailedRequests
ReturnedItemCount vs ScannedCount ◄ efficiency
CONTRIBUTOR INSIGHTS
names the most-accessed and most-throttled KEYS.
the fastest path from "we are throttled" to
"this key is why". (ch 3, 13)
ScannedCount ≫ ReturnedItemCount means you are
reading and paying for data you then discard —
a filter doing an index's job. (ch 4)
UNIT TESTS DynamoDB Local, or a mocking
library. fast, and they prove
your key logic.
INTEGRATION TESTS a real table in a test
account, created and
destroyed per run. on-demand
billing makes this nearly
free.
LOAD TESTS a real table, provisioned or
on-demand, with traffic RAMPED
— a cold table splits as it
learns your pattern, so the
first 90 seconds are not
representative (ch 14)
WHAT TO ASSERT IN INTEGRATION TESTS
• every access pattern from the design document
(ch 8) returns what it should
• pagination works past one page — insert enough
items to cross 1 MB, or force a small Limit
• conditional writes fail as expected
• consumed capacity is within the expected range
— a regression here is a cost regression

That last assertion is unusual and worth adopting: capturing ConsumedCapacity in tests catches the day someone adds a filter where an index belonged.

COST ALLOCATION
tag every table with a team/service/environment
tag, activate the tags in Billing, and the
DynamoDB line item becomes attributable rather
than a single large number.
ACCOUNT SEPARATION
production and non-production in separate AWS
accounts is the simplest guarantee that a test
cannot scan a production table.
SERVICE LIMITS
tables per account, concurrent control-plane
operations, and on-demand throughput defaults
are all per-account quotas — raise them BEFORE
a launch, not during it.
config = Config(
retries={"max_attempts": 5, "mode": "adaptive"},
connect_timeout=1, read_timeout=2,
max_pool_connections=50,
)
ddb = boto3.resource("dynamodb", config=config) # created ONCE
• adaptive retry mode backs off on throttling
instead of amplifying it
• short timeouts: a DynamoDB call that takes two
seconds is not going to succeed
• create the client ONCE, outside the handler (ch 5)
• ReturnConsumedCapacity in dev and load tests
  • IAM is the only access control, and indexes are a separate resource ARN — forgetting /index/* is the classic permission bug.
  • Fine-grained conditions (dynamodb:LeadingKeys, dynamodb:Attributes) restrict a principal to its own items and attributes, which is what makes direct client access possible — if keys were designed around identity.
  • Encryption at rest is always on; choose a customer-managed KMS key when auditability matters. A VPC gateway endpoint keeps traffic private and removes NAT charges.
  • DAX is a write-through cache cluster for hot, repetitive reads; check eventually consistent reads and an existing cache first.
  • DynamoDB Local is a correctness harness, not a performance or capacity simulator. NoSQL Workbench helps design single-table models.
  • Express tables in infrastructure as code with PITR, deletion protection, TTL and streams configured — and read the plan before changing a GSI, which can mean delete-and-recreate.
  • Monitor per table and per index, and use Contributor Insights to name hot keys. ScannedCount ≫ ReturnedItemCount means a filter is doing an index’s job.
  • Configure the SDK: adaptive retries, short timeouts, one client instance, and ReturnConsumedCapacity while developing.

Next: tuning and operations — the levers that actually change latency and cost, and the checklist to run before you ship.

Written by Sepuri Sai Krishna

© 2026CC BY 4.0Source