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 thereservationId
, 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!!
- A
- 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 theReservation
class) – this will create a shallow copy of theReservation
object, and then we can add any missing fields as we like. For example, as theid
will not exist on the originalReservation
object, we need a way to add theid
field onto theReservation
object. Similarly, this is also performed for theduration
field, as this is something we want to determine ourselves. - Then at the end, we call
reservationBuilder. build()
which will create the newupdated 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()
orsetDuration()
method, however, you are changing the original state of theReservation
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 ๐.