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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin and Spring Boot: Hypermedia Driven Web Service
30 mins
- Getting Started
- What Is a State Machine?
- Three Level Review Workflow
- Building the 3LRW State Machine
- Handling Events
- Understanding Hypermedia Responses
- Supporting Hypermedia in Spring Boot
- Media Types
- Building the “Self” Link
- Affordance
- Building the “update” Link
- Building the Tasks Link
- Understanding the Hypermedia Client
- A Non-Hypermedia-Based Client
- Four Level Review Workflow
- Building the 4LRW State Machine
- Where to Go From Here?
Four Level Review Workflow
The tutorial team is growing! With team members from around the world, the management team decided to improve the content’s quality by introducing Editors into 3LRW.
After technical editing, the article goes to the editor. Editors ensure the writing is clean, concise and grammatically correct and follows guidelines before they send it to the final pass editor. This successor to 3LRW is called the Four Level Review Workflow (4LRW):
This is a breaking change for non-hypermedia clients. But can this be implemented without making any changes to the client?
Building the 4LRW State Machine
Before starting with the code, ensure the React app is running. Go to localhost:3000 in your browser and ensure 3LRW is working.
First, you need to define the additional states and events the 4LRW requires. Go to ArticleState.kt and add EDITOR_DONE
state to the enum class. And in the ArticleEvent.kt, add EDITOR_APPROVE("editorApprove")
.
In the ArticleStateMachineBeanConfig
, define a new ArticleStateMachineFactory
for 4LRW by adding the following method:
@Primary
@Bean(FOUR_LEVEL_REVIEW_STATE_MACHINE)
fun providesFourLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
val configuration = StateMachineConfigurer.StateBuilder<ArticleState, ArticleEvent>()
.withStartState(DRAFT)
.withEndState(PUBLISHED)
.withStates(
DRAFT,
AUTHOR_SUBMITTED,
TE_APPROVED,
EDITOR_DONE,
PUBLISHED
)
.and()
.withTransitions {
// 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)
// Editor
defineTransition(start = TE_APPROVED, trigger = EDITOR_APPROVE, end = EDITOR_DONE)
// FPE
defineTransition(start = EDITOR_DONE, trigger = FPE_APPROVE, end = PUBLISHED)
defineTransition(start = EDITOR_DONE, trigger = FPE_REJECT, end = DRAFT)
}
return StateMachineFactory(ReviewType.FOUR_LEVEL_WORKFLOW, configuration)
}
Outside the class, add the following line:
const val FOUR_LEVEL_REVIEW_STATE_MACHINE = "FourLevelReviewStateMachineFactory"
And finally, add FOUR_LEVEL_WORKFLOW
in the ReviewType
:
FOUR_LEVEL_WORKFLOW {
override val key = this.name
}
The most important part is annotating the providesFourLevelReviewStateMachineFactory()
with @Primary
and removing it from the providesThreeLevelReviewStateMachineFactory()
. After this, the outline of the ArticleStateMachineBeanConfig
becomes:
const val THREE_LEVEL_REVIEW_STATE_MACHINE = "ThreeLevelReviewStateMachineFactory"
const val FOUR_LEVEL_REVIEW_STATE_MACHINE = "FourLevelReviewStateMachineFactory"
@Configuration
class ArticleStateMachineBeanConfig {
@Bean(THREE_LEVEL_REVIEW_STATE_MACHINE)
fun providesThreeLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
// ...
}
@Primary
@Bean(FOUR_LEVEL_REVIEW_STATE_MACHINE)
fun providesFourLevelReviewStateMachineFactory(): ArticleStateMachineFactory {
// ...
}
}
The StateMachineFactoryProvider
‘s getDefaultStateMachineFactory()
will now return the 4LRW StateMachineFactory
because you annotated it with @Primary
.
Check ArticleService
‘s save()
:
fun save(title: String, body: String): ArticleEntity {
val stateMachineFactory = stateMachineFactoryProvider.getDefaultStateMachineFactory()
return ArticleEntity
.create(
title = title,
body = body,
reviewType = stateMachineFactory.identifier as ReviewType
)
.let(repository::save)
}
Every newly created article will use 4LRW, because getDefaultStateMachineFactory()
will return the 4LRW factory.
Also, look at the handleEvent()
:
fun handleEvent(articleId: Long, event: ArticleEvent) {
// ...
val stateMachineFactory = stateMachineFactoryProvider
.getDefaultStateMachineFactory<ArticleState, ArticleEvent>()
val stateMachine = stateMachineFactory
.buildFromHistory(article.getPastEvents())
// ...
As you might have guessed, this code won’t work for existing 3LRW articles because the default factory now follows 4LRW.
Because there is more than one StateMachineFactory
now — one for 3LRW and one for 4LRW — you need to choose which StateMachineFactory
to use. Do this by passing the review type to StateMachineFactoryProvider
‘s getStateMachineFactory()
.
Replace the code in handleEvent()
, where it gets the default factory, to the following:
fun handleEvent(articleId: Long, event: ArticleEvent) {
// ...
val stateMachineFactory = stateMachineFactoryProvider
.getStateMachineFactory<ArticleState, ArticleEvent>(article.reviewType)
// ...
Also, open ArticleAssembler.kt and replace the code in getAvailableActions()
where it gets the default factory to the following:
private fun getAvailableActions(entity: ArticleEntity): List<ArticleEvent> {
// ...
val stateMachineFactory = stateMachineFactoryProvider
.getStateMachineFactory<ArticleState, ArticleEvent>(entity.reviewType)
// ...
This means the code now is retro-compatible, because getStateMachineFactory<ArticleState, ArticleEvent>(article.reviewType)
will return the 3LRW factory for those articles previously saved with the THREE_LEVEL_WORKFLOW
review type.
Now, restart the server and go to localhost:3000. Use the form at the bottom of the page to create a new article. Go to its detail page and use the workflow buttons to approve or reject the article. You’ll find that the newly created article uses 4LRW without you having to change any client-side code. Also, try out the 3LRW retro-compatibility! Choose an article created with 3LRW and go through the workflow. It should now follow the 4LRW process.
The client “adjusts” itself to changes in the workflow. This is possible because the hypermedia-based server offers the necessary metadata.
Where to Go From Here?
Click Download Materials at the top or bottom of the tutorial to download the final project.
Good job making it all the way through! You learned to create a hypermedia-based server using Spring-HATEOAS and how it helps lower the coupling between the client and the server. You also saw how the hypermedia-based client adjusted to the changes in the review workflow.
To complete the review workflow, try introducing the illustrator into the workflow. When the editor is done, the illustrator picks that article up and designs the perfect illustration for it before handing it over to the final pass editor. Its state diagram is:
Find its solution in the challenge folder of the materials.
Here are some links for further reading:
- Spring HATEOAS – Reference Documentation
- Richardson Maturity Model
- REST APIs must be hypertext-driven
If you have questions or comments, please join the forum discussion below!