The Type Attribute in DynamoDB
In SQL, a row's table is its type — a row in documents is a document. A
DynamoDB single table mixes every entity under one schema, so an item carries no
built-in answer to "what is this?".
The Type attribute puts that answer back: a plain string on every item naming the entity it represents.
What is the Type attribute in DynamoDB?
The Type attribute is a plain string you stamp on every item — like EntityType: "Document" — naming the entity that item represents. Because a single table mixes many entities under one schema, items carry no built-in type. The Type puts it back, so your code identifies rows, filters a GSI to one entity, and survives migrations.
- Stamp a Type on every write. One attribute —
EntityType: "Document"— on every item, no exceptions. It costs a few bytes and saves you later. - It identifies entities in a mixed partition. A
Queryreturns workspaces, documents, and comments together; the Type tells your code which is which without parsing key prefixes. - It powers single-entity filtering on a . Project the Type into an index and you can narrow an overloaded index to exactly one entity type.
- It's your escape hatch for migrations. When you export to re-model or move an entity to its own table, the Type is the column you split on.
Why a mixed table loses the type
Single-table design stores every entity in one
table behind generic keys like PK and SK. That's the whole point — one
Query returns a parent and its children together. But it means a partition is
heterogeneous.
Take a SaaS doc-collaboration app. One workspace partition holds the workspace record, its documents, and the comments on those documents:
| PK | SK | attributes |
|---|---|---|
| WS#acme | META | name, plan, seats |
| WS#acme | DOC#a1#META | title, owner, wordCount |
| WS#acme | DOC#a1#CMT#0007 | author, body, createdAt |
| WS#acme | DOC#a1#CMT#0008 | author, body, createdAt |
Query PK = "WS#acme" hands back all four items in one billed read. Now your
code has a list of raw items and no reliable way to say which is a document and
which is a comment — short of string-matching the SK, which is brittle the
moment your key format changes.
Stamp the Type on every item
The fix is one attribute on every write, naming the entity:
| PK | SK | EntityType | title |
|---|---|---|---|
| WS#acme | META | Workspace | — |
| WS#acme | DOC#a1#META | Document | Q3 Roadmap |
| WS#acme | DOC#a1#CMT#0007 | Comment | — |
Branching on item.EntityType === "Document" is a stable equality check.
Parsing SK.startsWith("DOC#") && SK.includes("#CMT#") is a guess that breaks
when you rev the key. The Type decouples your read logic from your key encoding —
that's the real win.
One read returns three entity types; the Type attribute routes each item to the right handler without touching the keys.
Filter a GSI down to one entity
The Type earns its keep on indexes. Say you add a GSI keyed on
GSI1PK = WS#acme, GSI1SK = updatedAt to list "everything recently changed in
this workspace, newest first". An overloaded index sweeps in documents and
comments — but a feed UI may want documents only.
Two ways to narrow it, and the difference is money:
| Approach | What it costs | When to use |
|---|---|---|
FilterExpression on Type | Reads all matching items, bills for all of them, drops non-matches after read | Mixed entities are rare in the result; quick to ship |
Sparse index (Type in GSI1PK) | Only the entity you want ever lands in the index | One entity dominates; you want zero waste |
A FilterExpression runs after items are read and after capacity is
consumed — AWS is explicit that filtering doesn't reduce read cost
(DynamoDB Developer Guide: FilterExpression).
Filtering on Type is honest, not free: you pay for the comments you throw away.
To narrow the feed to documents, the query carries a condition on the Type
attribute. Assemble the FilterExpression, names, and values with the
DynamoDB expression builder — it emits the
#t = :doc placeholder so you don't fat-finger a reserved word.
KeyConditionExpression GSI1PK = :ws
FilterExpression #t = :doc
ExpressionAttributeNames { "#t": "EntityType" }
ExpressionAttributeValues { ":ws": "WS#acme", ":doc": "Document" }
Want the index to carry only documents and skip the filter entirely? Write
GSI1PK only on document items — a .
Items without the GSI key never replicate into the index, so the read touches
documents alone. The Type attribute is what tells your writer which items
qualify.
Keep the value stable and singular
Pick the value once and treat it as an enum. Document, never sometimes Doc
and sometimes document — a drifting value is worse than no value, because your
equality checks pass on one casing and silently miss the other.
One Type per item. If an item feels like two entities, that's usually a modeling smell — it should be two items, each in its own collection or sort-key range, not one row wearing two hats.
The migration payoff
The reason to stamp the Type before you need it: re-modeling. The recommended re-model path is export, transform, reimport — and AWS documents bulk export to S3 for exactly this kind of offline reshaping (Exporting DynamoDB to S3).
When that day comes, the Type is the column you GROUP BY. Want to lift comments
into their own table, or renormalize the export into per-entity files for an
analytics warehouse? You split the dump on EntityType. Without it, you're back
to reverse-engineering keys across millions of rows.
Next steps
The Type attribute is cheap insurance: identify entities in a mixed read, filter an overloaded GSI, and split cleanly when you re-model. Stamp it on every write from day one — retrofitting it onto a live table means a full backfill.
Related reading: single-table design for the
mixed-partition pattern this serves, GSI vs LSI for
choosing the index shape behind a sparse index, and
Query vs Scan for why a FilterExpression never saves
you read cost.
Build the filter on the Type with the DynamoDB expression builder, and try DynoTable to browse a real mixed-entity table and see the Type column line up across every item.