Gaming profile schema design in DynamoDB
Gaming profile business use case
This use case talks about using DynamoDB to store player profiles for a gaming system. Users (in this case, players) need to create profiles before they can interact with many modern games, especially online ones. Gaming profiles typically include the following:
-
Basic information such as their user name
-
Game data such as items and equipment
-
Game records such as tasks and activities
-
Social information such as friend lists
To meet the fine-grained data query access requirements for this application, the primary keys (partition key and sort key) will use generic names (PK and SK) so they can be overloaded with various types of values as we will see below.
The access patterns for this schema design are:
-
Get a user's friend list
-
Get all of a player's information
-
Get a user's item list
-
Get a specific item from the user's item list
-
Update a user's character
-
Update the item count for a user
The size of the gaming profile will vary in different games. Compressing large attribute values can let them fit within item limits in DynamoDB and reduce costs. The throughput management strategy would depend various on factors such as: number of players, number of games played per second, and seasonality of the workload. Typically for a newly launched game, the number of players and the level of popularity are unknown so we will start with the on-demand throughput mode.
Gaming profile entity relationship diagram
This is the entity relationship diagram (ERD) we'll be using for the gaming profile schema design.
Gaming profile access patterns
These are the access patterns we'll be considering for the social network schema design.
-
getPlayerFriends
-
getPlayerAllProfile
-
getPlayerAllItems
-
getPlayerSpecificItem
-
updateCharacterAttributes
-
updateItemCount
Gaming profile schema design evolution
From the above ERD, we can see that this is a one-to-many relationship type of data
modeling. In DynamoDB, one-to-many data models can be organized into item collections,
which is different from traditional relational databases where multiple tables are
created and linked through foreign keys. An item collections is a group of items that share the same partition key value
but have different sort key values. Within an item collection, each item has a unique
sort key value that distinguishes it from other items. With this in mind, let’s use the
following pattern for HASH
and RANGE
values for each entity
type.
To begin, we use generic names like PK
and SK
to store
different types of entities in the same table to make the model future-proof. For better
readability, we can include prefixes to denote the type of data or include an arbitrary
attribute called Entity_type
or Type
. In the current example,
we use a string starting with player
to store player_ID
as the
PK
; use entity name#
as the prefix of SK
, and
add a Type
attribute to indicate which entity type this piece of data is.
This allows us to support storing more entity types in the future, and use advanced
technologies such as GSI Overloading and Sparse GSI to meet more access patterns.
Let’s start implementing the access patterns. Access patterns such as adding players
and adding equipment can be realized through the PutItem
operation, so we can ignore them. In this document,
we’ll focus on the typical access patterns listed above.
Step 1: Address access pattern 1
(getPlayerFriends
)
We address access pattern 1 (getPlayerFriends
) with this step. In our
current design, friendship is simple and the number of friends in the game is small. For
simplicity's sake, we use a list data type to store friend lists (1:1 modeling). In this
design, we use GetItem
to satisfy this access pattern. In the
GetItem
operation, we explicitly provide the partition key and sort key
value to get a specific item.
However, if a game has a large number of friends, and the relationships between them
are complex (such as friendships being bi-directional with both an invite and accept
component) it would be necessary to use a many-to-many relationship to store each friend
individually, in order to scale to an unlimited friend list size. And if the friendship
change involves operating on multiple items at the same time, DynamoDB transactions can be
used to group multiple actions together and submit them as a single all-or-nothing
TransactWriteItems
or TransactGetItems
operation.
Step 2: Address access patterns 2
(getPlayerAllProfile
), 3 (getPlayerAllItems
), and 4
(getPlayerSpecificItem
)
We address access patterns 2 (getPlayerAllProfile
), 3
(getPlayerAllItems
), and 4 (getPlayerSpecificItem
) using
this step. What these three access patterns have in common is a range query, which uses
the Query operation. Depending on the scope of
the query, Key Condition and Filter Expressions are used, which are
commonly used in practical development.
In the Query operation, we provide a single value for Partition Key and get all items
with that Partition Key value. Access pattern 2 (getPlayerAllProfile
) is
implemented in this way. Optionally, we can add a sort key condition expression — a
string that determines the items to be read from the table. Access pattern 3
(getPlayerAllItems
) is implemented by adding the key condition of sort
key begins_with ITEMS#
. Further, in order to simplify the development of
the application side, we can use filter expressions to implement access pattern 4
(getPlayerSpecificItem
).
Here's a pseudocode example using filter expression that filters items of the
Weapon
category:
filterExpression: "ItemType = :itemType" expressionAttributeValues: {":itemType": "Weapon"}
Note
A filter expression is applied after a Query finishes, but before the results are returned to the client. Therefore, a Query consumes the same amount of read capacity regardless of whether a filter expression is present.
If the access pattern is to query a large dataset and filter out a large amount of
data to keep only a small subset of data, the appropriate approach is to design DynamoDB
Partition Key and Sort Key more effectively. For example, in the above example for
obtaining a certain ItemType
, if there are many items for each player and
querying for a certain ItemType
is a typical access pattern, it would be
more efficient to bring ItemType
into the SK
as a composite
key. The data model would look like this: ITEMS#ItemType#ItemId
.
Step 3: Address access patterns 5
(updateCharacterAttributes
) and 6 (updateItemCount
)
We address access patterns 5 (updateCharacterAttributes
) and 6
(updateItemCount
) using this step. When the player needs to modify the
character, such as reducing the currency, or modifying the quantity of a certain weapon
in their items, use UpdateItem
to implement these access patterns. To update a
player's currency but ensure it never goes below a minimum amount, we can add a DynamoDB condition expression CLI example to reduce the balance only if it's
greater than or equal to the minimum amount. Here is a pseudocode example:
UpdateExpression: "SET currency = currency - :amount" ConditionExpression: "currency >= :minAmount"
When developing with DynamoDB and using Atomic Counters to decrement inventory, we can ensure idempotency by using optimistic locking. Here is a pseudocode example for Atomic Counters:
UpdateExpression: "SET ItemCount = ItemCount - :incr" expression-attribute-values: '{":incr":{"N":"1"}}'
In addition, in a scenario where the player purchases an item with currency, the
entire process needs to deduct currency and add an item at the same time. We can use
DynamoDB Transactions to group multiple actions together and submit them as a single
all-or-nothing TransactWriteItems
or TransactGetItems
operation. TransactWriteItems
is a synchronous and idempotent write
operation that groups up to 100 write actions in a single all-or-nothing operation. The
actions are completed atomically so that either all of them succeed or none of them
succeeds. Transactions help eliminate the risk of duplication or vanishing currency. For
more information on transactions, see DynamoDB transactions example .
All access patterns and how the schema design addresses them are summarized in the table below:
Access pattern | Base table/GSI/LSI | Operation | Partition key value | Sort key value | Other conditions/filters |
---|---|---|---|---|---|
getPlayerFriends | Base table | GetItem | PK=PlayerID | SK=“FRIENDS#playerID” | |
getPlayerAllProfile | Base table | Query | PK=PlayerID | ||
getPlayerAllItems | Base table | Query | PK=PlayerID | SK begins_with “ITEMS#” | |
getPlayerSpecificItem | Base table | Query | PK=PlayerID | SK begins_with “ITEMS#” | filterExpression: "ItemType = :itemType" expressionAttributeValues: { ":itemType": "Weapon" } |
updateCharacterAttributes | Base table | UpdateItem | PK=PlayerID | SK=“#METADATA#playerID” | UpdateExpression: "SET currency = currency - :amount" ConditionExpression: "currency >= :minAmount" |
updateItemCount | Base table | UpdateItem | PK=PlayerID | SK =“ITEMS#ItemID” | update-expression: "SET ItemCount = ItemCount - :incr" expression-attribute-values: '{":incr":{"N":"1"}}' |
Gaming profile final schema
Here is the final schema design. To download this schema design as a JSON file, see
DynamoDB Examples
Base table:
Using NoSQL Workbench with this schema design
You can import this final schema into NoSQL Workbench, a visual tool that provides data modeling, data visualization, and query development features for DynamoDB, to further explore and edit your new project. Follow these steps to get started:
-
Download NoSQL Workbench. For more information, see Download NoSQL Workbench for DynamoDB.
-
Download the JSON schema file listed above, which is already in the NoSQL Workbench model format.
-
Import the JSON schema file into NoSQL Workbench. For more information, see Importing an existing data model.
-
Once you've imported into NOSQL Workbench, you can edit the data model. For more information, see Editing an existing data model.
-
To visualize your data model, add sample data, or import sample data from a CSV file, use the Data Visualizer feature of NoSQL Workbench.