Last Updated on November 25, 2022
Welcome to the first of this three part series where we’ll take a Spring Boot microservice from inception to deployment.
In this article, we’ll create a simple Spring Boot API application with integration tests, and then build it in a Jenkins pipeline every time a change is pushed to version control.
The full series of articles includes:
- Part 1: writing a Spring Boot application and setting up a Jenkins pipeline to build it (this article)
- Part 2: wrapping the application in a Docker image, building it in Jenkins, then pushing it to Docker Hub
- Part 3: deploying the Docker image as a container from Jenkins into AWS
After this series you’ll have a solid foundation in the best practices to bring continuous integration to microservice architectures.
Ready? Let’s get right into it!
UPDATED in June 2021 to use latest Spring, Gradle, Jenkins and other plugin/dependency versions.
1. Creating a Spring Boot application
Spring Boot is a framework for easily writing Java web applications. In our case, we’ll use it to create an API service. This is a common requirement of microservice architectures, especially those that have a UI that communicates with a backend API.
Just for fun, we’ll base the application around theme park rides. 🎢 It will be geared towards doing basic get & create operations on theme park rides, comprising a:
GET
API at/ride
to get all the rides the application knows aboutGET
API at/ride/{id}
to get details of a specific ride by idPOST
API at/ride
to add a new ride
Code repo: following along with the code snippets and creating the project from scratch is the best way to get more familiar with Spring Boot. If you want a fast-pass ticket though, here’s all the code in GitHub.
Application design
We’ll use a fairly typical Spring Boot Model-View-Controller format, which normally looks like this:

