Building an Educational Platform on Neo4j


The data model for the GraphAcademy database

In a previous article, I went into detail on the reasons why we rebuilt Neo4j GraphAcademy. If you are not aware, GraphAcademy is a free, self-paced online training platform that acts as the first port of call for many developers starting with Neo4j.

Introducing the New GraphAcademy

It was always my intention to build the new site in the open on the Neo4j Twitch Channel, but in the end, time got the better of me and I ended up quietly developing the backend in stealth mode. So, in lieu of a Twitch stream, I thought I would take some time to write a follow-up post with some of the more technical aspects of the build.

Educating users about Neo4j. Using Neo4j.

Naturally, the platform is built on top of a Neo4j Aura database. It may be a surprise that many companies don’t use their own products but I thought it was important to eat our own dog food with this project. With that said, there were some moments of serendipity along the way that justified the decision.

I’ll go through some of those moments in more detail.

The Data Model

As this is a Neo4j blog, let’s start with the data model used in the graph.

The approach we recommend in the Graph Data Modeling Fundamentals course is to split the data model out into statements and use the verbs and nouns to form a data model. This being an educational platform, we are storing data on users who enrol in courses. To make the courses more manageable, we decided to split out the new courses into modules, each of which would contain one or more lessons.

Here is how those parts translated into the graph:

  • A Course is listed in one or more Categories. Categories form a hierarchy, so a Category may also have child categories.
A Graph diagram produced with arrows.app.

