Learn, Build, Deploy

Building a RESTful CRUD API using Spring Boot – Part 4

Table of Contents

Creating the Service Layer

For the Reseravation Management System, one of the key requirements the system needs to handle the following functionality:

  • Create a new reservation, with a unique reservationId. To create the reservationId, the business has told us that the requirement is as follows:
    • A reservationId should use the first and last characters of the customer’s first and last name, along with a 5 digit number. e.g. Joe Bloggs => 'JEBS-12345'.
    • If the reservation has less than 6 people, then set the duration to be for 1 hour, otherwise set the duration for at least 2 hours.
    • These requirements make a great candidate forโ€ฆ. the service layer!!
  • Read a reservation if the customer wants access to it
  • Update a reservation with any updated reservation details
  • Delete a reservation if the customer is no longer coming.

What is the Service Layer?

This is where the magic happens ๐Ÿ˜‰ ! Within the Service layer, all the custom business logic specific to your application will be kept here, based on your organisational requirements. You can have custom ways of handling errors or even make calls out to multiple other services. We will cover error handling in a later tutorial, don’t worry!

Let’s stick to some best practises, shall we?

When creating any layer within any project, we should always program to an interface and not to an implementation. This is because you want to tie yourself to the behaviour of a function, rather than to the implementation details of that function. Therefore, this will help to have loosely coupled layers within the RMS.

Creating the Reservation Service Interface

Firstly, Create a new package called service. Then create an interface called ReservationService which will contain all the CRUD methods that we wish to have.

import com.rms.reservationservice.model.Reservation;

public interface ReservationService {
    Reservation saveReservation(Reservation reservation);
    Reservation getReservationById(String reservationId);
    Reservation updateReservation
(String reservationId, Reservation reservation);
    void deleteReservation(String reservationId);
}

Phew, that was easy! what now??

Implementing the Reservation Service Interface

Now that we have an interface to work with, we need an implementation class that will keep our business logic, and perform operations like calling our repository layer, so that we can interact with the database easily.

Create a class called ReservationServiceImpl within the service package. This will implement all the methods from the ReservationService interface.

package com.rms.reservationservice.service;

import com.rms.reservationservice.model.Reservation;
import com.rms.reservationservice.repository.ReservationRepository;
import org.springframework.stereotype.Service;

@Service
public class ReservationServiceImpl implements ReservationService {

    private final ReservationRepository reservationRepository;

    public ReservationServiceImpl(ReservationRepository 
reservationRepository) {
        this.reservationRepository = reservationRepository;
    }

    @Override
    public Reservation saveReservation(Reservation reservation) { 
        return null; 
    }

    @Override
    public Reservation getReservationById(String reservationId) { 
        return null;
    }

    @Override
    public Reservation updateReservation(String reservationId,
 Reservation reservation) {
         return null;
     }

    @Override
    public void deleteReservation(String reservationId) { }
}

Understanding this new @Service annotation…

  • @Service – Here, we are telling Spring that this class is a Spring Component, like when we annotated the ReservationRepository with the @Repository annotation.
  • As the Repository Layer was annotated with @Repository, we know that this dependency can be injected into our service class, so we can start to make use of the repository’s functionality.
  • Though I will make post on this, this concept of injecting a dependency from one place to another is literally called DEPENDENCY INJECTION or autowiring in Spring lingo.

If you got stuck at any point during this, please checkout branch part-4-creating-service-layer and compare your code against mine.

Let’s start to break down each of the ReservationServiceImpl methods one by one, until the happy path functionality is in place.

The Save Reservation Method – Create

To save a new reservation in our database, the client needs to provide all the necessary reservation information to the service, so that we can generate an ID, attach it to the reservation, and store this in the database.

what needs to be done?

Step 1: Make the ID Generator

Create a new package called utils Then create a class called IdGenerator:

package com.rms.reservationservice.utils;

import java.util.concurrent.ThreadLocalRandom;

public class IdGenerator {

    private static final int MIN_VALUE = 10000;
    private static final int MAX_VALUE = 99999;

    public static String generateId(String firstName, String lastName) {
        long randomNum = ThreadLocalRandom
            .current()
            .nextInt(MIN_VALUE, MAX_VALUE + 1);
        return getCharacters(firstName) + 
              getCharacters(lastName) + "-" + randomNum;
    }

