Build GraphQL Services With Spring Boot Like Netflix
Link |
|
Author(s) |
Paul Bakker |
Length |
48:20 |
Date |
21-09-2024 |
Language |
English 🇺🇸 |
Rating |
⭐⭐⭐☆☆ |
-
✅ Impressive DGS integration with virtual threads.
-
⛔ Database integration ignored: how to build the DB query and test it with DGS?
-
⛔ Batch loading skipped which I was curious about since it is a common problem to solve.
-
⛔ The speaker’s coding habits are questionable despite of being a Java Champion.
"It becomes very easy to compose different data fetchers."
GraphQL
Every GraphQL must have a schema and brings flexibility by selecting fields to query (no over-fetching). The communication happens typically over HTTP via one endpoint with any HTTP library: GrapshQL is a high level abstraction. Also WebSockets are possible to use.
GraphQL Federation microservices
GraphQL Federated Gateway distributes parts of the request into various DGS Spring Boot microservices. The Gateway needs only to know about the schemas of these different microservices with no custom logic required despite of ownership of data by various services.
DGS
Open-source Netflix framework was created 4 years ago though Spring Boot introduced a GraphQL support 2 years ago. The advantage is that both can be used simultaneously because DGS was recently integrated into Spring.
Create a schema/schema.graphsqls
file:
Example schema
type Query {
lolomo: [ShowCategory]
}
type ShowCategory {
id: Int
name: String
shows: [Show]
}
type Show {
title: String
artworkUrl: String
}
Create a fetcher responsible for the lolomo
query, ex. LolomoDataFetcher
(there is a DGS plugin to link the annotations with the GraphQL schema for better navigation):
@DgsComponent
public class LolomoDataFetcher {
@Autowired
private ShowsRepository showsRepository;
@DgsQuery
public List<ShowCategory> lolomo() {
return List.of(
ShowCategory.newBuilder().id(1).name("Top 10")
.shows(showsRepositorye.showsForCategory(1))
.build(),
ShowCategory.newBuilder().id(2).name("Continue Watching")
.shows(showsRepositorye.showsForCategory(2))
.build()
);
}
}
We are free to use Java POJOs as classes, simple records or generated via a GraphQL codegen plugin.
Upon running, enter localhost:8080/graphql
that opens a standard GraphQL editor with autocompletion based on the query.
Request
{
lolomo {
name
shows {
title
afterworkUrls
}
}
}
Response
{
"data" : {
"lolomo" : [
{
"name" : "Top 10",
"shows" : [
{
"title" : "The Witcher",
"artworkUrl" : null
}
...
]
},
{
"name" : "Continue Watching",
"shows" : null
}
]
}
}
Parallelization
To make the things realistic, the artworkUrl
is not in the database but rather generated for each user with an image.
The method is dumb and does not accept a batch.
@Component
public class ArtowrkService {
private final static Logger LOGGER = LoggerFactory.getLogger(ArtowrkService.class);
public Stirng generateForTitle(String title) {
LOGGER.info("Generating for {}", title);
// Simulate latency, assume try-catch.
Thread.sleep(200);
return UUID.randomUUID() + "-" + title.toLowerCase().replaceAll(" ", "-");
}
}
The method needs to be for each show, how to do this? We don’t want to run the method if artworkUrl
is not requested, otherwise we generate images that were not requested and it is a bad model.
We need to create a specific fetcher for the artworks in LolomoDataFetcher
service.
private final ArtworkService artworkService;
@DgsQuery(parentType = "Show)
public String artworkUrl(DgsDataFetchingEnvironment dfe) {
Show show = dfe.getSourceOrThrow();
return artworkService.generateForTitle(show.getTitle());
}
The result is really slow. The data fetcher is called all in serial for each of those shows. Very often serial behaviors are acceptable, and we don’t want to mess with threads when we don’t need to do, but in this case we have to think about that.
Since we are on Java 21, we don’t need to think about scheduling executors and thinking of how big pool we need.
dgs.graphql.virtualthreads.enabled = true
The logging shows each generating was each called on a different virtual threads. We don’t need to care about sizing as virtual threads are super cheap.
Though we can get parallel behavior out of box without thinking about it, we need to be aware that the component is running on different threads, so we keep in mind the context propagation, security context, etc. though the DGS framework nicely integrates that.
Batch loading
We can achieve the same behavior using batch loader if we don’t want to use parallelization and the API supports generating artwork URLs for multiple IDs:
We use a GraphQL Data Loader where we do the same mechanism by instead of calling ArtworkService
directly we are going to call a batch loader that loads all the IDs the backend should be called for and then do one call.
Search query
We modify the GraphQL schema by expanding Query
:
-
search: [Show]
-
search(title: Stirng): [Show]
- simple type -
`search(searchFilter: SearchFilter) - complex type to search on title, category, etc.
We need to add a new input
type that are different from outputs (type
), implement a new query and run it:
input SearchFilter {
title: String
}
@DgsQuery
public List<Show> search(@InputArgument SearchFilter searchFilter) {
showsRepository.allShows().stream
.filter(s -> s.getTitle().toLowerCase().startsWith(searchFilter.getTitle().toLowerCase()))
.toList();
}
The queries can be also combined and we can run both search
and lolomo
:
{
search(searchFilter : { title : "The" }) {
title
artworkUrl
}
lolomo {
name
shows {
title
artworkUrl
}
}
}
DGS testing
We want to test the data fetcher without loading the whole Spring Boot application, because there can be database, Flyway, warming of PIC clients, etc.
Use @EnableDgsTest
to execute the query so it imports the DGS core framework.
@SpringBootTest(classes = {
LolomoDataFetcher.class,
ArtworkService.class,
ShowsRepository.class
})
@EnableDgsTest
public class LolomoDataFetcherTest {
@Autowired
DgsQueryExecutor dgsQueryExecutor;
@Test
void search() {
@Language("GraphQL")
var query = """
query {
search(searchFilter: {title: "the"}) {
title
}
}
""";
List<String> titles = dgsQueryExecutor.executeAndExtractJsonPath(query, "data.search[*].title");
assertThat(titles).containsExactly("The Withcher", "The Last Dance");
}
}
URL
Get more information at https://netflix.github.io/dgs/