In our case though, we won’t implement a service as there is no business logic to speak of.
Build file
We’ll be using the popular Gradle tool to build this application. Make sure you’ve got a recent version installed on your machine before proceeding.
Create a new directory for the project, and navigate into it. Run gradle init
to start the Gradle setup wizard, choosing to create a basic Groovy project with the default name. This creates a skeleton of a Gradle project, including the Gradle wrapper used for interacting with the application.
Gradle help:
1) If you’re using IntelliJ IDEA check out this article to learn how to create Gradle projects from your IDE
2) For a more general overview check out my introduction to Gradle YouTube video
3) Here’s an article all about the infamous Gradle wrapper
A build.gradle file is automatically generated. Modify it so it looks like the one below:
plugins { id 'java' id 'io.spring.dependency-management' version "1.0.11.RELEASE" id 'org.springframework.boot' version '2.5.0' id 'pl.allegro.tech.build.axion-release' version '1.13.2' } version = scmVersion.version sourceCompatibility = 8 repositories { mavenCentral() jcenter() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-devtools' implementation group: 'com.h2database', name: 'h2', version: '1.4.200' compileOnly 'org.projectlombok:lombok:1.18.20' annotationProcessor 'org.projectlombok:lombok:1.18.20' testImplementation 'org.springframework.boot:spring-boot-starter-test' } test { useJUnitPlatform() }
- the Spring
dependency-management
andboot
plugins bring Spring Boot functionality to our build - the
axion-release
plugin adds versioning using tags - we’re got several Spring Boot dependencies to allow us to use the Spring Boot web framework with databases
- the
h2
dependency will enable us to use an in memory database - Lombok has been configured as an annotation processor to reduce boilerplate code through code generation
Now let’s create some Java classes. This, and all the following classes, should be created in src/main/java in the package that appears at the top of each individual class definition.
Entity class
The entity class represents each instance of a theme park ride. Let’s create a ThemeParkRide
class:
package com.tomgregory.entity; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.ToString; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.validation.constraints.NotEmpty; @Entity @Getter @ToString @NoArgsConstructor public class ThemeParkRide { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; @NotEmpty private String name; @NotEmpty private String description; private int thrillFactor; private int vomitFactor; public ThemeParkRide(String name, String description, int thrillFactor, int vomitFactor) { this.name = name; this.description = description; this.thrillFactor = thrillFactor; this.vomitFactor = vomitFactor; } }
- this is a simple POJO (plain old Java object) class with
id
,name
,description
,thrillFactor
, andvomitFactor
fields 🤮 - the class is annotated with
@Entity
so Spring Boot knows what kind of class it is - we’re using the Lombok annotation processor to generate a default constructor, a
toString()
method, and getter methods for each field @NotEmpty
annotations are applied to the name and description fields for validation when we create a theme park ride

Repository class
We’ll use a repository interface to read and write ThemeParkRide
entities from/to the database. The beauty of the spring-boot-starter-data-jpa library that we imported in build.gradle is that you only have to define the method signatures of the interactions you want with the database, and the library does the rest.
Create an interface called ThemeParkRideRepository
:
package com.tomgregory.repository; import com.tomgregory.entity.ThemeParkRide; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import java.util.List; @Repository public interface ThemeParkRideRepository extends CrudRepository<ThemeParkRide, Long> { List<ThemeParkRide> findByName(String name); }
- the
@Repository
annotation lets Spring Boot know what kind of component this class is - extending
CrudRepository
provides some default methods such assave
andfindById
- the
findByName
method definition will be implemented automatically by Spring Boot, matching to thename
field of theThemeParkRide
entity class
Controller class
A controller class defines an API and how it interacts with the rest of the application. Create a class called ThemeParkRideController
:
package com.tomgregory.controller; import com.tomgregory.repository.ThemeParkRideRepository; import com.tomgregory.entity.ThemeParkRide; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.*; import org.springframework.web.server.ResponseStatusException; import javax.validation.Valid; @RestController public class ThemeParkRideController { private final ThemeParkRideRepository themeParkRideRepository; public ThemeParkRideController(ThemeParkRideRepository themeParkRideRepository) { this.themeParkRideRepository = themeParkRideRepository; } @GetMapping(value = "/ride", produces = MediaType.APPLICATION_JSON_VALUE) public Iterable<ThemeParkRide> getRides() { return themeParkRideRepository.findAll(); } @GetMapping(value = "/ride/{id}", produces = MediaType.APPLICATION_JSON_VALUE) public ThemeParkRide getRide(@PathVariable long id){ return themeParkRideRepository.findById(id).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, String.format("Invalid ride id %s", id))); } @PostMapping(value = "/ride", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public ThemeParkRide createRide(@Valid @RequestBody ThemeParkRide themeParkRide) { return themeParkRideRepository.save(themeParkRide); } }
- the
@RestController
annotation tells Spring Boot what type of component this is - for the
getRides
method we use the@GetMapping
annotation to tell Spring Boot we’re expecting aGET
request at the specified path - the
getRide
method has a@PathVariable
annotation so we can pass the ride id in the path/ride/{id}
- the
createRide
method uses the@RequestBody
annotation to automatically map the JSON request to theThemeParkRide
entity. - the
@Valid
annotation is included on the incomingThemeParkRide
entity to validate it according to any annotations on its fields (e.g.@NotEmpty
)
Application class
The final class we have to add to have a fully working (but untested) application will be ThemeParkApplication
:
package com.tomgregory; import com.tomgregory.entity.ThemeParkRide; import com.tomgregory.repository.ThemeParkRideRepository; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @SpringBootApplication public class ThemeParkApplication { public static void main(String[] args) { SpringApplication.run(ThemeParkApplication.class); } @Bean public CommandLineRunner sampleData(ThemeParkRideRepository repository) { return (args) -> { repository.save(new ThemeParkRide("Rollercoaster", "Train ride that speeds you along.", 5, 3)); repository.save(new ThemeParkRide("Log flume", "Boat ride with plenty of splashes.", 3, 2)); repository.save(new ThemeParkRide("Teacups", "Spinning ride in a giant tea-cup.", 2, 4)); }; } }
- the
@SpringBootApplication
annotation tells Spring Boot that this class is defining our main application - the
main
method means that when we run this class the application will start - the
sampleData
bean definition adds three default rides for us
2. Trying out our Spring Boot application
Run the ThemeParkApplication
class or ./gradlew bootRun
to start the application:

