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?
- Triple quotes (
""") - Multi-line descriptions that appear in documentation - Single quotes after fields - Inline descriptions
- Enum type -
Genrewith predefined values - Nullable fields -
rating,runtime,plotcan be null - New types -
Actortype 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
}
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; }
}
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!