Review of Rejoiner

We review Rejoiner, an interesting tool by Google that generates a unified GraphQL schema for gRPC microservices.

As microservices have become more prominent in the web development space over the last decade, how each microservice communicates with related microservices has become incredibly important. While a single monolithic API has its faults, it does have the benefit of allowing everything to exist within the same silo. The idea of having a vast number of microservices, each with their unique caveats and considerations in interaction, is a potentially troubling one. After all, microservices are meant to improve the general experience, not make it more complicated and difficult.

This is a core problem in the microservice space. So, how do we solve that problem, and what do we do to ensure we solve it while providing for extensibility and scalability? Below, we look at Rejoiner, a solution that leverages GraphQL. Is it a good solution? Read on to find out.

What is Rejoiner?

Rejoiner: Google’s unofficial solution to unify gRPC microservices with GraphQL schema. View it on Github, or get started with the docs.

Google’s answer to the problem of microservices complexity is to leverage the best of both worlds. On one side of the argument, a microservice is far more scalable, extensible, and efficient than monolithic alternatives. On the other hand, it is essentially service fragmentation, which suffers from additional complexity.

Google’s unofficial solution is Rejoiner, a method by which microservices (specifically, gRPC microservices) can be used to generate GraphQL schema. This schema is designed to be flexible, easily understood, and easily referenced – the idea, then, is that the schema can be used to form a type of merging API, a “frontend of microservices,” so to speak.

In practice, the Rejoiner GraphQL layer exists as a sort of translation layer. While an interface may have hundreds of microservices behind it, the singular experience with those microservices can effectively be rendered as a singular interface API experience due to the use of GraphQL as a mediator.

Rejoiner isn’t just a merging layer solution, however. It also boasts some impressive flexibility in how this translation is done. Protobuf, an open standard used for serializing structured data, is used to define types for sourcing the schema definitions. These definitions can then be edited quite effectively using the provided Domain Specific Language (DSL).

Most importantly, the methods for data fetching are joined through annotations, allowing for effective handling of requests using the GraphQL methodology and design ethos while still presenting a singular, easy to understand top-level API methodology.

Experimental Features

Rejoiner isn’t just content to do what it currently does, no matter how effectively it may do it. As of March 2020, Rejoiner is investigating several experimental features. Perhaps most interesting among them is the development of a feature to effectively have Rejoiner operate in reverse – the plan is to “expose any GraphQL schema as a gRPC service,” which is a reverse of its current operational approach. This would add a great amount of extensibility to the solution, and would help in rapid development, iteration, and testing.

Additionally, support for Relay is planned, and support for both lossless end to end proto scalar types and a GraphQL Stream solution is planned.

How Do You Use Rejoiner?

As an example, we can look to the Rejoiner documentation for how an edge between microservices can be created. In this example, we have already installed Rejoiner, and have added some basic types in the GraphQL/Rejoiner schema. Here, we have a Todo type that executes some function on an API. What we want to do is pull the user data from another microservice in the schema, and push that content to retrieve an email.

final class TodoToUserSchemaModule extends SchemaModule {
  @SchemaModification(addField = "creator", onType = Todo.class)
  ListenableFuture<User> todoCreatorToUser(UserService userService, Todo todo) {
    return userService.getUserByEmail(todo.getCreatorEmail());
  }
}

What this does is simply adds a reference to the “User” type in one API to the “Todo” type in a second API. By adding this reference, we have tied together the two types with an annotation that allows Rejoiner to understand how to handle that request. Now, when we make a request for the email and specify the user under request, this data can be pulled from both APIs, joined together, and served as a single request.

Theoretical Example

While this might seem like a relatively simple request, we can envision a much more complicated one. Let’s assume we are using Rejoiner to deal with requests regarding eCommerce. We have a storefront that handles orders, and those orders might include shipping vendors, storefront fulfillment identifiers, and, ultimately, customer notifications. In such a situation, we have each aspect of the total experience separated into APIs – order status is handled by one microservice, shipping vendors by another, and store fulfillment identifiers by yet another.