Let’s make some requests to the application. I recommend the Postman tool for easily making requests via a nice UI.
Get all rides
Let’s call GET
/ride
to get all rides

We can see the three rides are returned as per the sampleData
bean definition in ThemeParkApplication
.
Create ride
Let’s POST
some JSON data to /ride
to create a new ride:
{ "name":"Monorail", "description":"Sedate ride that takes you around the park.", "thrillFactor":2, "vomitFactor":1 }
We’ll need to remember to also add a Content-Type
header of application/json
.

That’s a 200
OK
response. So far, so good.
Get ride
Let’s GET
the ride we just added calling /ride/4
:

The Monorail ride is returned as expected!
Adding a test
It’s great that everything’s working, but let’s add an automated test to validate this functionality so our fingers don’t get too tired. 🤞
What follows is an integration test class ThemeParkApplicationIT
that should be added to src/test/java
. We’ll use it to test interactions with the three APIs all the way through our application to the database.
package com.tomgregory; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @ExtendWith( SpringExtension.class) @SpringBootTest @AutoConfigureMockMvc public class ThemeParkApplicationIT { @Autowired private MockMvc mockMvc; @Test public void getsAllRides() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/ride") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); } @Test public void getsSingleRide() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/ride/1") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); } @Test public void returnsNotFoundForInvalidSingleRide() throws Exception { mockMvc.perform(MockMvcRequestBuilders.get("/ride/4") .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()) .andReturn(); } @Test public void addsNewRide() throws Exception { String newRide = "{\"name\":\"Monorail\",\"description\":\"Sedate travelling ride.\",\"thrillFactor\":2,\"vomitFactor\":1}"; mockMvc.perform(MockMvcRequestBuilders.post("/ride") .contentType(MediaType.APPLICATION_JSON) .content(newRide) .accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andReturn(); } }
- the 3 class level annotations are required so that Spring Boot initialises itself, creates the beans, and configures the
MockMvc
class MockMvc
can be used to make requests into our application. For example, in the first test we’re making aGET
request to/ride
and expecting astatus().isOk()
response
Run the test by executing ./gradlew test
. Open up build/reports/tests/test/index.html
to see your test report.

Everything’s A-OK!

3. Running Jenkins
The next stage is to automatically build our Spring Boot application on commit, or in other words to use continuous integration. This will ensure that our master branch is always in a good state, and anything that gets deployed into production is a working version of our application.
We’ll use the popular build tool Jenkins for this purpose, and will set it up to run as a Docker container on our local machine. For the following steps you’ll need Docker, which you can get from the Docker website.
Bootstrapping Jenkins
A good devops philosophy is to define all infrastructure and configuration as code. So, we’ll bootstrap Jenkins to start up and configure whatever jobs we need automatically, with minimal manual setup required.
This concept has been discussed in detail in my How to bootstrap Jenkins for disaster recovery and accountability YouTube video. We’ll build on top of the code example from the video, which can be found in this GitHub repository and cloned like this:
git clone https://github.com/tkgregory/jenkins-demo.git
Make sure the Spring Boot application is stopped as Jenkins uses the same port. Then build and run Jenkins:
./gradlew build docker dockerRun
Navigate to http://localhost:8080 and you’ll have an instance of Jenkins running:

You can see we already have one job called seed-job automatically available. This job is responsible for creating any other jobs that we need. Let’s run it by clicking on the name and then clicking Build Now.
Once it finishes, go back to the Jenkins home page and you’ll see that seed-job has generated an additional pipelineJob for us.

Build that job and you’ll see we have a successful pipeline execution, with a Build and Test stage:

Info: a pipeline in Jenkins is just a specific kind of job that can be split into different stages, as shown in the UI above
If you click on the blue circle to the left of #1 in Build History, you can see the console output:

These two stages just print out some text for now. The point is that we can very quickly have a working instance of Jenkins available. In the next section, we’ll add a new pipeline for our theme park application.
4. Adding a Jenkins pipeline job to build the Spring Boot application
Let’s modify the jenkins-demo project to include a new pipeline to build our Spring Boot application. The diagram below summarises the flow from when we initially run seed-job to when our pipeline job will run against our theme park application.

