Best Practices in Spring Boot Project Structure

Updated: Aug 9



In this blog, we will learn about different layers in Microservice and how we can leverage these layers to create a clean spring boot project structure.


Clean Project Structure plays a crucial role in taking the first step toward Production-Grade and Enterprise Ready Microservice. The project structure is the base of every Microservice, so it has to be strong.


You can check out the complete project on my GitHub. It will contain all the necessary classes and code.


Let's define an example/ use-case that we will be using in this blog.


Use Case

We will use Student Management System to understand the steps involved in creating Microservice. Our Student Management System will implement only one function to allow college/ school to make Student Records. We need first name, last name, date of birth, subject name, section, and teacher name during student onboarding.


The Three Layers

The three layers of Microservice


Controller Layer:

  • It will contain Rest APIs definition and request body.

  • Only API calls should invoke the Controller Layer.


Service Layer:

  • It will only take the data from the controller layer and transfer it to the repository layer.

  • It will also contain business logic and model the data for the repository layer.

  • It will also take the data from the repository layer and send it back to Controller Layer.

  • Only the Controller layer should invoke the Service Layer.


Repository Layer:

  • It will interact with the underlying database.

  • The service Layer should only invoke the repository layer.


Few Important Terminologies

There are a few terminologies and concepts you should familiarize yourself, before going to the next section.


DTO

  • DTOs (Data Transfer objects) are the objects or classes used to transfer data between layers through the service layer.

  • The service layer only accepts data in the form of DTOs.

  • Any data returned to the controller layer will be in the form of DTOs.


Model

Models are the object used by the repository layer to call the Database stored procedure or perform CRUD operations without using Stored Procedure.


Mapper

Mappers are used to converting the form of data when transferred between layers. There are two types of Mappers:

  • Model Mapper: This will map any data to the Model.

  • DTO Mapper: This will map any data to DTOs.


In the next section, we will create the various packages using the concept we learned until now.


Creating Packages

  • We need to have package under src > main > java > <your group id> for all three layers defined above.

  • Controller

  • Service

  • Repository

  • We need to have two packages under src > main > java > <your group id> > controller for defining request body and API.

  • Request

  • API

  • We need to have package for DTO (Data Transfer Object) under src > main > java > <your group id> and for corresponding mappers under src > main > java > <your group id> > dto.

  • DTO

  • Mapper (It will be under DTO)

  • We need to have package for Model under src > main > java > <your group id> and corresponding Mapper under src > main > java > <your group id> > model.

  • Model

  • Mapper


Now, we can use these layers to create a project structure. At the high level, we need three packages under src > main > java > <your group id>. In my case, group id is com.codewithnk.microservice_demo



Different Packages in Microservice


Having these packages will be our first step toward a clear separation of work boundaries and cleaner code. Each package has its own defined functionality, and there is no overlap between any package.


UPDATE: URI Versioning is replaced with Custom MIME Type.
There are different ways to version the API; we can add API Versions in URI, Headers, Request Params, or Custom MIME type. However, each method has its own pros and cons. It will be scenario-based. Though we will be able to reduce, not avoid, code duplication. Any help or suggestions will be highly appreciated to prevent code duplication. If anyone has any different opinions, please feel free to comment.

Defining Model for Request Body

REST endpoints (API) can perform different actions. These actions are called HTTP verbs. Below are the most commonly used HTTP verbs.

  • POST: Used to save the record in the Database.

  • PUT: Used to update the existing records in the Database.

  • GET: Used to fetch the data from Database.

  • DELETE: Used to delete a record from Database.

Out of these four verbs, POST and PUT verbs require Request Body. The request body is a structure of data used to exchange information between the frontend and backend.

Use the below steps to create the Request body. Along with the Request Body, we also code a corresponding DTO and Model.


Steps for creating Request Body, DTO, and Model

We will create Request Body under src > main > java > <your group id> > controller > request


Step1: Create JSON

Creating JSON is like an agreement between the frontend and backend teams that will allow them to develop corresponding classes in their code.


{
  "student": {
  "fname": "",
  "lname": "",
  "dob": ""
 },
 "class": {
  "name": "",
  "section": "",
  "teacher": ""
  }
 }


