Codementor Events

How to Implement an Online Table Reservation System with Microservices

Published Jun 27, 2018

In this post, Saurabh Sharma, the author of Mastering Microservices with Java 9, walks you through the implementation of an Online Table Reservation System with Microservices.

Building a microservice-based online table reservation system (OTRS) obviously entails developing separate microservices for the respective functionalities within the system.

In that sense, an OTRS can broadly be divided into three main microservices: Restaurant service, Booking service, and User service. Additional microservices and customizations can, of course, be defined in the OTRS based on individual requirements. However, this article focuses on the three aforementioned functionalities, in particular, the implementation details of the Restaurant service (the other two implementations are pretty much the same). The idea is to make the services independent, with their own separate databases.

Here’s brief overview of the services:

Restaurant service: This service provides create, read, update, and delete (CRUD) operations and criteria-based searching for the restaurant resource. It provides the association between restaurants and tables. Restaurant would also provide access to the Table entity.
User service: This service, as the name suggests, allows the end user to perform CRUD operations on User entities.
Booking service: This makes use of the Restaurant service and User service to perform CRUD operations on booking. It will leverage restaurant searching and its associated table lookup & allocation based on the table availability for a specified duration of time. It creates a relationship between the restaurant/table and the user.

fc.jpg

The above flowchart shows how each microservice works independently. This is the most significant advantage of using microservices—every microservice can be developed, enhanced, and maintained separately, without affecting the others. These services can each have their own layered architecture and database; plus, there are no restrictions in terms of using different technologies/languages to develop the respective services.

There’s no limit to the number of new microservices you can integrate at any given point in time. For instance, you can introduce an accounting service that can be exposed to restaurants for bookkeeping. Similarly, analytics and reporting are other services that can be integrated.

Developing and implementing microservices

Domain-driven implementation is the most optimal way of implementing the three microservices using Spring Cloud. Take a look at the key artifacts of the implementation:

Entities are categories of objects that are identifiable and remain the same throughout the states of the product/service. These objects are not defined by their attributes, but by their identities and threads of continuity. Entities comprise an identity, a thread of continuity, and attributes that do not define their identity.
Value objects (VOs) just have attributes and no conceptual identity. A best practice is to keep VOs as immutable objects. In the Spring Framework, entities are pure POJOs, so you can also use them as VOs.
Service objects are common in technical frameworks and are used in the domain layer in a domain-driven design. A service object does not have an internal state; its only purpose is to provide the behavior to the domain. Service objects provide behaviors that cannot be related with specific entities or VOs. They may provide one or more related behaviors to one or more entities or VOs. It is best practice to define the services explicitly in the domain model.
Repository objects are parts of the domain model that interact with storage, such as databases, external sources, and so on, to retrieve the persisted objects. When a request is received by the repository for an object reference, it returns the existing object reference. If the requested object doesn’t exist in the repository, then it retrieves the object from storage.

Each OTRS microservice API represents a RESTful web service. The OTRS API uses HTTP verbs such as GET, POST, etc., and a RESTful endpoint structure. Request and response payloads are formatted as JSON. If required, XML can also be used.

The Restaurant microservice

The Restaurant microservice will be exposed to the external world using REST endpoints for consumption. The Restaurant microservice has the following endpoints. Of course, you can add as many endpoints as you require:

  1. Endpoint for retrieving the restaurant by ID:

1.png

2. Endpoint for retrieving all the restaurants that match the value of the query parameter, Name:

2.png

  1. Endpoint for creating a new restaurant:

3.png

OTRS implementation

In this tutorial, a multi-module Maven project is created for implementing OTRS. The following stack will be used to develop the OTRS application:

• Java version 1.9
• Spring Boot 2.0.0.M1
• Spring Cloud Finchley.M2
• Maven Compiler Plugin 3.6.1 (for Java 1.9)

All the above points are mentioned in the root pom.xml, along with the following OTRS modules:

• eureka-service
• restaurant-service

• user-service
• booking-service

