DEV Community

Cover image for Dynamic Types with GraphQL
Petr Filaretov
Petr Filaretov

Posted on • Updated on

Dynamic Types with GraphQL

TL;DR

It looks pretty natural to implement dynamic types with GraphQL. However, the implementation involves a bunch of challenges.

Intro

Okay, I know I failed my challenge of posting a "learned" update once a week. There were some reasons I could not find time to do the update in time. However, I will continue this series of posts, pretending that nothing happened.

And today I would like to tell you about one way that I learned how to implement dynamic types with GraphQL.

What are dynamic types?

"Dynamic types" is a feature of the application where users can define their own types, and then create instances of these types.

Say, we have The Lord of the Rings encyclopedia application, and we initially provided users with a small set of basic races: elves, dwarves, hobbits, and men. And also, we have some persons of these races, for instance, Legolas (elf), Gimli (dwarf), Frodo (hobbit), and Aragorn (man).

So, races are types, and persons are instances of these types.

And then, the administrator logs in and says: "Hey, where is Gandalf?" So, they create Maiar race first and then create Gandalf person.

But how do we implement this?

Dynamic types with GraphQL

The way we do this in Okko on the project I am working on right now is with GraphQL. Simply put, a gateway provides an API to create types and instances. Once a new type is created, the schema on the gateway is refreshed so that the new type is available for queries and mutations via the gateway.

Let's have a closer look at how it works.

Dynamic Types with GraphQL
(here is the direct link to the image in case it is blurry - not sure why and how dev.to and cloudinary does this)

Here we have three main backend services: gateway, settings, and data-access-layer.

Gateway service is an API gateway that provides a GraphQL API of all the backend services by merging their schemas. And it simply forwards API requests.

Settings service is responsible for managing types and properties.

Data-access-layer service is the main abstraction on top of the database. It provides GraphQL API CRUD operations for instances of types.

So, the flow begins when the frontend sends an API call to the gateway service to create a new type (1). The request is proxied to the settings service which creates the new type (2) in the database.

Then, the settings service sends a "refresh schema" event to RabbitMQ (3) which is consumed by the data-access-layer service.

Data-access-layer service in turn sends a request to the settings service (4) to get all types (5) and type properties (6) from the database. Once it receives a result (7), it does two things:

  • updates its own GraphQL schema so that CRUD operations for the new type appear;
  • sends a "refresh schema" event to RabbitMQ (8).

When the gateway service consumes the "refresh schema" event, it fetches updated schemas from all the services (9), merges schemas, and provides an updated GraphQL API that can be used to create an instance of the new type (10).

Why so complex? 😱

Okay, I admit it does not look very simple. And at this point, you may have a lot of questions. So, let's try to address some.

Fetch updated schemas and incremental merge

In step (3) the settings service sends an event to the data-access-layer service, which calls back the settings service (4) to get GraphQL schema with all types and their properties (7). So, why not just send an incremental update (i.e., the new type) in the event? Then we do not need steps (4)-(7) at all.

Once the data-access-layer service receives a "refresh schema" event, it should update its GraphQL schema to add CRUD operations for the new type. And this incremental schema merge is tricky. So, for now, we simply retrieve all the types (4)-(7) and build a new GraphQL schema from that.

For the same reason in step (9) the gateway service fetches schemas from all the services, even though only the data-access-layer service schema is updated.

So, an incremental merge is our tech debt and one way we can improve here.

Scaling

Now, let's see what is going on when backend services are scaled horizontally.

If we have several gateway services, then each of them should consume a "refresh schema" event (8) to update its schema. And that is exactly what is happening, so we are good here.

If we have several data-access-layer services, then we need to make sure that

  • every instance consumes a "refresh schema" event (3);
  • when the gateway service sends a "get schema" request (9), the schema is already updated on the data-access-layer service instance that receives the request.

The former looks obviously the same as before with (8), but what is the solution for the latter here?

Elementary, my dear Watson! We simply do not scale the data-access-layer service. It always has a single instance, introducing a single point of failure for the system.

While this is one of the critical things we need to improve, the team decided at some point that done is better than perfect. And I think it was the right decision taking into account the deadlines we had.

Conclusion

So, what did I learn?

I learned that it looks pretty natural to implement dynamic types with GraphQL. However, the implementation involves a bunch of challenges - schema merges, services scaling, and event processing to name a few.

Take care. Tomorrow will be... better!

Top comments (0)