Skip to main content

Class 4: Mutations

Duration: 30 minutes Difficulty: Intermediate Prerequisites: Completed Classes 1-3

What You'll Learn

By the end of this class, you will:

  • Create mutations for adding new data
  • Use input types for complex arguments
  • Implement update and delete operations
  • Return meaningful responses from mutations
  • Follow mutation best practices

Queries vs Mutations

In GraphQL:

  • Queries = Read operations (GET in REST)
  • Mutations = Write operations (POST, PUT, DELETE in REST)
┌─────────────────────────────────────────────────────────────────────┐
│ QUERIES vs MUTATIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ QUERIES MUTATIONS │
│ ──────────────────────────────────────────────────────────────── │
│ - Read data - Write data │
│ - Run in parallel - Run sequentially │
│ - Idempotent (safe to repeat) - May have side effects │
│ - Cacheable - Not cacheable │
│ │
│ query { mutation { │
│ movies { title } createMovie(...) { id } │
│ } } │
│ │
└─────────────────────────────────────────────────────────────────────┘

Step 1: Define Input Types

Input types are special types used for mutation arguments. They're like regular types but prefixed with input:

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

# Add after your Query type

type Mutation {
"Create a new movie"
createMovie(input: CreateMovieInput!): Movie!

"Update an existing movie"
updateMovie(id: ID!, input: UpdateMovieInput!): Movie

"Delete a movie"
deleteMovie(id: ID!): DeleteResponse!

"Create a new actor"
createActor(input: CreateActorInput!): Actor!

"Create a new director"
createDirector(input: CreateDirectorInput!): Director!

"Add an actor to a movie"
addActorToMovie(movieId: ID!, actorId: ID!): Movie!

"Remove an actor from a movie"
removeActorFromMovie(movieId: ID!, actorId: ID!): Movie!
}

"""
Input for creating a new movie.
All required fields must be provided.
"""
input CreateMovieInput {
title: String!
releaseYear: Int!
genre: Genre!
rating: Float
runtime: Int
plot: String
directorId: ID!
actorIds: [ID!]
}

"""
Input for updating a movie.
All fields are optional - only provided fields will be updated.
"""
input UpdateMovieInput {
title: String
releaseYear: Int
genre: Genre
rating: Float
runtime: Int
plot: String
inTheaters: Boolean
}

input CreateActorInput {
name: String!
birthYear: Int
nationality: String
}

input CreateDirectorInput {
name: String!
birthYear: Int
nationality: String
}

"""
Standard response for delete operations.
"""
type DeleteResponse {
success: Boolean!
message: String!
deletedId: ID
}

Input Types vs Regular Types

┌─────────────────────────────────────────────────────────────────────┐
│ INPUT vs OUTPUT TYPES │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ type Movie { vs input CreateMovieInput { │
│ id: ID! title: String! │
│ title: String! releaseYear: Int! │
│ director: Director! genre: Genre! │
│ actors: [Actor!]! directorId: ID! ← Just ID │
│ } actorIds: [ID!] ← Just IDs │
│ } │
│ │
│ - Used in responses - Used in arguments │
│ - Can have resolvers - No resolvers │
│ - Can reference other types - Only references by ID │
│ │
└─────────────────────────────────────────────────────────────────────┘

Step 2: Create Input DTOs

Create Java classes to match your input types:

📁 src/main/java/com/example/moviedb/dto/CreateMovieInput.java

package com.example.moviedb.dto;

import com.example.moviedb.model.Genre;
import java.util.List;

public class CreateMovieInput {
private String title;
private int releaseYear;
private Genre genre;
private Double rating;
private Integer runtime;
private String plot;
private String directorId;
private List<String> actorIds;

// Getters and setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }

public int getReleaseYear() { return releaseYear; }
public void setReleaseYear(int releaseYear) { this.releaseYear = releaseYear; }

public Genre getGenre() { return genre; }
public void setGenre(Genre genre) { this.genre = genre; }

public Double getRating() { return rating; }
public void setRating(Double rating) { this.rating = rating; }

public Integer getRuntime() { return runtime; }
public void setRuntime(Integer runtime) { this.runtime = runtime; }

public String getPlot() { return plot; }
public void setPlot(String plot) { this.plot = plot; }

public String getDirectorId() { return directorId; }
public void setDirectorId(String directorId) { this.directorId = directorId; }

public List<String> getActorIds() { return actorIds; }
public void setActorIds(List<String> actorIds) { this.actorIds = actorIds; }
}

