loading...

Multi-module Multi-feature gradle project

silentsudo profile image Ashish Agre ・4 min read

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.

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:

png

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.

png

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

https://silentsudo.gitlab.io/post/spring-boot/2020-05-05-multimodule-gradle-project-with-interface-fitst-design-approach/

Posted on by:

Discussion

markdown guide