Once again, make the code changes yourself for the best learning experience. To do this you’ll need to create and push two of your own repositories:
- a repository for the Spring Boot theme park application (everything created in sections 1 and 2 of this article). Once created, you’ll need to substitute your own Git repository URL in the createJobs.groovy code snippet below.
- a repository for the Jenkins code. This should be based on the jenkins-demo project (you can fork the project if you like). Once created, edit the seedJob.xml, replacing the existing Git repository on line 10 with your own e.g.
<url>https://github.com/<your-username>/jenkins-demo.git</url>
If you don’t want to create any repositories and instead prefer to use the ones I’ve made available for you, I’ll also explain how to do that below.
Creating a new job
Open up the createJobs.groovy file, which is responsible for creating any jobs when seed-job is run. We’ll add the following section which defines an additional pipeline job:
pipelineJob('theme-park-job') { definition { cpsScm { scm { git { remote { url 'https://github.com/tkgregory/spring-boot-api-example.git' } branch 'master' } } } } }
This definition uses the Jenkins Job DSL Plugin syntax to create a pipeline job from a Jenkinsfile in the spring-boot-api-example repository (or use your own if you prefer). A Jenkinsfile is a pipeline definition that lives inside the repository of the project that it applies to.
Info: the Jenkins Job DSL plugin provides a mechanism to bootstrap Jenkins jobs. Without it, you’d have to create the jobs from the UI.
Push the above change to your own repository, as Jenkins has to fetch the createJobs.groovy file remotely.
If you don’t want to set that up, you can modify seedJob.xml and change the branch name on line 15 from master to theme-park-job.
<branches> <hudson.plugins.git.BranchSpec> <name>*/theme-park-job</name> </hudson.plugins.git.BranchSpec> </branches>
This way it will fetch the updated createJobs.groovy file from a branch I’ve made available just for you.
Adding a Jenkinsfile to spring-boot-api-example
We’ll create a Jenkinsfile in the top level of the project with the following contents:
pipeline { agent any triggers { pollSCM '* * * * *' } stages { stage('Build') { steps { sh './gradlew assemble' } } stage('Test') { steps { sh './gradlew test' } } } }
This is a basic pipeline that checks for any changes to our code every minute, then runs two stages:
- Build – where we assemble the project
- Test – where we test the project
Push these changes to your own repository, or just rely on the repository I’ve already added with the required Jenkinsfile.
Running the new job
Stop the previous Jenkins instance by running ./gradlew dockerStop
then bring up a fresh one with:
./gradlew build docker dockerRun
First run the seed-job again to create the new theme-park-job:

Then click on the new job and hit Build Now:

