Kotlin and Spring Boot: Hypermedia Driven Web Service

Learn about HATEOAS, build a state machine to model an article review workflow, use Spring-HATEOAS and see how hypermedia clients adapt. By Prashant Barahi.

Leave a rating/review
Download materials
Save for later
Share

In software engineering, much effort goes toward building a loosely coupled system. A loosely coupled system allows you to reduce the effect of changes made in one part of the system on other parts.

You can decouple the frontend application from the server by making the latter send extra information in the response. This information tells the client what actions are possible on a given resource. Clients can then use this information to drive the application from one state to another. This is HATEOAS.

HATEOAS stands for Hypermedia as the Engine of Application State. Although it sounds overwhelming, the idea is rather simple. It’s the way of designing a REST application so the server embeds hypermedia links as metadata in the response. The server crafts these links using the current application state before sending them to the client. The client can then use those hypermedia links to drive the application from the current state to another.

This article and every other one you’ve read on our website went through a review workflow. In this tutorial, you’ll build a hypermedia-based server by modeling a similar review workflow that’s practiced by our tutorial team.

In the process, you’ll:

  • Learn the concepts of HATEOAS.
  • Build a state machine to model the review workflow.
  • Use Spring-HATEOAS to build a hypermedia-powered Spring Boot server.
  • Handle change requirements and see how hypermedia-based clients can adapt.
Note: This tutorial assumes you’re familiar with Spring Boot and Kotlin. If you’re new to Spring Boot, refer to Kotlin and Spring Boot: Getting Started tutorial.

Getting Started

Click Download Materials at the top or bottom of the tutorial to download the starter project. Fire up the IntelliJ IDEA and select Open…. Then, navigate to and open the starter project.

First, make sure you’ve installed JDK 11 or higher. Open Application.kt and run the project.


Main application

Or you can also open a Terminal and execute:

./gradlew bootRun

You’ll find the server running on port 8080.

The starter project also contains a React application in the /ui folder. To build and run it, make sure you have Node.js v16.15.1 and npm v8.11.0 (or greater) installed by executing the following commands:

node -v

And:

npm -v

If you don’t have them, go to the official site to download and install them.

Then, build and run the React application by executing this:

cd ui
npm ci
npm run start

You should find it running on port 3000.

Open your browser and go to http://localhost:3000. By default, it makes requests to localhost:8080 (where the Spring Boot server is running), so you’ll see a list of articles like this:

Articles home page

The app isn’t fully usable now, so minimize it.

Note: If you aren’t familiar with React, don’t worry: You won’t need to make any changes to the React app. This tutorial will explain its workings later.

Now, look at the starter project. It contains the following files:

  • ArticleSeedInitializer.kt seeds the empty database with initial data.
  • StateMachineConfigurer.kt provides APIs you use to configure the states and the transitions of a state machine. Then, you supply it to a StateMachineFactory.
  • ArticleStateMachineBeanConfig.kt is where you configure ArticleStateMachineFactory.
  • StateMachineFactoryProvider.kt provides methods to retrieve a StateMachineFactory bean you’ve configured.
  • FakeNetworkDelayFilter.kt adds a random delay to every request.

What Is a State Machine?

A state machine is a mathematical model of computation. It has a finite number of states and can be in any one of them at a time.

It starts from an initial state and responds to events by transitioning from its current state to the next if the current state allows it. On reaching the next state, it becomes ready to accept events again.

In this project, you use a state machine to drive the review workflow. Before you dive into the code, you’ll first learn about the business problem you’ll solve.

Three Level Review Workflow

Each article goes through a review process before it gets published. For now, consider there are just three steps. Call it the Three Level Review Workflow (3LRW)!

The author starts the article. They pick a topic from the topic pool and write the article. Once the draft is ready for review, it goes to the technical editor (TE).

Technical editors proofread the article and perform an expert-level review of the content. They can either approve the article and send it to the final pass editor (FPE) or reject the article with feedback if they feel it needs further polish.

Final pass editors stand as the last line of defense. Once they approve the article, they publish it, making it available to readers. After this, any updates to the article are not allowed. However, if the article fails the quality check, the final pass editor can send it back to the author and the process restarts.

The 3LRW state diagram is:

Three Level Review Workflow

Next, you’ll model this workflow using a state machine.

Building the 3LRW State Machine

The starter project already has an outline of a state machine implemented.

ArticleState defines all the states a state machine can go through in 3LRW, namely DRAFT, AUTHOR_SUBMITTED,TE_APPROVED and PUBLISHED.

Similarly, AUTHOR_SUBMIT, TE_APPROVE, TE_REJECT, FPE_APPROVE and FPE_REJECT are the events the 3LRW supports. They’re defined in the ArticleEvent.

Refer to the state diagram to understand how these events and the states relate to each other.

To build a StateMachine with a 3LRW configuration, you need a StateMachineFactory. Therefore, you also need to provide a configuration to the StateMachineFactory to comply with 3LRW. Open ArticleStateMachineBeanConfig.kt and go to providesThreeLevelReviewStateMachineFactory():

fun providesThreeLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
  // ...
  .withTransitions {
    // Author
    defineTransition(start = DRAFT, trigger = AUTHOR_SUBMIT, end = AUTHOR_SUBMITTED)

    // TODO: define other transitions
  }
  // ...

This is an incomplete implementation of a 3LRW state machine. So replace all the block content with the following snippet:

// Author
defineTransition(start = DRAFT, trigger = AUTHOR_SUBMIT, end = AUTHOR_SUBMITTED)

// TE
defineTransition(start = AUTHOR_SUBMITTED, trigger = TE_APPROVE, end = TE_APPROVED)
defineTransition(start = AUTHOR_SUBMITTED, trigger = TE_REJECT, end = DRAFT)

// FPE
defineTransition(start = TE_APPROVED, trigger = FPE_APPROVE, end = PUBLISHED)
defineTransition(start = TE_APPROVED, trigger = FPE_REJECT, end = DRAFT)

Here, you’ve defined the event (i.e., trigger) that — when consumed — causes the state machine to transition from the start state to the end state.

Notice the return type of this function is an ArticleStateMachineFactory. This is just an alias for StateMachineFactory<ArticleState, ArticleEvent>.