Skip to main content

Class 2: Schema Design Fundamentals

Duration: 30 minutes Difficulty: Beginner Prerequisites: Completed Class 1

What You'll Learn

By the end of this class, you will:

  • Understand all GraphQL scalar types
  • Create enums for type-safe values
  • Design object types with relationships
  • Use descriptions for self-documenting schemas
  • Apply schema design best practices

The GraphQL Type System

GraphQL has a powerful type system that ensures your API is predictable and self-documenting. Let's explore it.

┌─────────────────────────────────────────────────────────────────────┐
│ GraphQL Type System │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Scalar Types (Primitives) │
│ ├── Int → 32-bit signed integer │
│ ├── Float → Double-precision floating-point │
│ ├── String → UTF-8 character sequence │
│ ├── Boolean → true or false │
│ └── ID → Unique identifier (serialized as String) │
│ │
│ Complex Types │
│ ├── Object → Custom types with fields (e.g., Movie) │
│ ├── Enum → Predefined set of values │
│ ├── List → Collection of items [Type] │
│ ├── Non-Null → Cannot be null (Type!) │
│ ├── Interface → Abstract type with shared fields │
│ └── Union → One of several types │
│ │
└─────────────────────────────────────────────────────────────────────┘

Step 1: Expanding Our Schema

Let's evolve our movie database schema to be more realistic. Replace your schema.graphqls with:

📁 src/main/resources/graphql/schema.graphqls

"""
The root query type for reading data from the Movie Database.
"""
type Query {
"Get a single movie by its unique ID"
movie(id: ID!): Movie

"Get all movies, optionally filtered by genre"
movies(genre: Genre): [Movie!]!

"Search movies by title (case-insensitive partial match)"
searchMovies(title: String!): [Movie!]!

"Get a single actor by ID"
actor(id: ID!): Actor

"Get all actors"
actors: [Actor!]!
}

"""
A movie in the database.
"""
type Movie {
"Unique identifier"
id: ID!

"The movie's title"
title: String!

"Year the movie was released"
releaseYear: Int!

"The movie's genre"
genre: Genre!

"IMDB-style rating from 0.0 to 10.0"
rating: Float

"Runtime in minutes"
runtime: Int

"Brief plot summary"
plot: String

"Is the movie currently in theaters?"
inTheaters: Boolean!
}

"""
An actor who appears in movies.
"""
type Actor {
"Unique identifier"
id: ID!

"Actor's full name"
name: String!

"Year of birth"
birthYear: Int

"Country of origin"
nationality: String
}

"""
Movie genres available in the database.
"""
enum Genre {
"Action and adventure films"
ACTION

"Animated films"
ANIMATION

"Comedy films"
COMEDY

"Crime and gangster films"
CRIME

"Documentary films"
DOCUMENTARY

"Drama films"
DRAMA

"Fantasy films"
FANTASY

"Horror films"
HORROR

"Musical films"
MUSICAL

"Mystery and thriller films"
MYSTERY

"Romance films"
ROMANCE

"Science fiction films"
SCIFI

"War films"
WAR

"Western films"
WESTERN
}