    private static String getCharacters(String name) {
        String[] nameSplit = name.split("");
        return nameSplit[0]
            .concat(nameSplit[nameSplit.length - 1])
            .toUpperCase();
    }
}

// Joe Bloggs => JEBS-12345

(There is a better way of implementing this, but it’s for demonstration, not production ๐Ÿ˜‰ )

Note that for this method, I have made a basic static helper method in a utility package. Even though the ID is specific to our domain, it is just a helper method to give us an ID. Therefore it does not need to be part of the model or service package, nor does it need to be a Spring Component.

Step 2: Convert the DTO into a DAO

Convert the Reservation object (DTO) passed into the method into a ReservationEntity object (DAO) and attach an ID to it.

We are going to create a helper method called from() within our ReservationEntity class. This will help converting the Reservation object to the ReservationEntity object.

import com.rms.reservationservice.model.Reservation;

//...annotations we created before
public class ReservationEntity {
//...fields that we created before e.g id, firstName etc

    public static ReservationEntity from(Reservation reservation) {
        return ReservationEntity.builder()
            .id(reservation.getId())
            .firstName(reservation.getFirstName())
            .lastName(reservation.getLastName())
            .reservationTime(reservation.getReservationTime())
            .numberOfGuests(reservation.getNumberOfGuests())
            .duration(reservation.getDuration())
            .build();
    }
}

We know that once we save the record in the database, another ReservationEntity object will be returned back. This will need to be converted back into a Reservation DTO so that we can continue to use this Reservation DTO object around our application! Let’s go and make that quickly.

Step 3: Convert the DAO back into a DTO

Convert the response back into a Reservation DTO from the ReservationEntity DAO. (remember, the entity objects ARE ONLY USED FOR DB INTERACTIONS, NOTHING ELSE). Just like how we created a helper method in ReservationEntity, we can follow a similar pattern in the Reservation class and have a converter in here.

import com.rms.reservationservice.entity.ReservationEntity;

//...annoations and imports
public class Reservation {

//... fields such as id, firstName etc. 

    public static Reservation from(ReservationEntity reservationEntity) {
            return Reservation.builder()
                    .id(reservationEntity.getId())
                    .firstName(reservationEntity.getFirstName())
                    .lastName(reservationEntity.getLastName())
                    .reservationTime(reservationEntity.getReservationTime())
                    .numberOfGuests(reservationEntity.getNumberOfGuests())
                    .duration(reservationEntity.getDuration())
                    .build();
        }
}

(You could put the converter in a different Converters class or something if you did prefer thatโ€ฆ entirely up to you! I’m not your mother ๐Ÿ˜‚ )

Step 4: Implement the Save Reservation Method

Now we can start to implement the saveReservation method. Remember the two requirements are to generate a reservation ID and set the duration of their visit based on the given number of people:

private static final int LARGE_GROUP_OF_GUESTS = 6;
private static final int ONE_HOUR = 1;
private static final int TWO_HOURS = 2;

@Override
public Reservation saveReservation(Reservation reservation) {
    Reservation.ReservationBuilder reservationBuilder =
        reservation.toBuilder();

    if (reservation.getId() == null) {
        reservationBuilder.id(generateId(reservation.getFirstName(),
 reservation.getLastName()));
    }
    if (reservation.getNumberOfGuests() < LARGE_GROUP_OF_GUESTS) {
        reservationBuilder.duration(ONE_HOUR);
    } else {
        reservationBuilder.duration(TWO_HOURS);
    }
    Reservation updatedReservation = reservationBuilder.build();

    ReservationEntity reservationEntity =
        ReservationEntity.from(updatedReservation);

    ReservationEntity savedEntity =
        reservationRepository.save(reservationEntity);
    return Reservation.from(savedEntity);
}

Woah ๐Ÿ˜ฎ ! This is the most code we have written!
Well, like I said, the Service layer is where we keep all our business logic. In this case, before saving a new record, we need to do some checks:

  • If a reservationId does not exist on the incoming reservation object, (which it will not if it’s a new reservation), then generate a new one.
  • If it’s a group booking of less than 6, then customers can only stay for 1 hour, otherwise 2 hours.
  • This is all custom logic specific to us!

