Multi-module Multi-feature gradle project
Ashish Agre
Posted on June 7, 2020
Introduction
So you are starting new backend work and have planned to use spring boot because of existing java development skills in the team.
For me when i'll be in this situation i'll be like
Why would I choose spring boot?
- A LOT of boilerplate code is removed
- Have strong Spring framework support
- Good Support of DB layer more here(https://thorben-janssen.com/what-is-spring-data-jpa-and-why-should-you-use-it/)
- Handy and configurable Spring security
- Easier management of profiles
- Ready to use
starter-packs
- And not to forget spring integration
- And many more plug and play components
If you have ever developed a servlet/jsp based application then you will find there is no web.xml
in SpringBoot application.
While starting a new project it is very easy to jump start coding. As a normal spring boot application it is easy to get up and running from https://start.spring.io/ project and evolves as we develop further.
- Controllers
- Services
- Repositories
Until the features goes on increasing and the basic root package becomes collection of many sub packages and then you need drill down or search for matching file of a feature.
in.silentsudo
configs
mysql
kafka
controllers
validators
annotations
[@interface]
dto
[*.java]
[either-feature-wise packaging or individuals]
services
[either-feature-wise packaging or individuals]
reposotories
domain
[*.java]
dto
[*.java]
[either-feature-wise packaging or individuals]
Spring project by default is shipped with either maven or gradle build tool. We use this to further structure our code properly.
Instead of getting right into developing a feature we can spend some time deciding meta info about the feature.
Consider a simple use case of file uploading feature. If we are ok to share our credentials to the public client then the burden on this feature reduces. But since this is our sample usecase we are not sharing any credentials to the public client to have write access to our storage system and it is through the api server itself a user can upload a file. π‘
Let's try to define our interface before we start anything else.
Simple File Upload feature-
- Get a file
- store in one of the provider we are supporting[disk, aws or azure storage]
Implementation
Definition is as below:
Create the following project structure:
PROJECT_ROOT
build.gradle
settings.gradle
This becomes the root of the project.
All child project mentioned below reside under PROJECT_ROOT.
- Spring boot web application
- storageservice(base interface for storing files)
- awsstoreservice(concrete implementation of storageservice to fulfill storing file on aws)
- azurestoreservice(concrete implementation of storageservice to fulfill storing file on aws)
- diskstorageservuce(concrete implementation of storageservice to fulfill storing file on local or attached disk)
Create a base interface definition project module inside PROJECT_ROOT
.
This is how it should look.
PROJECT_ROOT
build.gradle
settings.gradle
storageservice
build
src
build.gradle
The only code in this module is this
package in.silentsudo.storageservice;
import java.io.File;
public interface FileStorageService {
String store(File file);
}
Similarly lets create different implementers for this
PROJECT_ROOT
build.gradle
settings.gradle
storageservice
build
src
build.gradle
diskstorageservice
build
src
build.gradle
awsstorageservice
build
src
build.gradle
azurestorageservice
build
src
build.gradle
One of the sample implementer like this
For example, if AwsStore is implemented as below:
@Service
@Qualifier("awsstore")
public class AwsStore implements FileStorageService {
// Define bunch of constant or a connection/reference to aws store to put file
@Override
public String store(File file) {
// awsSdk.store(file) similar call
return "Storing File in AWS Storage";
}
}
Finally create application module which uses these services
PROJECT_ROOT
build.gradle
settings.gradle
storageservice
build
src
build.gradle
diskstorageservice
build
src
build.gradle
awsstorageservice
build
src
build.gradle
azurestorageservice
build
src
build.gradle
application
build
src
build.gradle
At this moment we have not configured any of the dependencies for any module.
As shown in above diagram diskstorageservice, awsstorageservice, azurestorageservice are concrete implementer of storageservice which just defines the contract to store. Here implementers by their name indicates where they are storing those files.
Directory structure for this.
dependencies of application module is as follow:
implementation project(':storageservice')
implementation project(':diskstorageservice')
implementation project(':awsstore')
Usage
Let us see how we can use this different implementation of storage service
@SpringBootApplication(scanBasePackages = "in.silentsudo")
@RestController
public class MainApplication {
private final MyService myService;
private final FileStorageService fileStorageService;
private final FileStorageService awsStore;
@Autowired
public MainApplication(MyService myService,
@Qualifier("diskstorage") FileStorageService diskStorageService,
@Qualifier("awsstore") FileStorageService awsStore) {
this.myService = myService;
this.fileStorageService = diskStorageService;
this.awsStore = awsStore;
}
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
// This is post request all the time, get is for demo purpose
@GetMapping("/store")
public String store() {
// You would get file from request this is for test purpose
return fileStorageService.store(new File("/home/ashish/remove_outliers"));
}
// This is post request all the time, get is for demo purpose
@GetMapping("/awsstore")
public String awsStore() {
// You would get file from request this is for test purpose
return awsStore.store(new File("/home/ashish/remove_outliers"));
}
}
Note @Qualifier
annotation is used to locate bean with name specified because in our use case if you can see
@Autowired
public MainApplication(MyService myService,
@Qualifier("diskstorage") FileStorageService diskStorageService,
@Qualifier("awsstore") FileStorageService awsStore) {
this.myService = myService;
this.fileStorageService = diskStorageService;
this.awsStore = awsStore;
}
We are trying to wire aws/azure/disk store for reference to FileStorageService. It is as good as asking
Give me concrete implementer of `FileStorageService` named `awsstore`
Of Course no one uses different storage mechanism but this same principle can be used for example when implementing
mongodb and mysql in the same project for different use cases.
This implementation gives feature separation in large projects where dependencies are not cluttered in 1 build.gradle file instead they are in their own modules. Modules are not limited to spring-boot modules only but can be any library module.
If It is modular to write, it should be modular enough to separate it in a module.
Source Code
https://gitlab.com/silentsudo/spring-boot-multi-module-project
Reference
https://spring.io/guides/gs/multi-module/
Article source
Posted on June 7, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.