Intermediate7 min read

Singleton Items in DynamoDB

A singleton item is a single row with a fixed, hardcoded key that holds state for your whole application — not one record per user or per order, but one record, period. Feature flags, a config blob, a global kill-switch: the kind of thing a relational app would keep in a one-row settings table.

Coming from SQL, you'd reach for a config table with id = 1 and a SELECT * FROM config. In DynamoDB you do the same thing with a hardcoded partition key — and because you always know that key, you read it with a GetItem, not a Queryor aScan.

What is a singleton item in DynamoDB?

A singleton item is a single DynamoDB row stored under a fixed, hardcoded key that holds global state for your whole application — feature flags, a config blob, a system-wide version — rather than one record per user or order. Because you always know the key, you read it with a GetItem and update it with plus condition expressions.

  • A singleton is one item with a constant key. You hardcode the PK/SK in your code (e.g. CONFIG#GLOBAL) instead of templating in a user or order id.
  • Read it with GetItem, never Scan. You always know the full key, so a point read is one consistent, predictable RCU — no filter, no table walk.
  • It's a hot key by definition. Every request can touch the same partition, so cache it and keep the item small; don't make it a write bottleneck.
  • Mutate it safely with update + condition expressions, not read-modify-write in your app — that's where the lost-update race lives.

Recognise the pattern

You have global state when the data isn't scoped to any one entity. A few tells:

  • A flag that's the same for everyone (signup_enabled = false).
  • A blob of tunables your app reads on boot (rate limits, default quotas).
  • A counter or version number for the whole system, not per-row.

Anything scoped to a user, tenant, or order is not a singleton — that's an ordinary item keyed by that entity's id. The singleton is the leftover global slice that has nowhere else to live.

Give it a constant key

The whole pattern hinges on one decision: the key is a literal, not a template. For a global feature-flags item in an overloaded single table, pick a fixed prefix and a fixed value:

PKSKattributes
SETTINGS#APPFLAGS#V1signup_enabled, maintenance_mode, ai_search_enabled

PK = "SETTINGS#APP" and SK = "FLAGS#V1" are baked into the code. There's no user id, no tenant id — the application asks for exactly this item every time. That predictability is the point: a known key is a GetItem, and a GetItem is the cheapest, most consistent read DynamoDB offers.

The V1 suffix is deliberate. If the flag schema changes shape later, you write a FLAGS#V2 item and flip readers over, instead of mutating the live one in place. Versioning the singleton key buys you a clean migration seam.

Read it with GetItem

Because the key is fully known, you never Query and you never Scan for a singleton. A Scan reads the whole table and filters client-side — the classic Scan footgun — and it's absurd overkill for fetching one row you can address directly.

A GetItem against SETTINGS#APP / FLAGS#V1 returns the flags in a single strongly- or eventually-consistent read. AWS bills a GetItem of an item ≤ 4 KB as 0.5 RCU eventually-consistent or 1 RCU strongly-consistent (AWS read/write capacity docs). Keep the singleton small and that cost stays flat forever.

The read path is just: app boots or a request lands, you GetItem the fixed key, you cache the result. Here's the flow.

yesnoApp / requestGetItem PK=SETTINGS#APPSK=FLAGS#V1Item found?Use flags, cache locallyFall back to safe defaults

The fixed key turns a global lookup into one point read with a built-in default path.

Note the no branch: a missing singleton should never crash you. Default to the safe value (feature off, maintenance on) so a first-deploy gap or a bad key fails closed, not open.

Update it without a race

The trap is updating a singleton with read-modify-write in your app: you GetItem the flags, flip one in memory, then PutItem the whole thing back. Two concurrent writers both read the old item and the second Put clobbers the first's change. Lost update.

Two DynamoDB features kill the race without app-side locking:

  • Update expressions mutate one attribute server-side, leaving the rest untouched. No need to re-Put the whole item.
  • Condition expressions make the write succeed only if the item still looks the way you expect, so a stale write is rejected with ConditionalCheckFailedException (AWS condition expression docs).

To flip one flag, target just that attribute with a SET and guard it with a version bump so concurrent writers can't trample each other:

# UpdateItem
Key                  PK=SETTINGS#APP  SK=FLAGS#V1
UpdateExpression     SET signup_enabled = :on, schema_version = :next
ConditionExpression  schema_version = :current

If two writers race, the second one's schema_version = :current check fails and it retries against the fresh value. You can scaffold the names, values, and this exact expression shape in the DynamoDB Expression Builder before wiring it into code. For a deeper look at the operators, see the update-expression idioms guide.

Mind the hot key

A singleton is, by construction, a hot key — every part of your app may read the same partition. That's fine for reads if you cache, but it's the one real risk of the pattern.

  • Cache aggressively. Read the flags once per process (or per N seconds), not on every request. The singleton's value is the cheapest thing to memoise.
  • Don't make it a write hot spot. A flag toggled by an admin a few times a day is nothing. A singleton you increment on every request is a partition-throughput bottleneck — that's a counter problem, not a singleton.
  • Keep it small. Read cost scales with item size in 4 KB blocks. A bloated config blob makes every boot more expensive than it needs to be.

If you genuinely need a high-write global counter, the singleton is the wrong shape — shard it across N items and sum on read. That's a different pattern.

Singleton vs per-entity item

The line is simply what the data is scoped to.

Singleton itemPer-entity item
KeyHardcoded constant (SETTINGS#APP)Templated with an id (USER#42)
How manyExactly oneOne per user / order / tenant
Typical readGetItem on the known keyGetItem or Query by entity
ScopeWhole applicationA single entity
Use forGlobal flags, config, system versionProfiles, orders, anything per-id

If you find yourself wanting two singletons of the same kind, you don't have a singleton — you have a per-entity item and the entity is the thing you forgot to key by (per-tenant config, say).

Pitfalls and next steps

  • Don't Scan for it. You know the key; address it directly.
  • Don't read-modify-write it. Use update + condition expressions.
  • Don't let it go missing silently. Default to the safe value on a cache miss.
  • Don't overload it with high-frequency writes. That's a sharded-counter job.

The singleton lives comfortably inside a single-table design — it's just one more item collection with a fixed key alongside your entity rows.

Try DynoTable to browse your table, find the singleton row by its fixed key, and edit flags by hand while you build the write path.

Updated