Step 2: Creating Class


Request Body will use the StudentRequest and SubjectRequest Classes.

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class StudentRequest {
 private String fname;
 private String lname;
 private String dob;
}

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class SubjectRequest {
  private String name;
  private String section;
  private String teacher;
}

The below class is the Request Body for POST requests in which we are adding students to a system.


@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class AppRequest {
  private StudentRequest student;
  private SubjectRequest subject;
}

Now let’s create the corresponding DTO and Model.

DTO will be created under src > main > java > <your group id> > dto

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class StudentModelDTO {
  private String fname;
  private String lname;
  private String dob;
}

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class SubjectModelDTO {
  private String name;
  private String section;
  private String teacher;
}
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class AppModelDTO {
  private StudentModelDTO student;
  private SubjectModelDTO subject;
}

Model will be created under src > main > java > <your group id> > model


@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class StudentModel {
  private String fname;
  private String lname;
  private String dob;
}

@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class SubjectModel {
  private String name;
  private String section;
  private String teacher;
}
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
public class AppModel {
  private StudentModel student;
  private SubjectModel subject;
}

Request, DTO, and Model all three look like same, but we still created three separate Request, DTO, and Model classes.

  • It allows easy minor changes in the Request Body without breaking the Application. We can only update Request Body and Mapper that converts Request Body into DTO without affecting any other classes.

  • The model class is usually different than Request and DTO; it just happened to be the same in this example. The model class represents the database schema for the entity we are interacting with.


Define API, Service Class, Repository Class, and Mappers

We have seen the definition of each class at the beginning of the blog.

API:

  • It will be created under src > main > java > <your group id> > controller > api

  • We have to annotate the class with @RestController to help Spring Container identify the API.


@RestController
public class AppController {

 @Autowired
 private AppService service;

 @PostMapping(name = "CreateStudentRecord", value = "/student-record")
 public ResponseEntity createStudentRecord(AppRequest request)     {
    boolean recordCreated = service.createStudentRecord(new 
    AppModelDTOMapper().mapToModel(request));
    if (recordCreated) return new ResponseEntity(HttpStatus.CREATED);
    return new ResponseEntity(HttpStatus.BAD_REQUEST);
 }
}

Service Class:

  • It will be created under src > main > java > <your group id> > service

  • We have to annotate the class with @Service to help Spring Container identify the Service Class.

@Service
public class AppService {

  @Autowired
  private AppRepository repository;

  public boolean createStudentRecord(AppModelDTO dto) {
   return repository.createStudentRecord(new 
                          AppModelMapper().mapToModel(dto));
  }
 }

Repository Class:

  • It will be created under src > main > java > <your group id> > repository

  • We have to annotate the class with @Repository to help Spring Container identify the Repository class.

@Repository
public class AppRepository {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    private SimpleJdbcCall simpleJdbcCall;

    public boolean createStudentRecord(AppModel model) {
        simpleJdbcCall = new SimpleJdbcCall(jdbcTemplate).withProcedureName("create_student_record");
        SqlParameterSource in = new MapSqlParameterSource()
                .addValue("student_fname", model.getStudent().getFname())
                .addValue("student_lname", model.getStudent().getLname())
                .addValue("student_dob", model.getStudent().getDob())
                .addValue("subject_name", model.getSubject().getName())
                .addValue("subject_section", model.getSubject().getSection())
                .addValue("subject_teacher", model.getSubject().getTeacher());
        simpleJdbcCall.execute(in);

        return true;
    }
}


DTO Mapper:

  • It will be created under src > main > java > <your group id> > dto > mapper

  • It will map the Request Body to DTO.


public class AppModelDTOMapper {

    public AppModelDTO mapToModel(AppRequest request) {
        return new AppModelDTO()
                .setStudent(
                        new StudentModelDTO()
                                .setLname(request.getStudent().getLname())
                                .setFname(request.getStudent().getFname())
                                .setDob(request.getStudent().getDob())
                )
                .setSubject(
                        new SubjectModelDTO()
                                .setName(request.getSubject().getName())
                                .setSection(request.getSubject().getSection())
                                .setTeacher(request.getSubject().getTeacher())
                );
    }
}