Sharing dependencies between Spring Boot Microservices and projects with Maven for better code-reuse and clean design
What’s the problem
You’re looking at microservices, but you realise you want to send an object from one service to the other. You write a class, and then you copy and paste it from the code of one service to the other … 💡 there must be a better way to do this. You’re a good engineer and you know that copying around this POJO is bad practice.
Setting up a parent POM
We want to setup a common maven project that’s imported by any other project that needs its dependencies. At a high level to do that we need to:
- Create a «parent» maven file (
pom.xml
) - this is apom.xml
which will sit outside all of the projects that make up our microservices and allow us to tie them together - the goal is to make acommon
project but the easiest way to do that is to first create a parent pom. - Create a common maven project as one of the child projects.
- Import the common dependency from any other dependencies that need it
In a simple example where a simple «sales-service» is being built and depends on some common project, the file structure will need to reflect this. We will have something like this at the end of this guide.
.
├── pom.xml -- the parent pom
├── common
│ ├── pom.xml -- shared dependencies go in here
│ ├── src
└── sales-service
├── pom.xml -- will be modified to point to our own custom parent pom and import code from common
└── src
Parent pom.xml
For this to work, the maven groupId
must be shared across all maven projects in the tree. Each project will need a different artifactId
, as usual.
It should also describe the children, listing the directory names of child projects that should be part of this parent. For this we’ll create two children sales-service
and common
under the <modules>
list. Any subsequent child projects must be added to that list.
A very minimal parent pom.xml
for a project group com.ordering.services
that has two child modules can be as simple as:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ordering.services</groupId>
<artifactId>ordering-services</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>sales-service</module>
<module>common</module>
</modules>
</project>
Notice that the packaging
property is set to pom
- aggregator projects (parent projects) need to be set as POM packaging. This is because when the code is packaged, we’re not expecting to see a target JAR produced for the parent package.
Adding a project
We’ll now create and add a spring boot based project for sales-service
mentioned above. We can create a new Spring Boot project using the Spring Initializr. Once generated and brought into the directory structure some small changes need to be made to the sales-service projects’ pom.xml
.
Generated spring boot projects set the maven parent
attribute to a spring-boot-starter-parent
artifact.
This causes some confusion because we need to set the parent to our own new parent project.
Looking at the newly created sales-service/pom.xml
we need to:
- Remove the existing parent which is pointing to spring-boot
- Add our own parent, pointing to the
ordering-services
parent pom. - refactor the parent section for spring-boot into a regular dependency in the
dependencyManagement
section.
Remove the existing parent
Find this code and remove it, including the parent
tags - versions may differ. Open the pom.xml in sales-service
or whatever project you generated using the Initilizr.
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.14.BUILD-SNAPSHOT</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Point to our own parent
We want to set the parent to the project we setup earlier. To do this, we will set the parent to the parents artifact name ordering-services
and of course the groupId
. version
needs to match, too.
<parent>
<groupId>com.ordering.services</groupId>
<artifactId>ordering-services</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
Refactor spring boot into a management dependency
Now the spring-boot-starter-parent dependency is missing. Essentially, the Spring Boot dependency is missing, so it should be added again inside the dependencyManagement
attribute. The version
should match that of the one set in the deleted parent dependency setup.
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.1.14.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
By this stage a parent project has been setup with one child project (sales-service
) in it. The parent can see the child, and the child knows it’s part of the parent 👩👦
Making use of a common
dependencies project
Any project that’s setup and part of this parent-child relationship will now be able to use each others classes as if they were one big project. Conventionally that’s done via creating a common
project. A common project in a Maven build system is very powerful and saves a lot of configuration. Here are some of the things a common project can do for us:
- Share common classes (models, DTO’s etc) - typically good for making sure objects are serialised and de-serialised from the same class.
- Share configuration that needs to be used in more than one project
- Dependencies that are imported by all projects can be refactored into the
pom.xml
of the common project, then any project wanting to use these can just importcommon
.
Setting up common
Create a new spring project with the Initilizr - don’t add any dependencies - at least initially. Give it the groupId
and set the artifactId
to common
.
Just like other maven projects that are part of this parent, their parent should be set appropriate (like we done above for the sales-service). Delete the existing parent, and point it to your own parent.
Any generated dependencies can also be removed, we will hand pick any dependencies we want to share across multiple projects and put them in here.
Finally set 1.8 on maven.compiler.source
and maven.compiler.target
versions in the properties
section. A simple common
(under common/pom.xml
) project POM will now look something like this:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ordering.services</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>common</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>com.ordering.services</groupId>
<artifactId>ordering-services</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
</project>
Since there are no test dependencies, be sure to delete any test classes from the common project. One is provided by the spring initializr.
Also delete the main CommonApplication.java
since our common project won’t actually be a runnning application but instead a project for sharing common code between projects.
Also delete any test classes from in the common project
Set language versions
Move the the language version properties into the parent pom (./pom.xml
):
<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
This means any child project won’t need to set these properties now, so remove them.
So far we’ve setup a tree of dependencies that resembles the following with the descriptions of the changes we’ve made so far.
.
├── pom.xml -- parent pom has modules attribute pointing to `common` + `sales-service`
├── common
│ ├── pom.xml -- generated from initializr but with dependencies removed and parent set to one above
│ ├── src -- as generated but with test classes deleted
└── sales-service
├── pom.xml -- generated with desired dependencies but parent set to our own + spring boot dependencies added into the `dependencyManagement` section.
└── src
Using Common - an example
Adding a class to common will mean that any project in our tree that in turn imports common
will be able to use that class. This is great, since if we’re building with microservices pattern, then we can avoid copy paste of classes that need to be shared across multiple services.
To demonstrate this, create a class called Monkey
in a new package called payloads
inside the common
project. Give Monkey
a property name
and generate constructor and getters and setters.
//Monkey.java
public class Monkey {
private String name;
public Monkey(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Monkey
can be used in any project that imports common. So import the common project from the sales-service
project. Add the following dependency to sales-service/pom.xml
in the dependencies
attribute section:
<dependency>
<groupId>com.ordering.services</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
Notice that adding this dependency looks like any regular maven dependency that you’d add for a publically available package. But in this case notice that the group is our own group com.ordering.services
and the artifact is that of the common project.
We will create a controller in sales-service
that will use Monkey
class (which is defined and lives in common
to demonstrate this point.
//SalesController.java
@RestController
public class SalesController {
@GetMapping("/monkeys")
public ResponseEntity<Monkey> getMonkey(){
return new ResponseEntity<>(new Monkey("George"), HttpStatus.OK);
}
}
That’s it! Notice how the class Monkey
can be detected in the class-path and easily imported because our sales-service
project imports the common
dependency where we defined Monkey
.
Very cool - for any subsiquent projects that need to use the Monkey
class, they just need to use dependency noted above to be able to do so.
Note on Generating Jar
If we want to make sure that Jar files are generated for every project in our tree, then the maven property packaging
should be set to jar for each project we want a jar from when packaging is run. Add this just below the project version.
<packaging>jar</packaging>
We also need to enable the repackaging
goal on the spring-boot-maven-plugin
. This needs to be done if we’re going to run the built jars from the command line with java -jar
.
To do that here, we’ll look into sales-service/pom.xml
and find locate the build secton. Adding an execution to this with the goal name repackage
will enable this. The whole build attribute section now looks like this:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions> <!--new-->
<execution> <!--new-->
<goals> <!--new-->
<goal>repackage</goal> <!--new-->
</goals> <!--new-->
</execution> <!--new-->
</executions> <!--new-->
</plugin>
</plugins>
</build>
Summary
We now have a project setup with an aggretator (parent) and a common that allows sharing of dependencies between multiple projects that are part of the larger parent group.
We’re also able to build the entire suite of projects using maven on the command line with mvn package
and then run an individual jar from the command line with java -jar myjar.jar
.
Check out the code
It is of course all on github
I figured this out myself. So there might be an easier or nicer way to do this.. if there is please get in touch - I’d love to know it.
Got feedback on the article? Got questions or corrections… If you want to talk to me, I am really open to meeting new people and learning things. Please reach out..
Say hi on Twitter https://twitter.com/colin_riddell