The root pom.xml file looks something like this:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.packtpub.mmj</groupId> 
<artifactId>6392_chapter4</artifactId> 
<version>PACKT-SNAPSHOT</version> 
<name>6392_chapter4</name> 
<description>Master Microservices with Java Ed 2, Chapter 4 - Implementing Microservices</description> 

<packaging>pom</packaging> 
<properties> 
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> 
    <java.version>1.9</java.version> 
    <maven.compiler.source>1.9</maven.compiler.source> 
    <maven.compiler.target>1.9</maven.compiler.target> 
</properties> 

<parent> 
    <groupId>org.springframework.boot</groupId> 
    <artifactId>spring-boot-starter-parent</artifactId> 
    <version>2.0.0.M1</version> 
</parent> 
<dependencyManagement> 
    <dependencies> 
        <dependency> 
            <groupId>org.springframework.cloud</groupId> 
            <artifactId>spring-cloud-dependencies</artifactId> 
            <version>Finchley.M2</version> 
            <type>pom</type> 
            <scope>import</scope> 
        </dependency> 
    </dependencies> 
</dependencyManagement> 

<modules> 
    <module>eureka-service</module> 
    <module>restaurant-service</module> 
    <module>booking-service</module> 
    <module>user-service</module> 
</modules> 

<!-- Build step is required to include the spring boot artifacts in generated jars --> 
<build> 
    <finalName>${project.artifactId}</finalName> 
    <plugins> 
        <plugin> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-maven-plugin</artifactId> 
        </plugin> 
        <plugin> 
            <groupId>org.apache.maven.plugins</groupId> 
            <artifactId>maven-compiler-plugin</artifactId> 
            <version>3.6.1</version> 
            <configuration> 
                <source>1.9</source> 
                <target>1.9</target> 
                <showDeprecation>true</showDeprecation> 
                <showWarnings>true</showWarnings> 
            </configuration> 
        </plugin> 
    </plugins> 
</build> 

<!-- Added repository additionally as Finchley.M2 was not available in central repository --> 
<repositories> 
    <repository> 
        <id>Spring Milestones</id> 
        <url>https://repo.spring.io/libs-milestone</url> 
        <snapshots> 
            <enabled>false</enabled> 
        </snapshots> 
    </repository> 
</repositories> 

<pluginRepositories> 
    <pluginRepository> 
        <id>Spring Milestones</id> 
        <url>https://repo.spring.io/libs-milestone</url> 
        <snapshots> 
            <enabled>false</enabled> 
        </snapshots> 
    </pluginRepository> 
</pluginRepositories> 

</project>

The booking and user modules can be developed on similar lines.

Controller class

The RestaurantController class uses the @RestController annotation to build the Restaurant service endpoints. The @RestController is a class-level annotation that is used for resource classes. It is a combination of the @Controller and @ResponseBody annotations and returns the domain object.

API versioning

Versioning APIs is critical because, no matter how well thought-out and comprehensive your API, it is going to change with time as your knowledge and experience grows. Replacing the API with new one instead of optimizing it may break existing client integrations and, eventually, your system. In this tutorial, the REST endpoint will be prefixed as v1, which indicates the API version.

There are various ways of managing API versions. However, the most common practice is to use the version in the path or in the HTTP header. The HTTP header can be a custom request header or an accept header to represent the calling API version.