After some time you should have a successful build. Jenkins will highlight in green the two stages we defined, as well as the initial checkout.
5. Final thoughts
Getting Jenkins running and building a Spring Boot project can be quite straightforward. Obviously in a proper setup you won’t run Jenkins on your local machine, as you’ll need high availability and most likely access by other team members. If you’re using AWS consider running Jenkins in an ECS or EKS cluster (see Deploy your own production-ready Jenkins in AWS ECS).
In part 2 of this series we’ll move our Spring Boot application into Docker, build the Docker image in Jenkins, and then push that to a remote repository.
6. Resources
GITHUB
Check out the sample Spring Boot application for the theme park API
Here you can find the code for the jenkins-demo project, for bringing up an instance of Jenkins. The theme-park-job branch contains the updated version of createJobs.groovy.
GRADLE
Gradle Tutorial – why you should use it and how to get started
VIDEO
If you prefer to learn in video format, check out the accompanying video to this post on the Tom Gregory Tech YouTube channel.
Hi
I had to run
./gradlew build docker dockerRun
as 2 separate commands (I am on a Mac). ./gradlew build & docker dockerRun
This is a great article btw.
Hi. If you run the command you suggested, you’re running 2 separate commands:
Since dockerRun isn’t a valid option for the docker command, it will fail with this error.
Could you please send a screenshot of your issue to me at tom@tomgregory.com so I can investigate further?
Hi Tom,
Please can you provide an example of how to use GitHub access token using environment variables on the seedJob.xml and the createJobs.groovy.
git {
remote { url ‘https://ACCESS_TOKEN@github.com/REPO_URL’ }
branch ‘master’
}
Hi Mohammed. I’ll try to incorporate this suggestion on the next update of this article. Thanks!
Hi Tom.
Is there any way to specify a different application.properties file than the one on Github or to modify the values inside when building the job?
Hi Gligor. I’m not sure what you mean, as there is no application.properties file in the spring-boot-api-example repository. Can you please link me to the file you’re talking about?
Hello Tom.
I’m trying to include a private repo in a pipelineJob.
Is there any way to include credentials (user/password, token, or else) in the remote calling to GitHub repository? or is there any the other way to achieve that?
Great job, thanks for your knowledge.
Laloto
Hi Laloto. Yes you can do that.
Use the credentials syntax within the environment section of the declarative pipeline to inject Jenkins Credentials. Check out the docs for more info
Here’s the example straight from the docs:
Great!,
Thanks for helping.
You are all right.
Laloto
Hi Tom,
I am running the command on my Ubunutu
./gradlew build docker dockerRun
However, I got the error,
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :docker FAILED
The command ‘/bin/sh -c /usr/local/bin/install-plugins.sh < /usr/share/jenkins/ref/plugins.txt' returned a non-zero code: 6
FAILURE: Build failed with an exception.
I am confused by this. Could you take a look?
Thanks
Hi Gavin. I’m unable to replicate this issue. Are you using the jenkins-demo GitHub repository from the article?
Hi Tom,
I generated this project by myself on Gradle 6.8. So, there is possible reason from Gradle.
This is because this issue disappears when I downgrade to gradle 6.5.1-all.
Thanks
Hi Gavin. I’ve also tried with Gradle 6.8 and am unable to replicate the issue. Can you please check if you get the same issue with the jenkins-demo repository?
Tom, I have another question.
Since I made private repositories I get below error when the seedjob.xml is executed trhough Jenkins:
stderr: remote: Invalid username or password
It seems I should provide username and password but.. I don’t know where to provide.
Do you have any ideas?
Oh.. there is an easy way! You can explicitly give username and password in the URL in seedJobs.xml:
https://username:password@bitbucket.org/username/my_jenkins.git
Does this mean you have to put the username and password in the seedJobs.xml in plain text? If so, can you somehow export credentials from within Jenkins? Or is there some other way to provide credentials to the seed job?
I tried to add credentials to the seed job within Jenkins and tried to copy them out with the seed job file but couldn’t get it to work.
Looks like you can set a
credentialsId
in thehudson.plugins.git.UserRemoteConfig
. I haven’t tried it, but the docs suggest it’s possible.Hi Tom,
I have a question.
When I do GET test I get below error:
org.hibernate.InstantiationException: No default constructor for entity: : com.tomgregory.entity.ThemeParkRide
at org.hibernate.tuple.PojoInstantiator.instantiate(PojoInstantiator.java:85) ~[hibernate-core-5.4.21.Final.jar:5.4.21.Final]
at org.hibernate.tuple.PojoInstantiator.instantiate(PojoInstantiator.java:105) ~[hibernate-core-5.4.21.Final.jar:5.4.21.Final]
I wonder why you didn’t experience it. What version of hibernate did you use?
Could you please help me?
In addition I want to translate these series in Korean in my blog for Korea developers.
Would it be possible? I’m asking your permission.
Oh.. I found out how to fix the error.
In Eclipse you have to install lombok plugin(https://stackoverflow.com/questions/42301333/eclipse-lombok-annotations-not-compiled-why) and enable Annotation Processing in properties.
Hi Jongsun. Thank you for sharing your solution.
I generally won’t give permission to copy articles from my blog. If you have a specific proposal though, please email me.