📁 src/main/java/com/example/moviedb/dto/UpdateMovieInput.java

package com.example.moviedb.dto;

import com.example.moviedb.model.Genre;

public class UpdateMovieInput {
private String title;
private Integer releaseYear;
private Genre genre;
private Double rating;
private Integer runtime;
private String plot;
private Boolean inTheaters;

// All nullable - only update what's provided
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }

public Integer getReleaseYear() { return releaseYear; }
public void setReleaseYear(Integer releaseYear) { this.releaseYear = releaseYear; }

public Genre getGenre() { return genre; }
public void setGenre(Genre genre) { this.genre = genre; }

public Double getRating() { return rating; }
public void setRating(Double rating) { this.rating = rating; }

public Integer getRuntime() { return runtime; }
public void setRuntime(Integer runtime) { this.runtime = runtime; }

public String getPlot() { return plot; }
public void setPlot(String plot) { this.plot = plot; }

public Boolean getInTheaters() { return inTheaters; }
public void setInTheaters(Boolean inTheaters) { this.inTheaters = inTheaters; }
}

📁 src/main/java/com/example/moviedb/dto/DeleteResponse.java

package com.example.moviedb.dto;

public class DeleteResponse {
private boolean success;
private String message;
private String deletedId;

public DeleteResponse(boolean success, String message, String deletedId) {
this.success = success;
this.message = message;
this.deletedId = deletedId;
}

public static DeleteResponse success(String id) {
return new DeleteResponse(true, "Successfully deleted", id);
}

public static DeleteResponse notFound(String id) {
return new DeleteResponse(false, "Item not found with id: " + id, null);
}

public boolean isSuccess() { return success; }
public String getMessage() { return message; }
public String getDeletedId() { return deletedId; }
}

📁 src/main/java/com/example/moviedb/dto/CreateActorInput.java

package com.example.moviedb.dto;

public class CreateActorInput {
private String name;
private Integer birthYear;
private String nationality;

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public Integer getBirthYear() { return birthYear; }
public void setBirthYear(Integer birthYear) { this.birthYear = birthYear; }

public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
}

📁 src/main/java/com/example/moviedb/dto/CreateDirectorInput.java

package com.example.moviedb.dto;

public class CreateDirectorInput {
private String name;
private Integer birthYear;
private String nationality;

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public Integer getBirthYear() { return birthYear; }
public void setBirthYear(Integer birthYear) { this.birthYear = birthYear; }

public String getNationality() { return nationality; }
public void setNationality(String nationality) { this.nationality = nationality; }
}

Step 3: Update Repositories to Support Mutations

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

Add these methods:

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;
import java.util.UUID;

@Repository
public class MovieRepository {

private final List<Movie> movies = new ArrayList<>();
private int idCounter = 100; // Start after sample data

public MovieRepository() {
// ... existing sample data initialization
}

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();
}

public List<Movie> findByDirectorId(String directorId) {
return movies.stream()
.filter(m -> m.getDirectorId().equals(directorId))
.toList();
}

public List<Movie> findByActorId(String actorId) {
return movies.stream()
.filter(m -> m.getActorIds().contains(actorId))
.toList();
}

// ===== NEW MUTATION METHODS =====

public Movie save(Movie movie) {
if (movie.getId() == null) {
// Create new movie with generated ID
Movie newMovie = new Movie(
String.valueOf(++idCounter),
movie.getTitle(),
movie.getReleaseYear(),
movie.getGenre(),
movie.getRating(),
movie.getRuntime(),
movie.getPlot(),
movie.isInTheaters(),
movie.getDirectorId(),
movie.getActorIds()
);
movies.add(newMovie);
return newMovie;
} else {
// Update existing - remove old and add new
movies.removeIf(m -> m.getId().equals(movie.getId()));
movies.add(movie);
return movie;
}
}

public boolean delete(String id) {
return movies.removeIf(m -> m.getId().equals(id));
}
}

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

Add save method:

private int idCounter = 100;

public Actor save(Actor actor) {
if (actor.getId() == null) {
Actor newActor = new Actor(
String.valueOf(++idCounter),
actor.getName(),
actor.getBirthYear(),
actor.getNationality()
);
actors.add(newActor);
return newActor;
}
return actor;
}

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

Add save method similarly.

Step 4: Create the Mutation Controller

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

package com.example.moviedb.controller;