@RestController
@RequestMapping("/v1/restaurants")
public class RestaurantController {

protected Logger logger = Logger.getLogger(RestaurantController.class.getName()); 

protected RestaurantService restaurantService; 

@Autowired 
public RestaurantController(RestaurantService restaurantService) { 
    this.restaurantService = restaurantService; 
} 

/** 
 * Fetch restaurants with the specified name. A partial case-insensitive 
 * match is supported. So <code>http://.../restaurants/rest</code> will find 
 * any restaurants with upper or lower case 'rest' in their name. 
 * 
 * @param name 
 * @return A non-null, non-empty collection of restaurants. 
 */ 
@RequestMapping(method = RequestMethod.GET) 
public ResponseEntity<Collection<Restaurant>> findByName(@RequestParam("name") String name) { 

logger.info(String.format("restaurant-service findByName() invoked:{} for {} ", restaurantService.getClass().getName(), name));
name = name.trim().toLowerCase();
Collection<Restaurant> restaurants;
try {
restaurants = restaurantService.findByName(name);
} catch (Exception ex) {
logger.log(Level.WARNING, "Exception raised findByName REST Call", ex);
return new ResponseEntity< Collection< Restaurant>>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return restaurants.size() > 0 ? new ResponseEntity< Collection< Restaurant>>(restaurants, HttpStatus.OK)
: new ResponseEntity< Collection< Restaurant>>(HttpStatus.NO_CONTENT);
}

/** 
 * Fetch restaurants with the given id. 
 * <code>http://.../v1/restaurants/{restaurant_id}</code> will return 
 * restaurant with given id. 
 * 
 * @param retaurant_id 
 * @return A non-null, non-empty collection of restaurants. 
 */ 
@RequestMapping(value = "/{restaurant_id}", method = RequestMethod.GET) 
public ResponseEntity<Entity> findById(@PathVariable("restaurant_id") String id) { 

   logger.info(String.format("restaurant-service findById() invoked:{} for {} ", restaurantService.getClass().getName(), id)); 
    id = id.trim(); 
    Entity restaurant; 
    try { 
        restaurant = restaurantService.findById(id); 
    } catch (Exception ex) { 
        logger.log(Level.SEVERE, "Exception raised findById REST Call", ex); 
        return new ResponseEntity<Entity>(HttpStatus.INTERNAL_SERVER_ERROR); 
    } 
    return restaurant != null ? new ResponseEntity<Entity>(restaurant, HttpStatus.OK) 
            : new ResponseEntity<Entity>(HttpStatus.NO_CONTENT); 
} 

/** 
 * Add restaurant with the specified information. 
 * 
 * @param Restaurant 
 * @return A non-null restaurant. 
 * @throws RestaurantNotFoundException If there are no matches at all. 
 */ 
@RequestMapping(method = RequestMethod.POST) 
public ResponseEntity<Restaurant> add(@RequestBody RestaurantVO restaurantVO) { 

    logger.info(String.format("restaurant-service add() invoked: %s for %s", restaurantService.getClass().getName(), restaurantVO.getName()); 
     
    Restaurant restaurant = new Restaurant(null, null, null); 
    BeanUtils.copyProperties(restaurantVO, restaurant); 
    try { 
        restaurantService.add(restaurant); 
    } catch (Exception ex) { 
        logger.log(Level.WARNING, "Exception raised add Restaurant REST Call "+ ex); 
        return new ResponseEntity<Restaurant>(HttpStatus.UNPROCESSABLE_ENTITY); 
    } 
    return new ResponseEntity<Restaurant>(HttpStatus.CREATED); 
} 

}

Service classes

The RestaurantController class uses the RestaurantService interface. RestaurantService is an interface that defines CRUD and some search operations:

public interface RestaurantService {

public void add(Restaurant restaurant) throws Exception; 

public void update(Restaurant restaurant) throws Exception; 

public void delete(String id) throws Exception; 

public Entity findById(String restaurantId) throws Exception; 

public Collection<Restaurant> findByName(String name) throws Exception; 

public Collection<Restaurant> findByCriteria(Map<String, ArrayList<String>> name) throws Exception; 

}

Now, we can implement the RestaurantService; use the @Service Spring annotation to define it as a service:

@Service("restaurantService")
public class RestaurantServiceImpl extends BaseService<Restaurant, String>
implements RestaurantService {

private RestaurantRepository<Restaurant, String> restaurantRepository; 

@Autowired 
public RestaurantServiceImpl(RestaurantRepository<Restaurant, String> restaurantRepository) { 
    super(restaurantRepository); 
    this.restaurantRepository = restaurantRepository; 
} 

public void add(Restaurant restaurant) throws Exception { 
    if (restaurant.getName() == null || "".equals(restaurant.getName())) { 
        throw new Exception("Restaurant name cannot be null or empty string."); 
    } 

    if (restaurantRepository.containsName(restaurant.getName())) { 
        throw new Exception(String.format("There is already a product with the name - %s", restaurant.getName())); 
    } 

    super.add(restaurant); 
} 

@Override 
public Collection<Restaurant> findByName(String name) throws Exception { 
    return restaurantRepository.findByName(name); 
} 

@Override 
public void update(Restaurant restaurant) throws Exception { 
    restaurantRepository.update(restaurant); 
} 

@Override 
public void delete(String id) throws Exception { 
    restaurantRepository.remove(id); 
} 

@Override 
public Entity findById(String restaurantId) throws Exception { 
    return restaurantRepository.get(restaurantId); 
} 

@Override 
public Collection<Restaurant> findByCriteria(Map<String, ArrayList<String>> name) throws Exception { 
    throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. 
} 

}

Repository classes

The RestaurantRepository interface defines two new methods, containsName and findByName, while also extending the Repository interface:

public interface RestaurantRepository<Restaurant, String> extends Repository<Restaurant, String> {

boolean containsName(String name) throws Exception; 

Collection<Restaurant> findByName(String name) throws Exception; 

}

The Repository interface defines three methods: add, remove, and update. It also extends the ReadOnlyRepository interface:

public interface Repository<TE, T> extends ReadOnlyRepository<TE, T> {

void add(TE entity); 

void remove(T id); 

void update(TE entity); 

}

The ReadOnlyRepository interface definition contains the get and getAll methods, which return an entity and a collection of entities, respectively. It can come in handy when you want to expose only a read-only abstraction of the repository:

public interface ReadOnlyRepository<TE, T> {

boolean contains(T id); 

Entity get(T id); 

Collection<TE> getAll(); 

}

The Spring Framework uses of the @Repository annotation to define the repository bean that implements the repository. In the case of RestaurantRepository, you can see that a map is used in place of the actual database implementation. This keeps all the entities saved in memory only. That’s why you find only two restaurants in memory when you start the service. You can use JPA for database persistence. This is the general practice for production-ready implementations:

@Repository("restaurantRepository")
public class InMemRestaurantRepository implements RestaurantRepository<Restaurant, String> {
private Map<String, Restaurant> entities;

public InMemRestaurantRepository() { 
    entities = new HashMap(); 
    Restaurant restaurant = new Restaurant("Big-O Restaurant", "1", null); 
    entities.put("1", restaurant); 
    restaurant = new Restaurant("O Restaurant", "2", null); 
    entities.put("2", restaurant); 
} 

@Override 
public boolean containsName(String name) { 
    try { 
        return this.findByName(name).size() > 0; 
    } catch (Exception ex) { 
        //Exception Handler 
    } 
    return false; 
} 

@Override 
public void add(Restaurant entity) { 
    entities.put(entity.getId(), entity); 
} 

@Override 
public void remove(String id) { 
    if (entities.containsKey(id)) { 
        entities.remove(id); 
    } 
} 

@Override 
public void update(Restaurant entity) { 
    if (entities.containsKey(entity.getId())) { 
        entities.put(entity.getId(), entity); 
    } 
} 

@Override 
public Collection<Restaurant> findByName(String name) throws Exception { 
    Collection<Restaurant> restaurants = new ArrayList<>(); 
    int noOfChars = name.length(); 
    entities.forEach((k, v) -> { 
        if (v.getName().toLowerCase().contains(name.subSequence(0, noOfChars))) { 
            restaurants.add(v); 
        } 
    }); 
    return restaurants; 
} 

@Override 
public boolean contains(String id) { 
    throw new UnsupportedOperationException("Not supported yet.");  
} 

@Override 
public Entity get(String id) { 
    return entities.get(id); 
} 

@Override 
public Collection<Restaurant> getAll() { 
    return entities.values(); 
} 

}

Entity classes

The Restaurant entity, which extends BaseEntity, is defined as follows:

public class Restaurant extends BaseEntity<String> {

private List<Table> tables = new ArrayList<>(); 

public Restaurant(String name, String id, List<Table> tables) { 
    super(id, name); 
    this.tables = tables; 
} 

public void setTables(List<Table> tables) { 
    this.tables = tables; 
} 

public List<Table> getTables() { 
    return tables; 
} 

@Override 
public String toString() { 
    return String.format("{id: %s, name: %s, address: %s, tables: %s}", this.getId(), 
                     this.getName(), this.getAddress(), this.getTables()); 
} 

}

The Table entity, which extends BaseEntity, is defined as follows:

public class Table extends BaseEntity<BigInteger> {

private int capacity; 

public Table(String name, BigInteger id, int capacity) { 
    super(id, name); 
    this.capacity = capacity; 
} 

public void setCapacity(int capacity) { 
    this.capacity = capacity; 
} 

public int getCapacity() { 
    return capacity; 
} 

@Override 
public String toString() { 
    return String.format("{id: %s, name: %s, capacity: %s}", 
                     this.getId(), this.getName(), this.getCapacity());    } 

}

The Entity abstract class is defined as follows:

public abstract class Entity<T> {

T id; 
String name; 

public T getId() { 
    return id; 
} 

public void setId(T id) { 
    this.id = id; 
} 

public String getName() { 
    return name; 
} 

public void setName(String name) { 
    this.name = name; 
} 

}

The BaseEntity abstract class is defined as follows; it extends the Entity abstract class:

public abstract class BaseEntity<T> extends Entity<T> {

private T id; 
private boolean isModified; 
private String name; 

public BaseEntity(T id, String name) { 
    this.id = id; 
    this.name = name; 
} 

public T getId() { 
    return id; 
} 

public void setId(T id) { 
    this.id = id; 
} 

public boolean isIsModified() { 
    return isModified; 
} 

public void setIsModified(boolean isModified) { 
    this.isModified = isModified; 
} 

public String getName() { 
    return name; 
} 

public void setName(String name) { 
    this.name = name; 
} 

}

You are done now with the Restaurant service implementation. It’s time to move on to developing the Eureka module (service).

The registration and discovery service (Eureka service)

You need a place for all the microservices to be registered and referenced—a service discovery and a registration application. Spring Cloud provides a state-of-the-art service registry and discovery application called Netflix Eureka, which you can use for your OTRS.

You can create a Eureka service in the following three steps (service registration and discovery service):

  1. Maven dependency: It needs a Spring Cloud dependency, as shown here, and a startup class with the @EnableEurekaApplication annotation in pom.xml:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
</dependency>

  1. Startup class: The startup class app will run the Eureka service seamlessly, using the @EnableEurekaApplication class annotation. Use <start-class>com.packtpub.mmj.eureka.service.App</start-class> under the <properties> tag in the pom.xml project:

package com.packtpub.mmj.eureka.service;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class App {

public static void main(String[] args) { 
    SpringApplication.run(App.class, args); 
} 

}

  1. Spring configuration: The Eureka service also needs the following Spring configuration for the Eureka server configuration (src/main/resources/application.yml):

server:
port: 8761 # HTTP port

eureka:
instance:
hostname: localhost
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: {vcap.services.{PREFIX:}eureka.credentials.uri:http://user:password@localhost:8761}/eureka/
server:
waitTimeInMsWhenSyncEmpty: 0
enableSelfPreservation: false

Once you have configured Eureka as described above, it will be available for all incoming requests. Eureka registers/lists all the microservices that have been configured by the Eureka client. Once you start your service, it pings the Eureka service configured in your application.yml and once a connection is established, Eureka registers the service.

It also enables the discovery of microservices through a uniform way to connect to other microservices. You don't need any IP, hostname, or port to find the service; all you need to do is just provide the service ID to it. Service IDs are configured in the application.yml of the respective microservices.

If you found this tutorial helpful, you can refer to this book, Mastering Microservices with Java 9, by Saurabh Sharma. The book is an end-to-end guide for Java developers familiar with the concepts of microservices who want to implement scalable and effective microservice architectures at an enterprise level.

Discover and read more posts from PACKT
get started