Multi-module Multi-feature gradle project

Share on:

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?

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:

png

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.

png

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