What's New Here?

  1. Triple quotes (""") - Multi-line descriptions that appear in documentation
  2. Single quotes after fields - Inline descriptions
  3. Enum type - Genre with predefined values
  4. Nullable fields - rating, runtime, plot can be null
  5. New types - Actor type for future relationships

Step 2: Create the Genre Enum in Java

Spring GraphQL automatically maps schema enums to Java enums:

📁 src/main/java/com/example/moviedb/model/Genre.java

package com.example.moviedb.model;

public enum Genre {
ACTION,
ANIMATION,
COMEDY,
CRIME,
DOCUMENTARY,
DRAMA,
FANTASY,
HORROR,
MUSICAL,
MYSTERY,
ROMANCE,
SCIFI,
WAR,
WESTERN
}
Enum Naming

GraphQL enum values must match exactly. SCIFI in schema maps to SCIFI in Java, not SciFi or SCI_FI.

Step 3: Update the Movie Model

Let's expand our Movie class:

📁 src/main/java/com/example/moviedb/model/Movie.java

package com.example.moviedb.model;

public class Movie {
private String id;
private String title;
private int releaseYear;
private Genre genre;
private Double rating; // Nullable
private Integer runtime; // Nullable
private String plot; // Nullable
private boolean inTheaters;

public Movie(String id, String title, int releaseYear, Genre genre,
Double rating, Integer runtime, String plot, boolean inTheaters) {
this.id = id;
this.title = title;
this.releaseYear = releaseYear;
this.genre = genre;
this.rating = rating;
this.runtime = runtime;
this.plot = plot;
this.inTheaters = inTheaters;
}

// Convenience constructor for common cases
public Movie(String id, String title, int releaseYear, Genre genre, Double rating) {
this(id, title, releaseYear, genre, rating, null, null, false);
}

// Getters
public String getId() { return id; }
public String getTitle() { return title; }
public int getReleaseYear() { return releaseYear; }
public Genre getGenre() { return genre; }
public Double getRating() { return rating; }
public Integer getRuntime() { return runtime; }
public String getPlot() { return plot; }
public boolean isInTheaters() { return inTheaters; }
}
Nullable Fields in Java

Use wrapper types (Double, Integer) for nullable GraphQL fields, not primitives (double, int). Primitives can't be null!

Step 4: Create the Actor Model

📁 src/main/java/com/example/moviedb/model/Actor.java

package com.example.moviedb.model;

public class Actor {
private String id;
private String name;
private Integer birthYear;
private String nationality;

public Actor(String id, String name, Integer birthYear, String nationality) {
this.id = id;
this.name = name;
this.birthYear = birthYear;
this.nationality = nationality;
}

public String getId() { return id; }
public String getName() { return name; }
public Integer getBirthYear() { return birthYear; }
public String getNationality() { return nationality; }
}

Step 5: Create a Data Repository

Let's organize our sample data:

📁 src/main/java/com/example/moviedb/repository/MovieRepository.java

package com.example.moviedb.repository;

import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Repository
public class MovieRepository {

private final List<Movie> movies = new ArrayList<>();

public MovieRepository() {
// Initialize with sample data
movies.add(new Movie("1", "The Shawshank Redemption", 1994, Genre.DRAMA, 9.3,
142, "Two imprisoned men bond over a number of years.", false));
movies.add(new Movie("2", "The Godfather", 1972, Genre.CRIME, 9.2,
175, "The aging patriarch of an organized crime dynasty.", false));
movies.add(new Movie("3", "The Dark Knight", 2008, Genre.ACTION, 9.0,
152, "Batman faces the Joker, a criminal mastermind.", false));
movies.add(new Movie("4", "Pulp Fiction", 1994, Genre.CRIME, 8.9,
154, "Various interconnected stories of criminals in Los Angeles.", false));
movies.add(new Movie("5", "Forrest Gump", 1994, Genre.DRAMA, 8.8,
142, "The story of a man with low IQ who achieved great things.", false));
movies.add(new Movie("6", "Inception", 2010, Genre.SCIFI, 8.8,
148, "A thief who enters people's dreams.", false));
movies.add(new Movie("7", "The Matrix", 1999, Genre.SCIFI, 8.7,
136, "A hacker discovers reality is a simulation.", false));
movies.add(new Movie("8", "Interstellar", 2014, Genre.SCIFI, 8.6,
169, "Explorers travel through a wormhole in space.", false));
movies.add(new Movie("9", "The Lord of the Rings: The Fellowship", 2001, Genre.FANTASY, 8.8,
178, "A hobbit inherits a powerful ring.", false));
movies.add(new Movie("10", "Gladiator", 2000, Genre.ACTION, 8.5,
155, "A Roman General seeks revenge against the emperor.", false));
}

public List<Movie> findAll() {
return new ArrayList<>(movies);
}

public Optional<Movie> findById(String id) {
return movies.stream()
.filter(m -> m.getId().equals(id))
.findFirst();
}

public List<Movie> findByGenre(Genre genre) {
return movies.stream()
.filter(m -> m.getGenre() == genre)
.toList();
}

public List<Movie> searchByTitle(String title) {
String lowerTitle = title.toLowerCase();
return movies.stream()
.filter(m -> m.getTitle().toLowerCase().contains(lowerTitle))
.toList();
}
}

📁 src/main/java/com/example/moviedb/repository/ActorRepository.java

package com.example.moviedb.repository;

import com.example.moviedb.model.Actor;
import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Repository
public class ActorRepository {

private final List<Actor> actors = new ArrayList<>();

public ActorRepository() {
actors.add(new Actor("1", "Morgan Freeman", 1937, "American"));
actors.add(new Actor("2", "Tim Robbins", 1958, "American"));
actors.add(new Actor("3", "Marlon Brando", 1924, "American"));
actors.add(new Actor("4", "Al Pacino", 1940, "American"));
actors.add(new Actor("5", "Christian Bale", 1974, "British"));
actors.add(new Actor("6", "Heath Ledger", 1979, "Australian"));
actors.add(new Actor("7", "Leonardo DiCaprio", 1974, "American"));
actors.add(new Actor("8", "Tom Hanks", 1956, "American"));
actors.add(new Actor("9", "Keanu Reeves", 1964, "Canadian"));
actors.add(new Actor("10", "Russell Crowe", 1964, "New Zealand"));
}

public List<Actor> findAll() {
return new ArrayList<>(actors);
}

public Optional<Actor> findById(String id) {
return actors.stream()
.filter(a -> a.getId().equals(id))
.findFirst();
}
}

Step 6: Update the Controller

📁 src/main/java/com/example/moviedb/controller/MovieController.java

package com.example.moviedb.controller;

import com.example.moviedb.model.Actor;
import com.example.moviedb.model.Genre;
import com.example.moviedb.model.Movie;
import com.example.moviedb.repository.ActorRepository;
import com.example.moviedb.repository.MovieRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;

import java.util.List;

@Controller
public class MovieController {

private final MovieRepository movieRepository;
private final ActorRepository actorRepository;

public MovieController(MovieRepository movieRepository,
ActorRepository actorRepository) {
this.movieRepository = movieRepository;
this.actorRepository = actorRepository;
}

@QueryMapping
public Movie movie(@Argument String id) {
return movieRepository.findById(id).orElse(null);
}

@QueryMapping
public List<Movie> movies(@Argument Genre genre) {
if (genre != null) {
return movieRepository.findByGenre(genre);
}
return movieRepository.findAll();
}

@QueryMapping
public List<Movie> searchMovies(@Argument String title) {
return movieRepository.searchByTitle(title);
}

@QueryMapping
public Actor actor(@Argument String id) {
return actorRepository.findById(id).orElse(null);
}

@QueryMapping
public List<Actor> actors() {
return actorRepository.findAll();
}
}

Step 7: Test Your Enhanced Schema

Restart your application and open GraphiQL at http://localhost:8080/graphiql

Test the Documentation

Click the Docs button on the right side. You should see all your descriptions!

Query with Enum Filter

query {
movies(genre: SCIFI) {
title
releaseYear
rating
}
}

Search Movies

query {
searchMovies(title: "the") {
title
genre
}
}

Query with Optional Fields

query {
movie(id: "1") {
title
rating
runtime
plot
inTheaters
}
}

Explore Actors

query {
actors {
name
birthYear
nationality
}
}

Schema Design Best Practices

1. Use Descriptive Names

# ❌ Bad
type M {
t: String!
y: Int!
}

# ✅ Good
type Movie {
title: String!
releaseYear: Int!
}

2. Document Everything

# ❌ Bad
type Movie {
rating: Float
}

# ✅ Good
"""
A movie in the database.
"""
type Movie {
"IMDB-style rating from 0.0 to 10.0"
rating: Float
}

3. Use Enums for Fixed Sets

# ❌ Bad - Any string is valid
type Movie {
genre: String!
}

# ✅ Good - Only valid genres allowed
type Movie {
genre: Genre!
}

enum Genre {
ACTION
COMEDY
DRAMA
}

4. Be Intentional with Nullability

type Movie {
id: ID! # Always exists - non-null
title: String! # Required - non-null
rating: Float # May not have rating yet - nullable
sequel: Movie # May not have a sequel - nullable
}

5. Use ID for Identifiers

# ❌ Bad
type Movie {
id: String!
}

# ✅ Good - Signals this is an identifier
type Movie {
id: ID!
}

Understanding Type Nullability

Here's a visual guide to nullability combinations:

┌─────────────────────────────────────────────────────────────────────┐
│ NULLABILITY CHEAT SHEET │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Type │ Valid Values │
│ ─────────────────────────────────────────────────────────────── │
│ String │ null, "hello", "" │
│ String! │ "hello", "" (NOT null) │
│ │
│ [String] │ null, [], ["a"], ["a", null] │
│ [String!] │ null, [], ["a"], ["a", "b"] (items NOT null) │
│ [String]! │ [], ["a"], ["a", null] (list NOT null) │
│ [String!]! │ [], ["a"], ["a", "b"] (nothing is null) │
│ │
│ Common Use Cases: │
│ ─────────────────────────────────────────────────────────────── │
│ movies: [Movie!]! │ Query always returns a list (may be empty) │
│ movie(id: ID!): Movie │ Query may return null (not found) │
│ tags: [String!] │ Tags may be null, but if present, no nulls │
│ │
└─────────────────────────────────────────────────────────────────────┘

Exercises

Exercise 1: Add a Director Type

Create a Director type with fields: id, name, birthYear, and nationality. Add it to the schema and create the corresponding Java model and repository.

Solution

schema.graphqls:

type Director {
id: ID!
name: String!
birthYear: Int
nationality: String
}

type Query {
# ... existing queries
director(id: ID!): Director
directors: [Director!]!
}

Create Director.java and DirectorRepository.java similar to Actor.

Exercise 2: Add More Enums

Create a Rating enum for movie ratings (G, PG, PG13, R, NC17). Add a contentRating field to the Movie type.

Solution
enum Rating {
G
PG
PG13
R
NC17
}

type Movie {
# ... existing fields
contentRating: Rating
}

Exercise 3: Create a MovieStats Type

Create a type that returns aggregate information:

type MovieStats {
totalCount: Int!
averageRating: Float!
oldestYear: Int!
newestYear: Int!
}

type Query {
movieStats: MovieStats!
}

Summary

In this class, you learned:

✅ All five GraphQL scalar types: Int, Float, String, Boolean, ID ✅ How to create and use enums for type-safe values ✅ How to document your schema with descriptions ✅ Nullability rules and best practices ✅ How Spring GraphQL maps schema types to Java types ✅ Schema design best practices for maintainable APIs

What's Next?

In Class 3: Queries Deep Dive, we'll explore:

  • Nested queries and field resolvers
  • Connecting Movies to Actors
  • Understanding the resolver chain
  • Optimizing query performance

Your movie database is about to become truly relational!