The Pattern
The adjacency list pattern uses a consistent key structure across all item types in a single table:
- Partition Key (PK): The ID of the "source" node — the entity you are querying from.
- Sort Key (SK): The ID of the "target" node or a relationship type prefix — what you are querying to.
This means a single Query(PK="USER#alice") returns Alice's profile AND every relationship Alice has, all sorted by SK. You then filter by SK prefix to get only the relationship type you care about.
Example: Social Network (Followers)
You want to track Users and who they follow.
| PK | SK | Attributes |
|---|---|---|
USER#alice |
PROFILE |
{Name: "Alice", Bio: "..."} |
USER#alice |
FOLLOWS#bob |
{Since: "2023-01-15"} |
USER#alice |
FOLLOWS#charlie |
{Since: "2023-03-22"} |
USER#bob |
PROFILE |
{Name: "Bob", Bio: "..."} |
The Queries This Enables
- "Who is Alice?" →
Query(PK="USER#alice", SK="PROFILE")— returns one item. - "Who does Alice follow?" →
Query(PK="USER#alice", SK begins_with "FOLLOWS#")— returns all follow edges. - "Does Alice follow Bob?" →
GetItem(PK="USER#alice", SK="FOLLOWS#bob")— O(1) lookup.
The Reverse Query: Inverted Index (GSI)
The adjacency list answers "who does Alice follow?" easily. But "who follows Alice?" is the reverse traversal — and the main table's PK structure doesn't support it directly.
The solution is a Global Secondary Index (GSI) with inverted keys:
- GSI PK: The original SK value (e.g.,
FOLLOWS#alice) - GSI SK: The original PK value (e.g.,
USER#bob)
Now, querying the GSI for PK="FOLLOWS#alice" returns every item whose SK was FOLLOWS#alice — meaning every user who follows Alice. This is sometimes called the Inverted Index Pattern.
Key trade-off: Every write to the main table that includes a GSI key attribute triggers a write to the GSI as well. This doubles the write cost for those items. For high-write workloads, be deliberate about which attributes you project into the GSI — use KEYS_ONLY or INCLUDE projections rather than ALL to minimize storage and write amplification.