The problem is that each request we might make is not simply a one-to-one relationship. It is entirely possible that a customer has a single order that addresses multiple vendors, and that these multiple vendors may need to consult shipping suppliers which can be unique for each individual shipment.

In such a case, using a traditional API is not a good solution. We’d be looking at a single order status request making five, six, or more circuits throughout the microservice collection, adding more time and complexity with each pass. With GraphQL and a service like Rejoiner, what we can instead do is append types to each of those aspects. The Orders microservice endpoint may have appended types including VendorID, StorefrontID, StatusID.

When an OrderID is supplied, the Orders microservice will respond with the current status as known and cached. The Rejoiner interface, however, will know that the Vendor fulfillment status (VendorFulfilled) is hosted in the Vendors API, and thus, will use the attached annotation to note that Vendor data is handled through that API, and not Orders. While the result may be stitched together at the end of the process through the Rejoiner interface API, each individual element can be routed and pushed to the appropriate endpoint given these annotations.

Rejoiner Benefits

It should be noted here that Rejoiner and GraphQL are two distinct solutions – many of the benefits we’ve discussed here are chiefly benefits of GraphQL with an additional benefit enabled through the Rejoiner interface at the top level. GraphQL is absolutely usable on its own, which bears the question – why not just use GraphQL and avoid Rejoiner altogether?

There are many benefits to GraphQL, but it’s really meant to be used in a relatively narrow scope in terms of number of supported services. Using GraphQL makes anything and everything possible for a single microservice, but when everything is enabled that way across a multitude of APIs, that can be a problem in terms of sheer exposed data. Thus, GraphQL is typically used on the front-facing API, while the underlying microservices are more clearly defined and interact in a more strict fashion.

Using Rejoiner brings up a wealth of opportunity in terms of treating internal microservices as if they were externally facing. In other words, the need to more strictly control the internal data between individual microservices that are meant to be used interoperably reduces considerably when one uses an annotated system such as Rejoiner. The practical impact of not having to control each and every aspect of the internal ecosystem only exposes greater flexibility, assuming that flexibility is both needed and appropriate.

Potential Cons

While there are some clear advantages to this approach, there are also some strong arguments that can be made against using Rejoiner in some applications. Part of the problem with Rejoiner is that it leans so heavily on GraphQL. The reality of GraphQL, however, is that not all situations are going to benefit from its ethos, development paradigm, and approach – in fact, there are some situations that GraphQL may actively make more difficult.

One of the big costs of adopting GraphQL is that it necessarily opens the database to a higher theoretical cost per call. When a call can be crafted to include as little or as much information as possible, the database must be designed in a way to specifically support this and to allow whatever it wants to allow to be called in whole or in part. This cost is by far negated by the benefits of GraphQL itself in most applications, but in such applications, the main thing negating that cost is the fact that external actors are fundamentally unpredictable. If you can’t predict the interaction, allowing the client requesting said interaction to control their own request is a good choice.

Internal services are predictable, however. A properly coded infrastructure should have a request flow that is predictable and understandable. Even if the end request is perhaps larger than you first thought, that larger request should necessarily stitch other API microservices together, not allow an endless amount of data. In practice, if an API is requesting a specific internal dataset from a microservice, it should be expected, known, and quantifiable. Therefore, utilizing GraphQL in such situations (and, thus, using Rejoiner) can increase database costs while not really delivering any actual value to the end-user.

There is also the fact that Rejoiner obfuscates the internal microservices to the point of making your API entirely unintelligible at the base level. While there are certainly cases where this is an ok thing to do (and many in the security space would argue this is a pro, not a con), in situations in which you need transparency and openness in the system, you simply are forced to invest a lot of effort into metadata and contextual communication.

Conclusion

As with most things in the API space, Rejoiner is a great option for what it does – given that what it does is highly specific, however, it’s not something that can be broadly recommended for all situations. At its best, Rejoiner elevates a network of microservices to higher extensibility, usability, and iterative potential. If used incorrectly, it could result in needless complexity in both management and operation. That being said, if your use case calls for internal synchronicity between microservices that face to the public in a single stance, then Rejoiner is an excellent option.

What do you think about Rejoiner? Are there any alternatives that are perhaps better serving to the general use case? Let us know in the comments below.