Once these checks are done, we can convert our DTO’s into DAO’s and save the record in the database. Notice we never implemented the save() method ourselves, as we let Spring JPA handle that for us.
Now that the record has been saved into the database successfully, we need to convert the response back into a Reservation DTO so that we can continue to use this in our app as we want.

Right, that was a lot of information to take in! Go have a cup of โ˜•๏ธ because you sure deserve it!

If you did get stuck at any point, please checkout branch art-4-implementing-saveReservation and compare your code against mine.

Extra Details that You May Want to Know

  • I am using this toBuilder() method (from @Builder in the Reservation class) – this will create a shallow copy of the Reservation object, and then we can add any missing fields as we like. For example, as the id will not exist on the original Reservation object, we need a way to add the id field onto the Reservation object. Similarly, this is also performed for the duration field, as this is something we want to determine ourselves.
  • Then at the end, we call reservationBuilder. build() which will create the new updated reservation object which has all the unchanged and changed values in it.
  • I could have easily used setters for this, but by using a setId() or setDuration() method, however, you are changing the original state of the Reservation object, which is something we want to avoid as much as possible.
  • Maintaining immutability helps to reduce side effects in your code, making your code more thread safe, and much easier to reason about as your code becomes more deterministic. I will create a post on this later to go into this in more detail. Generally speaking, immutability == GOOD.

Implementing the Get Reservation By ID Method – Read

For the happy path – based on the reservationId provided, go to the database and find the record with that reservationId. Return back the ReservationEntity object, convert it back to the Reservation DTO object and return it.

@Override
public Reservation getReservationById(String reservationId) {
    return reservationRepository.findById(reservationId)
        .map(Reservation::from)
        .get();  
}

Fortunately, retrieval from the database is a pretty chill process. You pass in the reservationId e.g. JEBS-12345, call the reservationRepository. findById() method. This returns back an Optional <ReservationEntity> object. Since we get back an optional, we can use the Higher Order Function .map() to map each of the values from the ReservationEntity DAO to the Reservation DTO and return back the Reservation DTO object.

If you did get stuck at any point, please checkout branch part-4-implementing-getReservationById and compare your code against mine.

Okay cool, we are half way there! Only update and delete left to go!

Implementing the Update Reservation Method – Update

To update the reservation, the client needs to provide the reservationID to update, as well as the information as the changes to the reservation.
Since the CrudRepository does not have an “update” method, all we need to do is override the existing object in the database given the reservationId passed in with the new Reservation object.
For the happy path, this will be quite straightforward:

@Override
public Reservation updateReservation(String reservationId,
 Reservation reservation) {
    Reservation reservationWithId = reservation.toBuilder()
            .id(reservationId)
            .build();
    return saveReservation(reservationWithId);
}

We simply need to attach the reservationId to the new Reservation object, and then call the saveReservation() method. Since we are providing an id field into the reservationWithId, a new reservation id will not be generated when we call the saveReservation() method and it will override the existing row with all the new info.

If you did get stuck at any point, please checkout branch part-4-implementing-updateReservation and compare your code against mine.

Implementing the Delete Reservation Method – Delete

Finally, we have to implement the logic around deleting a reservation.
As with the other CRUD methods, the deleteById() method is given to us out of the box. Under the hood, it will first find the row in the database with that reservationId, and if it exists, delete the row based on the reservationId provided.

@Override
public void deleteReservation(String reservationId) {
    reservationRepository.deleteById(reservationId);
}

Since we are deleting the row in the database, when successful, the delete request will not need to return anything back to us as the data has been removed from the database.

If you did get stuck at any point, please checkout branch part-4-implementing-deleteReservation and compare your code against mine.

You made to the end of Part 4! Awesome Job!๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

We have finally come to the end of the ReservationService implementation!! That wasn’t too stressful was it!? Fortunately, since we have only been focusing on the happy path, the logic needed was not overly stressful. When we dive into the unhappy path, you will also see how easy it is to handle for various types of errors, don’t you worry ๐Ÿ™‚.

For now though, take a break, have a ๐Ÿซ and come back shortly, as we only have one section left to go for the happy path – implementing the Controller layer and seeing the RMS come to life!
Check out Part 5: Creating the Controller layer where we add in the last piece of the puzzle, and test our RMS ๐Ÿ˜Ž.