Serendipity: This may seem pretty straightforward, but one thing I discovered at this point was how easily Neo4j can handle hierarchies. Back in my early years of development, I struggled with a few “product in child category of X” style problems. With Neo4j, you can quickly find paths through a hierarchy of relationships until you find a leaf with a single line of Cypher.

  • A Student enrols to a Course — or a student has an enrolment for a course.
  • A screenshot of my enrolment for the Neo4j Fundamentals course from Neo4j Browser.

    Relationships in Neo4j have a type, direction, and single start and end nodes. Extracting the enrolment out into a node in its own right gave me the ability to link that enrolment to other facts. For example, as part of the enrolment, a user may attempt a quiz. That attempt would contain one or more correct or incorrect answers to questions.

    • A Lesson may have one or more Questions — a simple one here, defaulting back to a “has” or “is” relationship. When in doubt…
    In this screenshot, the red Enrolment node is linked to a yellow Attempt node. The Attempt has an additional SuccessfulAttempt label applied to it. The two green answer nodes represent an Answer to the question.

    In order to judge the quality of our quizzes and challenges, each attempt that the user makes is stored in a node along with their answers. The success rates for each question are monitored, and where pass rates are low, we make changes to the course content and the pass criteria in order to make the questions as easy as possible.

    • Courses have prerequisites.

    Progression was a major factor during this rebuild. One piece of feedback was that the previous version of the website didn’t communicate clearly enough the order in which the courses should be completed, or what a user should do next once they have completed a course.

    Follow your path from Neo4j Fundamentals to Importing Data

    Each course now has recommended prerequisite courses and courses to advance to. These have been curated to help guide the user towards a Neo4j Certification (and a free t-shirt!).

    Showing the learning path through the Cypher Fundamentals course. Once you have learned the fundamentals of Cypher, you can either progress to Data Modeling or learn more advanced Cypher

    It takes a single line of Cypher to find a list of prerequisites for that course, and another to filter out any courses that the user is already enrolled on. This allows us to provide better recommendations and guide the user on what they should be doing next.

    MATCH (u:User {id: $sub})-[:HAS_ENROLMENT]->()-[:FOR_COURSE]->(c)
    WITH collect(c) AS enrolledTo

    MATCH (:Course {id: "importing-data"})-[:PREREQUISITE*]->(prerequisite)
    WHERE not prerequisite IN enrolledTo
    RETURN prerequisite

    In fact, there are linked lists all over the database. I almost add linked lists as a force of habit now but they are extremely useful.

    For example, we’ve made a lot of assumptions in our course catalogue. The Beginners courses were ordered this way due to a combination of intuition and experience. But as more users register and enrol, we can analyse :NEXT_ENROLMENT relationships between their enrolments to see how users are actually using the platform.

    We can also look at the attempts that learners are making during the quizzes or how they attempt code challenges. If there is a common pattern of multiple :UnsuccessfulAttempt nodes linked to a question before a :SuccessfulAttempt, that could be a sign that we need to revise the lesson or make some questions clearer.

    If you want to learn more about how to model your data in Neo4j, you can enrol to the Graph Data Modeling Fundamentals course.

    Tech Stack

    Here are the highlights of the technologies I used to build the site:

    Built with TypeScript

    The website is built using TypeScript — I wanted to build something as quickly and simply as possible, so rather than use any of the libraries or frameworks I had put together over the years (neode, nest-neo4j, use-neo4j react hooks), I thought why not just run Cypher queries directly through the Neo4j JavaScript Driver.

    As the complexity of the website grew, this decision was justified — there is some pretty complex Cypher in there at points.

    At first, I was on the fence about TypeScript. But the more I’ve worked with it, the more I‘ve enjoyed the experience. Many potential bugs are squashed early on in the development process. A few months on, it feels strange to write vanilla JavaScript without type definitions.

    In terms of the “proper way” of using TypeScript with Neo4j, I asked a couple of colleagues for their suggestions — there were a couple of ways to look at things. The first (bullet-proof way) would be to create classes with getters and setters — this had the drawback of having to somehow find a way of hydrating the response from the driver into these classes. But again, as the complexity grows, this takes more maintenance, and it seemed like overkill for an object that would just be passed through to a pug template.

    The driver also supports typescript generics, but there is no check on the result itself so these just become syntactical sugar.

    The way I settled on was to create an interface to reflect the object that would be returned by the query — loosely mapped to the properties of the nodes themselves.

    export interface Course {
    slug: string;
    title: string;
    link: string;
    video?: string;
    duration?: string;
    redirect?: string;
    thumbnail: string;
    caption: string;
    status: CourseStatus;
    interested?: string;
    usecase: string | undefined;
    modules: Module[];
    categories: Category[];
    prerequisites?: Course[];
    progressTo?: Course[];
    badge?: string;
    }

    Rather than returning Nodes from the driver, I settled on returning a map projection of the properties I was interested in. In the app, I have a function that generates the Cypher needed to return information about the course, modules and lessons. Here is an abridged version of the output:

    MATCH (c:Course {slug: "neo4j-fundamentals"})
    OPTIONAL MATCH (:User {username: "adam.cowley"})-[:HAS_ENROLMENT]->(e)-[:FOR_COURSE]->(


    RETURN c {
    .slug,
    .title,
    .thumbnail,
    .caption,
    .status,
    .usecase,
    .redirect,
    .link,
    .duration,
    .video,

    enrolled: e IS NOT NULL,
    categories: [
    (c)-[:IN_CATEGORY]->(category) |
    category {
    .id,
    .slug,
    .title,
    .description,
    link: '/categories/'+ category.slug +'/'
    }
    ],
    modules: [
    (c)-[:HAS_MODULE]->(${module}) |
    m { .id, .title }
    ],
    prerequisites: [
    (c)-[:PREREQUISITE]->(p) WHERE p.status <> 'disabled' |
    p { .link, .slug, .title, .caption, .thumbnail }
    ],
    progressTo: [
    (c)<-[:PREREQUISITE]-(p) WHERE p.status <> 'disabled' |
    p { .link, .slug, .title, .caption, .thumbnail }
    ]
    } AS course

    As you can see, the output of the Cypher query maps to the TypeScript interface.

    Express.js & Pug

    We use Asciidoc across our documentation and with the previous site. So building a site that was served by Express.js with Pug templates meant that I could easily integrate Asciidoctor.js to turn the courses into HTML.

    Straight up HTML

    The UI isn’t built with a front-end framework, and it is not a SPA — a colleague had put together a prototype using Gatsby, but in reality, the nature of the course files meant that these frameworks ended up complicating things. Some of the courses were generating a few KB’s of HTML.

    Some may think this is an antiquated way of building a website, but to be honest, after a few years of building apps with React & Vue — not having to deal with state management, etc. — it was a breath of fresh air. Everything just worked.

    Building on Neo4j Aura

    I know that you’d expect me to be full of praise for Neo4j & Aura, but in truth, it gave me some peace of mind. I like to do things myself, and I’ve been installing and tuning Neo4j databases for years. But GraphAcademy was developed and is currently being hosted on Aura Free, although as we scale up I expect that we will move over to the Professional tier. That’s a limited-size graph (200k nodes, 400k relationships) free of charge.

    As soon as we hit those limits, it’ll be $65 a month — you wouldn’t get much time from a DevOps or Server Admin for that amount.

    The fact that I could just scale up the database with a couple of clicks in the Aura Console made the job easy.

    What’s next for GraphAcademy?

    We’ll continue to develop the GraphAcademy platform, integrate existing Neo4j tools and build out the course catalogue with new courses. If you think there is something missing from the course catalogue, feel free to email us at graphacademy @ neo4j.com with your suggestions.

    If you are interested in getting started with graph databases but are not sure where to start, please feel free to reach out to me on Twitter. I would be happy to explore the possibilities with you.

    Otherwise, GraphAcademy is a great place to learn everything you need to know to be successful with Neo4j.

    Happy learning!


    Building an Educational Platform on Neo4j was originally published in Neo4j Developer Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.