DynamoDB and CodeWords: the access patterns come first
Key design decides what is possible, scans are the mistake, streams are the right trigger, and capacity mode decides the bill. Automating against a database you cannot query freely.
On this page
- What we'll cover
- Keys decide what you can ask
- Scans, and why they are the wrong answer
- What DynamoDB reaches
- Connecting it to CodeWords
- Seven automations worth building
- Streams, which are the right trigger
- Capacity mode, which decides the bill
- Single-table designs, from the outside
- Building it so it survives
- Limits worth knowing about
- What to build first
- Frequently asked questions
- Related reading
DynamoDB rewards knowing your access patterns in advance and punishes discovering new ones later. That is the trade it makes: predictable performance at any scale, in exchange for a query model that only answers questions your keys were designed for.
Automation arrives after the keys were designed, usually with a question nobody anticipated. How you answer it — without reaching for a scan — is most of what there is to say about automating against DynamoDB.
What we'll cover
- Keys decide what you can ask
- Scans, and why they are the wrong answer
- What DynamoDB reaches
- Connecting it to CodeWords
- Seven automations worth building
- Streams, which are the right trigger
- Capacity mode, which decides the bill
- Single-table designs, from the outside
- Building it so it survives
- Limits worth knowing about
- What to build first
- Frequently asked questions
Keys decide what you can ask
The partition key determines where an item lives, and every efficient read starts by knowing it.
The sort key orders items within a partition, which is what makes range queries and prefix matching possible.
Secondary indexes add access patterns — global indexes with a different partition key, local ones with a different sort key — and each carries storage and write cost.
Single-table designs pack several entity types into one table with composite keys, which is efficient and makes the data considerably harder to read casually.
Nothing supports an ad hoc query. There is no equivalent of adding a where clause to a column you did not index.
For automation, the practical sequence is: state the question, check whether an existing key or index answers it, and if not, decide between adding an index, maintaining a derived copy, or exporting for analysis. Reaching for a scan is the fourth option and rarely the right one.
Scans, and why they are the wrong answer
A scan reads every item and filters afterwards, so you pay for all of it regardless of how few match.
It competes with your application for capacity, which on provisioned tables means throttling real traffic.
It gets slower as the table grows, so an automation that worked at launch degrades invisibly.
Filter expressions do not reduce cost, only the response size. This is the detail that surprises people most.
The legitimate uses are narrow: a small table, a one-off migration, or a genuinely infrequent maintenance job on an off-peak schedule with rate limiting.
For everything else: add an index, maintain what you need through streams, or export to S3 and query there. All three are cheaper than a recurring scan and none is much work.
What DynamoDB reaches
Item operations — get, put, update, delete — individually or in batches, with conditional expressions.
Query retrieves items within a partition efficiently, optionally against a secondary index.
Transactions cover multiple items atomically, within limits.
Streams deliver an ordered, replayable record of changes, which is the right basis for reacting to anything.
Export to S3 produces a point-in-time snapshot without consuming table capacity, which is the answer to analytical questions.
Time to live expires items automatically, which is free cleanup if the design allows it.
Point-in-time recovery supports restore to any second in the retention window.
CloudWatch metrics report throttling, capacity consumption, and errors, which is where cost and health reporting comes from.
Connecting it to CodeWords
CodeWords connects to more than 3,000 integrations, and the connection is made once and reused.
- Open CodeWords and start a new automation.
- Describe what should happen in plain language to Cody, the automation builder: which table, which access pattern, and what should result.
- Authorize the connection with an IAM role scoped to the specific table and actions, rather than broad database access.
- Describe the exceptions: a conditional write that fails, a throttled request, an item missing an expected attribute.
- Test against a table restored from a backup before anything touches production.
You describe the outcome; Cody builds it, connects it, and deploys it. The free plan covers light use, with Pro at $39 per month and Business at $100 per month as usage grows; details are on the pricing page.
Seven automations worth building
Stream-driven synchronisation. Feed a warehouse, a search index, or another service from the stream, including deletions, so the downstream stays current without polling.
Aggregate maintenance. Counts and totals updated from the stream, because DynamoDB cannot compute them and every application eventually needs them.
Export-based reporting. Export to S3 on a schedule and answer analytical questions there, where a query costs nothing against your table.
Throttling and capacity reporting. Which tables and which indexes are throttling, and how capacity consumption is trending, which is both a cost and a reliability signal.
Data quality checks against the export, not the table — orphaned references, missing attributes, and items in impossible states.
Time to live monitoring. Whether expiry is working and what is accumulating, since a table designed to expire items and quietly not doing so grows without limit.
Cost attribution. Read and write consumption by table and index, so the team that added an index sees what it costs.
Streams, which are the right trigger
Streams are ordered per partition key, so changes to one item arrive in order, and changes across different items do not have a global order.
They are replayable within the retention window, which is how a consumer catches up after being down — and only within that window.
Choose the view type deliberately. New image, old image, both, or keys only. Both is what you need for anything comparing before and after, and it costs more.
Deletions appear, which is the thing polling cannot see and the usual cause of downstream systems holding records that no longer exist.
Consumers must be idempotent. Delivery is at-least-once and a retry will redeliver.
Watch for loops. A consumer writing back to the same table produces more stream records, which is an expensive mistake that compounds silently.
Capacity mode, which decides the bill
On-demand charges per request with no capacity planning. It suits unpredictable or spiky traffic and costs more per request at steady high volume.
Provisioned charges for capacity whether or not you use it, and is cheaper for predictable load. It throttles when exceeded, which is a correctness problem as well as a performance one.
Auto scaling on provisioned capacity reacts after the fact, so a sudden spike is throttled while scaling catches up.
Indexes have their own capacity. A global secondary index can throttle independently, and writes to the table fail when it does — a failure mode that is confusing the first time.
Automation adds load. A batch job against a provisioned table competes with your application, so schedule it off-peak or rate limit it deliberately.
A report on throttling events by table and index, weekly, catches both the cost problem and the reliability problem before a customer does.
Single-table designs, from the outside
If the table you are automating against uses single-table design, a few things are worth knowing before you open it.
Attribute names will be generic. Partition and sort keys are often named neutrally because they hold different things for different entity types, so the column names tell you nothing.
Entity type is usually an attribute. Find it, and the table becomes legible; without it you are guessing at what each item represents.
Composite sort keys encode hierarchy — a prefix identifying the type, then an identifier, then perhaps a timestamp — and the prefix is what makes range queries work.
Index overloading is normal. The same secondary index serves several access patterns for different entity types, which is efficient and unintuitive.
Ask for the access pattern document. Anyone who designed such a table has one, and it answers in a page what reverse engineering answers in a day.
None of this makes automation harder once you know it. It makes reading the table without knowing it nearly impossible, which is why the design document is the first thing to ask for.
Building it so it survives
Query, never scan, unless you have established that a scan is genuinely appropriate.
Make writes idempotent with conditional expressions, so a retry does not overwrite something changed in between.
Handle unprocessed items. Batch operations return items they did not process, and code that ignores that field loses writes silently.
Retry with exponential backoff on throttling, which is expected rather than exceptional.
Report the outcome. Items read and written, capacity consumed, and any throttling encountered.
Limits worth knowing about
Item size is capped, which matters for anything appending to a list.
Batch operations are capped at a fixed number of items, and transactions have their own lower limit.
Query results are paginated at a size limit, so a query returning many items needs pagination handled.
Stream retention is limited, so a consumer down beyond that window cannot catch up and needs a reconciliation from an export.
A hot partition throttles even when the table has capacity overall, which is a key design problem rather than a capacity one.
What to build first
Throttling and capacity reporting: which tables and indexes are throttling, and how consumption is trending, delivered weekly. It writes nothing, it reads from CloudWatch rather than your table, and throttling is simultaneously a cost signal and a reliability problem that shows up to customers as intermittent errors.
Two habits make the difference. Report indexes separately from tables, since a throttling index causes table writes to fail and the cause is not obvious. And include the trend, because capacity that is fine today and rising steadily is the thing worth acting on early.
Frequently asked questions
Why is my scan so expensive?
Because it reads every item and filters afterwards, and the filter reduces what comes back rather than what you pay for. Add an index, maintain a derived copy through streams, or export to S3 for analysis.
How do I run analytical queries?
Export to S3 and query there. DynamoDB is built for fast, known access patterns at any scale, and analytical questions against it are both slow and expensive. The export does not consume table capacity, which is the point.
Should I use streams or poll?
Streams. They are ordered per item, replayable within the retention window, and they include deletions, which polling on a timestamp cannot see. Just make the consumer idempotent, since delivery is at-least-once.
On-demand or provisioned capacity?
On-demand for unpredictable or spiky traffic and for anything new where you cannot forecast. Provisioned for steady, predictable load where it is cheaper — with the caveat that throttling when you exceed it is a correctness problem, not just a slow one.
Why did a write fail when the table has capacity?
Often a global secondary index throttling independently. Index capacity is separate, and writes to the table fail when the index cannot keep up. It is confusing the first time and obvious afterwards.
How do I add a new access pattern?
Add a global secondary index, maintain a derived view through streams, or export and query elsewhere — depending on whether the pattern is operational or analytical. Retrofitting it onto the existing keys with a scan is the option to avoid.
What happens if my stream consumer is down for too long?
Beyond the stream's retention window the changes are gone and you need a reconciliation from an export. It is worth building that reconciliation before you need it, since the moment you need it is during an incident.
How do I make sense of a single-table design?
Find the attribute holding the entity type, learn the sort key prefixes, and ask for the access pattern document — anyone who designed the table has one. Generic key names and overloaded indexes are normal and make the table unreadable without it.
Can time to live handle our cleanup?
Where the design allows an expiry timestamp per item, yes, and it costs nothing. Monitor that it is actually working, since a table designed to expire items and quietly not doing so grows without limit and nothing reports it.