import com.example.moviedb.dto.*;
import com.example.moviedb.model.Actor;
import com.example.moviedb.model.Director;
import com.example.moviedb.model.Movie;
import com.example.moviedb.repository.ActorRepository;
import com.example.moviedb.repository.DirectorRepository;
import com.example.moviedb.repository.MovieRepository;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.MutationMapping;
import org.springframework.stereotype.Controller;

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

@Controller
public class MutationController {

private final MovieRepository movieRepository;
private final ActorRepository actorRepository;
private final DirectorRepository directorRepository;

public MutationController(MovieRepository movieRepository,
ActorRepository actorRepository,
DirectorRepository directorRepository) {
this.movieRepository = movieRepository;
this.actorRepository = actorRepository;
this.directorRepository = directorRepository;
}

@MutationMapping
public Movie createMovie(@Argument CreateMovieInput input) {
// Validate director exists
directorRepository.findById(input.getDirectorId())
.orElseThrow(() -> new IllegalArgumentException(
"Director not found: " + input.getDirectorId()));

// Validate actors exist
if (input.getActorIds() != null) {
for (String actorId : input.getActorIds()) {
actorRepository.findById(actorId)
.orElseThrow(() -> new IllegalArgumentException(
"Actor not found: " + actorId));
}
}

Movie movie = new Movie(
null, // ID will be generated
input.getTitle(),
input.getReleaseYear(),
input.getGenre(),
input.getRating(),
input.getRuntime(),
input.getPlot(),
false, // Not in theaters by default
input.getDirectorId(),
input.getActorIds() != null ? input.getActorIds() : List.of()
);

return movieRepository.save(movie);
}

@MutationMapping
public Movie updateMovie(@Argument String id, @Argument UpdateMovieInput input) {
Movie existing = movieRepository.findById(id).orElse(null);
if (existing == null) {
return null;
}

// Apply partial updates - only update non-null fields
Movie updated = new Movie(
existing.getId(),
input.getTitle() != null ? input.getTitle() : existing.getTitle(),
input.getReleaseYear() != null ? input.getReleaseYear() : existing.getReleaseYear(),
input.getGenre() != null ? input.getGenre() : existing.getGenre(),
input.getRating() != null ? input.getRating() : existing.getRating(),
input.getRuntime() != null ? input.getRuntime() : existing.getRuntime(),
input.getPlot() != null ? input.getPlot() : existing.getPlot(),
input.getInTheaters() != null ? input.getInTheaters() : existing.isInTheaters(),
existing.getDirectorId(),
existing.getActorIds()
);

return movieRepository.save(updated);
}

@MutationMapping
public DeleteResponse deleteMovie(@Argument String id) {
boolean deleted = movieRepository.delete(id);
if (deleted) {
return DeleteResponse.success(id);
}
return DeleteResponse.notFound(id);
}

@MutationMapping
public Actor createActor(@Argument CreateActorInput input) {
Actor actor = new Actor(
null,
input.getName(),
input.getBirthYear(),
input.getNationality()
);
return actorRepository.save(actor);
}

@MutationMapping
public Director createDirector(@Argument CreateDirectorInput input) {
Director director = new Director(
null,
input.getName(),
input.getBirthYear(),
input.getNationality()
);
return directorRepository.save(director);
}

@MutationMapping
public Movie addActorToMovie(@Argument String movieId, @Argument String actorId) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new IllegalArgumentException("Movie not found"));

actorRepository.findById(actorId)
.orElseThrow(() -> new IllegalArgumentException("Actor not found"));

List<String> actorIds = new ArrayList<>(movie.getActorIds());
if (!actorIds.contains(actorId)) {
actorIds.add(actorId);
}

Movie updated = new Movie(
movie.getId(),
movie.getTitle(),
movie.getReleaseYear(),
movie.getGenre(),
movie.getRating(),
movie.getRuntime(),
movie.getPlot(),
movie.isInTheaters(),
movie.getDirectorId(),
actorIds
);

return movieRepository.save(updated);
}

@MutationMapping
public Movie removeActorFromMovie(@Argument String movieId, @Argument String actorId) {
Movie movie = movieRepository.findById(movieId)
.orElseThrow(() -> new IllegalArgumentException("Movie not found"));

List<String> actorIds = new ArrayList<>(movie.getActorIds());
actorIds.remove(actorId);

Movie updated = new Movie(
movie.getId(),
movie.getTitle(),
movie.getReleaseYear(),
movie.getGenre(),
movie.getRating(),
movie.getRuntime(),
movie.getPlot(),
movie.isInTheaters(),
movie.getDirectorId(),
actorIds
);

return movieRepository.save(updated);
}
}

