Graphcool Schema for Poll App


#1

I’m looking to create the architecture for a Poll app (similar to strawpoll, eventually). Ill need to store the Poll itself, the [dynamic] amount of options, and the number of votes per option.

This is my current structure:

type Poll implements Node {
  id: ID! @isUnique
  title: String!
  createdAt: DateTime!
  updatedAt: DateTime!
  options: [String!]
}

Of course this only saves the Poll and the Options, no Voting values. I’m not sure how I could go about linking how many votes each option gets to the current Poll. Possibly some @relation magic but coming from mongo this quite the change in architecture :disappointed_relieved:


#2

Thanks for your question, that’s a super interesting topic to discuss.

Your question boils down to designing the data schema, and as with all kinds of designs, we first have to understand the available primitives. In our case, we have:

  • Types, to model the entities we need in our system
  • (Scalar) Fields, defining the shape of a single type
  • Relations, describing the interaction between different types

The way you design your data schema using these three primitives directly affects the available operations in the generated GraphQL API you obtain, so let’s keep that in mind for later.

Which data schema fits your use case the best depends on different factors. Here are some features that we might want to support:

Basic Features

These are basic features we need for a poll app. A design that does not support all of them is not suitable:

  1. polls
  2. options
  3. anonymous voting

Advanced Features

There are many features we might want to add at a later point. They are not needed for a first version, but we should try to make it easy to add them later. Let’s take this one as an example:

  1. user voting

So, let’s take a look at a few different proposals that we can compare given these 4 features.

Option A

Built on top of your current structure, I simply added an array of integers to store the vote counts:

type Poll implements Node {
  id: ID! @isUnique
  title: String!
  createdAt: DateTime!
  updatedAt: DateTime!
  options: [String!]
  voteCounts: [Int!]
}
  • The type Poll models a single poll. An instance of a Poll is called a node.
  • A Poll node has several options that are modelled as a list of strings.
  • Similarly, a Poll node has several voteCounts that store the number of votes each option has received.

Let’s go through the list of features, and evaluate this design:

  1. :white_check_mark: polls: the Poll type allows us to manage polls easily. We can use the createPoll, updatePoll and deletePoll mutations to manage polls, and allPolls to query them.
  2. :question: option: the options field can store many options. however, the GraphQL API only supports setting the options field. We cannot add or remove single strings to/from the list, we always have to replace the value with the whole list. It works, but is tedious.
  3. :question: anonymous voting: this is similar as 2. Another added complexity is that the vote count and the option need to be coupled by some system (probably by the index in the two arrays), which is tedious and error-prone.
  4. :x: user voting: While 2. and 3. already started to be difficult to be implemented with this design, adding users to our design is just unfeasible. We could add a relation between User and Poll, but this doesn’t link the option/vote to the user.

Option B

While the available tools leave room for a lot of different designs that are more advanced, this is my personal favorite:

type Poll implements Node {
  id: ID! @isUnique
  title: String!
  createdAt: DateTime!
  updatedAt: DateTime!
  options: [Option!]! @relation(name: "PollOptions")
}

enum Color {
  red,
  blue,
  green
}

type Option {
  id: ID! @isUnique
  name: String!
  color: Color!
  createdAt: DateTime!
  updatedAt: DateTime!
  poll: Poll @relation(name: "PollOptions")
  votes: [Vote!]! @relation(name: "OptionVotes")
}

type Vote {
  id: ID! @isUnique
  createdAt: DateTime!
  updatedAt: DateTime!
  option: Option @relation(name: "OptionVotes")
  user: User @relation(name: "UserVotes")
}

type User {
  id: ID! @isUnique
  name: String!
  createdAt: DateTime!
  updatedAt: DateTime!
  votes: [Vote!]! @relation(name: "UserVotes")
}

Again, let’s take a look at our feature list:

Let’s go through the list of features, and evaluate this design:

  1. :white_check_mark: polls: nothing changed about the the Poll type compared to our previous design.

  2. :white_check_mark: option: the options relation connects a poll with its possible options. For example, it’s easy to query all options for a given vote, using

    query voteOptions($id: ID!) {
      allOptions(filter: {
        vote: {
          id: $id
        }
      }) {
        id
      }
    }
    

    I also added the color field (and enum), to emphasize that, by including Option as a first-level concept in our data schema, it’s easy to attach new properties to it. Adding an option color in design A would be very complicated.

  3. :white_check_mark: anonymous voting: simply doing

    mutation anonymousVote($id: ID!) {
      createVote(
        optionId: $id
      ) {
        id
      }
    }
    

    creates a new anonymous vote.

  4. :white_check_mark: user voting: we can adjust the solution for 3 like this:

    mutation anonymousVote($optionId: ID!, $userId: ID!) {
      createVote(
        optionId: $optionId
        userId: $userId
      ) {
        id
      }
    }
    

    to create a new user vote.

In general, it should be clear that design B is clearly more elegant for the basic features, while also offering high extensibility in terms of future advanced features.

I hope all of that makes sense! I’d love to hear your thoughts on the techniques and tools I mentioned in this post, and if you had other features that you’d like to support. Then we can discuss how we could implement them with the current proposal, or what adjustments could be made. :raised_hands:


#3

Hey @nilan. I updated my schema to include the Vote type as you demonstrated. I’m successfully creating votes tied to users. I was wondering what would be a good way to determine whether a poll was already voted on (either on frontend or server) to disable additional votes from that user on the poll in question?

EDIT: I’ve figured out a way to do it.