Multi-module Multi-feature gradle project
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.
- Presentation (Controllers)
- Business (Services)
- Persistence (Repositories)
- Database
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.
1in.silentsudo
2 configs
3 mysql
4 kafka
5 controllers
6 validators
7 annotations
8 [@interface]
9 dto
10 [*.java]
11 [either-feature-wise packaging or individuals]
12 services
13 [either-feature-wise packaging or individuals]
14 reposotories
15 domain
16 [*.java]
17 dto
18 [*.java]
19 [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. :rage:
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:
1PROJECT_ROOT
2 build.gradle
3 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.
1PROJECT_ROOT
2 build.gradle
3 settings.gradle
4 storageservice
5 build
6 src
7 build.gradle
The only code in this module is this
1package in.silentsudo.storageservice;
2
3import java.io.File;
4
5public interface FileStorageService {
6 String store(File file);
7}
Similarly lets create different implementers for this
1PROJECT_ROOT
2 build.gradle
3 settings.gradle
4 storageservice
5 build
6 src
7 build.gradle
8 diskstorageservice
9 build
10 src
11 build.gradle
12 awsstorageservice
13 build
14 src
15 build.gradle
16 azurestorageservice
17 build
18 src
19 build.gradle
One of the sample implementer like this
For example, if AwsStore is implemented as below:
1@Service
2@Qualifier("awsstore")
3public class AwsStore implements FileStorageService {
4 // Define bunch of constant or a connection/reference to aws store to put file
5 @Override
6 public String store(File file) {
7 // awsSdk.store(file) similar call
8 return "Storing File in AWS Storage";
9 }
10}
Finally create application module which uses these services
1PROJECT_ROOT
2 build.gradle
3 settings.gradle
4 storageservice
5 build
6 src
7 build.gradle
8 diskstorageservice
9 build
10 src
11 build.gradle
12 awsstorageservice
13 build
14 src
15 build.gradle
16 azurestorageservice
17 build
18 src
19 build.gradle
20 application
21 build
22 src
23 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:
1implementation project(':storageservice')
2implementation project(':diskstorageservice')
3implementation project(':awsstore')
Usage
Let us see how we can use this different implementation of storage service
1@SpringBootApplication(scanBasePackages = "in.silentsudo")
2@RestController
3public class MainApplication {
4
5 private final MyService myService;
6 private final FileStorageService fileStorageService;
7 private final FileStorageService awsStore;
8
9 @Autowired
10 public MainApplication(MyService myService,
11 @Qualifier("diskstorage") FileStorageService diskStorageService,
12 @Qualifier("awsstore") FileStorageService awsStore) {
13 this.myService = myService;
14 this.fileStorageService = diskStorageService;
15 this.awsStore = awsStore;
16 }
17
18 public static void main(String[] args) {
19 SpringApplication.run(MainApplication.class, args);
20 }
21
22 // This is post request all the time, get is for demo purpose
23 @GetMapping("/store")
24 public String store() {
25 // You would get file from request this is for test purpose
26 return fileStorageService.store(new File("/home/ashish/remove_outliers"));
27 }
28
29 // This is post request all the time, get is for demo purpose
30 @GetMapping("/awsstore")
31 public String awsStore() {
32 // You would get file from request this is for test purpose
33 return awsStore.store(new File("/home/ashish/remove_outliers"));
34 }
35}
Note @Qualifier
annotation is used to locate bean with name specified because in our use case if you can see
1 @Autowired
2 public MainApplication(MyService myService,
3 @Qualifier("diskstorage") FileStorageService diskStorageService,
4 @Qualifier("awsstore") FileStorageService awsStore) {
5 this.myService = myService;
6 this.fileStorageService = diskStorageService;
7 this.awsStore = awsStore;
8 }
We are trying to wire aws/azure/disk store for reference to FileStorageService. It is as good as asking
1Give 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/
comments powered by Disqus