Step 5: Test Your Mutations

Restart your application and try these in GraphiQL:

Create a New Movie

mutation {
createMovie(input: {
title: "The Prestige"
releaseYear: 2006
genre: MYSTERY
rating: 8.5
runtime: 130
plot: "Two rival magicians engage in a bitter competition"
directorId: "3"
actorIds: ["5", "7"]
}) {
id
title
director {
name
}
actors {
name
}
}
}

Update a Movie

mutation {
updateMovie(id: "1", input: {
rating: 9.5
inTheaters: true
}) {
id
title
rating
inTheaters
}
}

Delete a Movie

mutation {
deleteMovie(id: "101") {
success
message
deletedId
}
}

Create a New Actor

mutation {
createActor(input: {
name: "Scarlett Johansson"
birthYear: 1984
nationality: "American"
}) {
id
name
}
}

Add Actor to Movie

mutation {
addActorToMovie(movieId: "1", actorId: "101") {
title
actors {
name
}
}
}

Using Variables (Best Practice)

In production, you should use variables instead of inline values:

mutation CreateMovie($input: CreateMovieInput!) {
createMovie(input: $input) {
id
title
}
}

Variables (in the Variables panel of GraphiQL):

{
"input": {
"title": "Tenet",
"releaseYear": 2020,
"genre": "SCIFI",
"rating": 7.4,
"directorId": "3"
}
}

Mutation Best Practices

1. Use Input Types for Complex Arguments

# ❌ Bad - Too many arguments
mutation {
createMovie(
title: "...",
releaseYear: 2020,
genre: ACTION,
rating: 8.0,
runtime: 120,
plot: "...",
directorId: "1"
) { id }
}

# ✅ Good - Single input object
mutation {
createMovie(input: {
title: "...",
releaseYear: 2020,
...
}) { id }
}

2. Return the Modified Object

# ❌ Bad - Returns just an ID
type Mutation {
createMovie(input: CreateMovieInput!): ID!
}

# ✅ Good - Returns the full object
type Mutation {
createMovie(input: CreateMovieInput!): Movie!
}

3. Use Meaningful Response Types for Deletes

# ❌ Bad - Just a boolean
type Mutation {
deleteMovie(id: ID!): Boolean!
}

# ✅ Good - Detailed response
type Mutation {
deleteMovie(id: ID!): DeleteResponse!
}

type DeleteResponse {
success: Boolean!
message: String!
deletedId: ID
}

4. Validate Input

@MutationMapping
public Movie createMovie(@Argument CreateMovieInput input) {
// Validate year is reasonable
int currentYear = Year.now().getValue();
if (input.getReleaseYear() < 1888 || input.getReleaseYear() > currentYear + 5) {
throw new IllegalArgumentException("Invalid release year");
}

// Validate rating range
if (input.getRating() != null &&
(input.getRating() < 0 || input.getRating() > 10)) {
throw new IllegalArgumentException("Rating must be between 0 and 10");
}

// ... create movie
}

Exercises

Exercise 1: Add updateActor Mutation

Create a mutation to update actor details:

type Mutation {
updateActor(id: ID!, input: UpdateActorInput!): Actor
}

input UpdateActorInput {
name: String
birthYear: Int
nationality: String
}

Exercise 2: Add Rate Movie Mutation

Create a simple mutation for rating a movie:

type Mutation {
rateMovie(id: ID!, rating: Float!): Movie!
}

Validate that rating is between 0 and 10.

Exercise 3: Batch Create

Create a mutation that creates multiple movies at once:

type Mutation {
createMovies(inputs: [CreateMovieInput!]!): [Movie!]!
}

Summary

In this class, you learned:

✅ The difference between queries (reads) and mutations (writes) ✅ How to define input types for structured arguments ✅ Using @MutationMapping to handle mutations ✅ Creating, updating, and deleting data ✅ Returning meaningful responses from mutations ✅ Input validation best practices

What's Next?

In Class 5: Relationships & DataLoader, we'll tackle:

  • The N+1 problem in GraphQL
  • Efficient data loading with DataLoader
  • Batching and caching strategies
  • @BatchMapping for optimized queries

This is where performance optimization gets serious!