Practical Guide to Getting Started with Spring Boot and Netflix DGS
Recently, in my self-directed learning journey, GraphQL and Netflix DGS caught my interest, prompting me to plan to write an article about it.
According to the official documentation:
GraphQL is a query language for your API, and a server-side runtime for executing queries using a type system you define for your data. GraphQL isn’t tied to any specific database or storage engine and is instead backed by your existing code and data.
The DGS framework makes it easy to create GraphQL services with Spring Boot. The framework provides an easy-to-use annotation-based programming model and all the advanced features needed to build and run GraphQL services at scale.
In this article, we’ll get started with GraphQL and Netflix DGS by building a Spring Boot project that integrates both technologies. Key points about this article include:
- Request Processing Journey
- Database diagram
- Scenarios
- Hands on
Request Processing Journey
Users interact with a web application to send requests to the server, typically in the form of GraphQL queries and mutations. The GraphQL server receives these requests, interprets the queries, and resolves them using the defined logic.
The server then communicates with the repository layer, which acts as an intermediary, handling the details of data storage and retrieval. The repository layer interacts with the H2 database to execute operations such as fetching, inserting, and updating data.
Once the database operations are complete, the repository processes the results and sends them back to the users.
Database diagram
Customer
table has One-to-Many relationship withPurchase
table.Item
table has One-to-Many relationship withPurchase
table.Shop
table has One-to-Many relationship withItem
table.
Please note that this example demonstrates a simple e-commerce database schema, whereas a real-world schema would be much more complex.
Scenarios
The application supports the following scenarios:
- Shop: Create new shops, retrieve a shop by ID, or retrieve all shops.
- Item: Create new items, retrieve an item by ID, or retrieve items by shop ID.
- Customer: Create new customers, retrieve a customer by ID, or retrieve all customers.
- Purchase: Create new purchases.
Hands on
Create the project as a Spring Boot project with the dependencies provided inside the pom file at this link: POM
I have named it graphql-dgs-netflix
.
GraphQL Schema Creation
We organize our GraphQL schemas in the /src/main/resources/schema
directory. Netflix DGS will automatically load the schemas from this directory. Each scenario will have its own file with .graphql
extension.
Below is the customer.graphqls
file:
type QueryResolver {
customers: [Customer]
customer(id: ID!): Customer!
}
type MutationResolver {
createCustomer(customer: CustomerInput!): Customer
}
input CustomerInput {
firstName: String!
lastName: String!
email: String!
phone: String!
state: String!
city: String!
street: String!
amount: Float
}
type Customer {
id: ID!
firstName: String!
lastName: String!
email: String!
phone: String!
state: String!
city: String!
street: String!
amount: Float
}
schema {
query: QueryResolver
mutation: MutationResolver
}
In this schema, we define two new types: QueryResolver
for read operations and MutationResolver
for write operations. These types are bound to the respective query
and mutation
operations in GraphQL through the following schema definition.
schema {
query: QueryResolver
mutation: MutationResolver
}
Now, let’s look at shop.graphqls
file
extend type QueryResolver {
shops: [Shop]
shop(id: ID!): Shop!
}
extend type MutationResolver {
createShop(shop: ShopInput!): Shop
}
input ShopInput {
name: String!
email: String!
phone: String!
state: String!
city: String!
street: String!
}
type Shop {
id: ID!
name: String!
email: String!
phone: String!
state: String!
city: String!
street: String!
items: [Item]
}
Notice that we extend QueryResolver
and MutationResolver
because we cannot define multiple instances of the same type, so we use extend
.
Similarly in item.graphqls
and purchase.graphqls
. All GraphQL schema files can be found on GitHub at this link: GraphQL Schemas
Entities Creation
We are creating entities that map between our relational database and GraphQL schema.
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
@Builder
public class Customer {
@Id
@GeneratedValue
private UUID id;
private String firstName;
private String lastName;
private String email;
private String phone;
private String state;
private String city;
private String street;
private double amount;
@OneToMany(mappedBy = "customer", cascade = CascadeType.ALL)
private Set<Purchase> items;
}
The Customer
entity is the only example listed here. Other entities, including Item
, Purchase
, Shop
can be found on GitHub at this link: Entities
Mutation Objects Creation
We have defined mutation scenarios for write operations, as discussed earlier. Now, we’ll create corresponding mutation objects. These objects will be used to handle writing requests, with their names matching those specified in the GraphQL schema.
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class CustomerInput {
private String firstName;
private String lastName;
private String email;
private String phone;
private String state;
private String city;
private String street;
private double amount;
}
The CustomerInput
mutation object is the only example listed here. Other mutation objects, including ItemInput
, PurchaseInput
, ShopInput
can be found on GitHub at this link: Mutation Objects
Repository creation
We’ll use JpaRepository for the repository layer, leveraging its comprehensive JPA capabilities to save and query our database objects.
@Repository
public interface CustomerRepository extends JpaRepository<Customer, UUID> {
}
Other repositories can be found on GitHub at this link: Repositories
Netflix DGS Aspects
First, let’s explore some key annotations provided by Netflix DGS, which are essential for interpreting and resolving requests:
@DgsComponent
: Indicates a class containing data fetchers.@DgsData
: Marks a method as a data fetcher. The method must be within a@DgsComponent
class.@DgsQuery
: A shorthand to define data fetchers on theQuery
@DgsMutation
: A shorthand to define data fetchers on theMutation
DgsMutation
: Defines input arguments as method parameters in a data fetcher.
Another aspect to consider when working with DGS is the ability to create a custom context using the DgsCustomContextBuilder class. This can be utilized for logging, maintaining states, or storing information accessed via the DataFetchingEnvironment class.
Querying with Netflix DGS
Now, we’ll create a query class for the Customer using the annotations mentioned earlier.
@DgsComponent
@RequiredArgsConstructor
public class CustomerQuery {
private final CustomerRepository customerRepository;
private final CustomContextBuilder contextBuilder;
@DgsData(parentType = "QueryResolver", field = "customers")
public Iterable<Customer> findAll(DgsDataFetchingEnvironment dfe) {
var customers = (List<Customer>) customerRepository.findAll();
contextBuilder.customContext(null, null, customers, null).build();
return customers;
}
@DgsData(parentType = "QueryResolver", field = "customer")
public Customer findById(@InputArgument("id") String id, DgsDataFetchingEnvironment dfe) {
CustomContext customContext = DgsContext.getCustomContext(dfe);
var customers = customContext.getCustomers();
if (CollectionUtils.isEmpty(customers)) {
return customerRepository.findById(UUID.fromString(id))
.orElseThrow(DgsEntityNotFoundException::new);
}
var customer = customers.stream().filter(it -> it.getId().equals(UUID.fromString(id)))
.findFirst();
return customer.orElseGet(() -> customerRepository.findById(UUID.fromString(id))
.orElseThrow(DgsEntityNotFoundException::new));
}
}
Other query classes, including ItemQuery
, PurchaseQuery
, ShopQuery
can be found on GitHub at this link: Querying with Netflix DGS
Mutation with Netflix DGS
Now, we’ll create a query class for the Customer using the annotations mentioned earlier.
@DgsComponent
@RequiredArgsConstructor
public class CustomerMutation {
private final CustomerRepository customerRepository;
@DgsData(parentType = "MutationResolver", field = "createCustomer")
public Customer createCustomer(@InputArgument("customer") CustomerInput customer) {
return customerRepository.save(Customer.builder().firstName(customer.getFirstName())
.lastName(customer.getLastName()).email(customer.getEmail())
.phone(customer.getPhone()).state(customer.getState()).city(customer.getCity())
.street(customer.getStreet()).amount(customer.getAmount()).build());
}
}
Other mutation classes, including ItemMutation
, PurchaseMutation
, ShopMutation
can be found on GitHub at this link: Mutation with Netflix DGS
Test the app with GraphiQL
To launch the application, run GraphDgsNetflixApplication.main()
method, it should run successfully on port 8080.
Open a browser and navigate to http://localhost:8080/graphiql to access the interface for executing queries.
Now, Let’s do some play around.
Start by creating a Shop:
Fetching all shops:
Find a shop by ID:
There are still more scenarios to explore, but I’ll leave that as homework for you!
We have just explored the concept of Netflix DGS and conducted a brief demonstration to observe its behavior when integrated with Spring Boot and GraphQL.
Hope you can find something useful!
The completed source code can be found in this GitHub repository: https://github.com/buingoctruong/graphql-dgs-netflix