How to model one-to-many relationships with AppSync and DynamoDB

Thank you to Josh for asking this question on the AppSync Masterclass forum. His original question goes like this:

Let’s say I want to add a one-to-many relationship from Profile to a new property called “Tag” (a complex object with “name” and “color” properties) so a user can define their own Tags. I would also like a Tweet to reference one or more of the user’s own Tags. The Tag(name, color) object can change over time (the “name” can be renamed for example) so I don’t think I want to copy it into a Tweet.

Do you have an example of something like that you could point me to? Would “Tag” be a new DynamoDB table in this case, or is there a better way to model it so it can be referenced in a Tweet? Would I store Tag ids in an array on the Tweet table item? Would I need a “creator” column in the Tag table, like we already have on the Tweet table?

To answer this question, there are two aspects we need to consider:

1. How to represent this one-to-many relationship in the GraphQL model?

2. How to model this one-to-many relationship in DynamoDB?

Both depend on if the tags array’s length is unbounded.

For example, if you include a tweets array in IProfile interface then I would say for sure that it’s an unbounded array. Whereas tags is usually a bounded array.

Modelling in GraphQL schema

If it’s a bounded array, then the example Josh included makes sense, the only change I’d make is to change the tags array to:

interface IProfile {
  ...
  tags: [Tag!]
}

This is so that you can’t return null in the tags array.

If it’s an unbounded array, like tweets, then you should return something like the TweetsPage type we have:

interface IProfile {
  ...
  tweets: TweetsPage!
}
type TweetsPage {
  tweets: [ITweet!]
  nextToken: String
}

Essentially, you attach the first page of the user’s tweets (with a pre-determined page size) and give the caller a way to fetch more if they want (with a query like getTweets(limit: Int!, nextToken: String)).

Modelling in DynamoDB

For a bounded array, I find it easier (so long the nested items aren’t huge, and the max array size is relatively small) to nest the array in the Profile object.

For an unbounded array, nesting wouldn’t work because there’s a hard limit on the size of each DynamoDB object. And the cost of reading the item from DynamoDB can be excruciating since DynamoDB calculate read units based on the size of the items returned.

You can model these in a number of ways.

Using single-table design this would be how you do it.

Or you could follow the approach we have taken in the AppSync Masterclass and put them in a separate table (like the TweetsTable where the HASH key is the userId).

Personally, I’d go with using a separate table – it’s just much simpler:

  • You write less custom VTL code in VTL templates.
  • It’s easy to understand what data you have in a table.
  • You can monitor the cost for different tables and see how much each type of data is costing you to store and access and therefore where to optimize your data access code.
  • You can (more easily) use DynamoDB streams to react to data changes.
  • It’s easy to follow the least privilege principle and restrict access to data in DynamoDB.
  • Plus, GraphQL and AppSync are great at stitching the data together.

If you listen to our last live Q&A session, we talked about the benefits of single-table design and why in most cases they wouldn’t matter to you anyway unless you’re running a very high throughput workload and you need to optimize for cost.

And if you want to learn more about AppSync and GraphQL, then check out my video course – the AppSync Masterclass.