DEV Community

Cover image for Singleton Items in DynamoDB
DynoTable
DynoTable

Posted on • Originally published at dynotable.com

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 Query or a Scan.

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 update 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 has a flat, predictable cost (at most 1 RCU for a small item) — 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.

Recognize 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:

PK SK attributes
SETTINGS#APP FLAGS#V1 signup_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. On on-demand in us-east-1, 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 — bloating the config
blob into a second 4 KB block doubles the billed read.

On the read path, when the app boots or a request lands, you GetItem the fixed
key, you cache the result. The flow:

Singleton Items in DynamoDB

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:

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:

Example Notes
# 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 memoize.
  • 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 item Per-entity item
Key Hardcoded constant (SETTINGS#APP) Templated with an id (USER#42)
How many Exactly one One per user / order / tenant
Typical read GetItem on the known key GetItem or Query by entity
Scope Whole application A single entity
Use for Global flags, config, system version Profiles, 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.

Top comments (0)