Writing a GraphQL server in Kotlin

In a previous post I wrote about the advantages of designing a graph-based relational-database schema. This post introduces a sample implementation of that design in the form of a GraphQL service written in Kotlin. The example exposes a Star Wars based API similar to the GraphQL Docs

View the code on GitHub

Toolkit

A rundown of how the app was built.

  • Kotlin My favorite language. It's statically typed so the compiler will tell you when you're doing something wrong. Runs on the JVM and 100% interoperable with Java. Concise syntax makes it easy to read and write.
  • Sprint Boot The easiest way to get a JVM web-application up and running.
  • GraphQL The successor to REST APIs. GraphQL is the way to expose an application server's interface. Clients request exactly what they need, nothing more, and there is no server-side work when a client changes what data it needs.
  • Exposed A library offering a type safe SQL wrapper for Kotlin.
  • GraphQL-Tools A library that builds an application's GraphQL schema at launch from a file written in the GraphQL schema language.

Domain layer

The app is split into two main packages. framework contains all generic code and does the heavy lifting. starwars builds on top of framework code and contains anything domain-specific. The domain layer follows the Model-View-Controller pattern. Here's a quick sample of what each component looks like.

Model


enum class Episode {
    newHope,
    empire,
    jedi,
}

abstract class Character(
        @SerializedName("name") val name: String,
        @SerializedName("appearsIn") val appearsIn: Set<Episode>,
        createdAt: Instant
) : AbstractNode(
        createdAt = createdAt
) {

    val toFriends get() = to(friends)

    val toSecondDegreeFriends get() = to(secondDegreeFriends)

    companion object {
        val friends = Relationship.AsymmetricManyToMany<Character, Character>(name = "friends")
        val secondDegreeFriends = friends.to(friends)
    }
}

The Character model has a name and set of episodes it appearsIn. A character's state (properties) does not include friends nor secondDegreeFriends since those are relationships that must be traversed tp using to(<Relationship>).

View (GraphQL schema)

# A character in the Star Wars Trilogy
interface Character {

    # The id of the character
    id: ID!

    # The name of the character
    name: String!

    # The friends of the character, or an empty list if they have none
    friends: [Character!]!

    # Which movies they appear in
    appearsIn: [Episode!]!

    # The friends of the droid, or an empty list if they have none
    secondDegreeFriends(limit: Int): [Character!]!
}

type Mutation {

    # Create a new droid
    createDroid(name: String!, primaryFunction: String!, friendIds: [ID!]!, appearsIn: [Episode!]!): Droid!
}

In the snippet above, we define the Character interface and a mutation to create a new Droid that will be publicly exposed to the world via the GraphQL. The graphql-tools library allows us to write this in the GraphQL Schema Language.

Controller

interface CharacterTypeResolver {

    val db: GraphDBRepository

    fun getId(character: Character) = character.id
    fun getName(character: Character) = character.name
    fun getAppearsIn(character: Character) = character.appearsIn
    fun getFriends(character: Character) = db.traverse(character.toFriends)

    fun getSecondDegreeFriends(character: Character, limit: Int?): List<Character> {
        val secondDegree = db.traverse(character.toSecondDegreeFriends)
                .distinct()
                .filter { secondDegreeFriend -> secondDegreeFriend != character }
        return if (limit == null) secondDegree else secondDegree.subList(0, minOf(limit, secondDegree.size))
    }
}

class DroidMutationResolver(
        val db: GraphDBRepository
) : GraphQLMutationResolver {

    fun createDroid(
            name: String,
            primaryFunction: String,
            friendIds: Set<Long>,
            appearsInIds: Set<Episode>
    ): Droid {
        val now = Instant.now()
        val droid = db.save(
                Droid(
                        primaryFunction = primaryFunction, 
                        name = name, 
                        appearsIn = appearsInIds, 
                        createdAt = now))
        val friendEdges = db.fetch<Character>(friendIds).map { friend ->
            Edge(
                    from = droid,
                    relationship = Character.friends,
                    to = friend,
                    createdAt = now)
        }
        db.saveEdges(friendEdges)
        return droid
    }
}

The controllers (aka resolvers) connect our GraphQL schema with our internal datamodel and apply any necessary application logic such as fetching nodes, traversing edges and manipulating data to fulfill graphQL requests.

Framework layer

The framework layer is essentially an object-graph mapper (OGM) that maps models in the domain layer with a persistance layer defined generically in terms of nodes and relationships, eliminating the need for database schema migrations.

Concepts

  • Node
    The base class for all entity types. Nodes have a non-null id field which determines whether two nodes are equal. Node objects are given a primary key when they are persisted, however it's not required that they be persisted. When a concrete Node instance is persisted, it uses gson serialization to capture the state of the object and puts the resulting json in a column named 'attributes'
  • Edge
    An edge connects two Node instances in the graph.

  • Relationship
    A Relationship describes a path through the graph from one Node type to a second Node type.

  • Hop
    A Hop is a special type of Relationship that doesn't go through any other Node types. In other words, a Hop is a Relationship with a distance equal to 1.

  • Symmetric vs Asymmetric
    Hops may optionally have a direction. If a Hop has a Direction, it is said to be Asymmetric. A 'mother' relationship would be an example of an Asymmetric Relationship. On the other hand a 'sibling' Relationship would be an example of a Symmetric Relationship.

  • ToOptional vs ToSingle vs ToMany
    The cardinality of a Relationship can be expressed in one of 3 ways. ToOptional indicates the destination Node may or may not exist. ToSingle indicates the destination Node is guaranteed to exist. ToMany indicates there may be any number of destination nodes. Because we can call Relationship.inverse it is also important to define the 'from' cardinality of a relationship in terms of FromOptional FromSingle or FromMany.

  • Traversal
    A Traversal is a simple structure that encapsulates both a Relationship and the Node(s) to start from when preparing to traverse a Relationship through the graph.

  • FrameworkRepository
    An interface that must be implemented which registers the mapping between domain object classes and their Node type(s). This is required for serialization/deserialization as well as efficient fetching.

Check out the source code on GitHub.

Show Comments