Microservices Architecture: Building Scalable Applications with Spring Boot

In today’s fast paced world of software development, scalability and fast delivery cycles is a key consideration for modern application. This is where microservice architecture comes in picture – an architecture design approach that enables applications to be more scalable, easier to maintain, and more resilient.

Microservice Architecture Overview

In 2014, James Lewis and Martin Fowler defined the microservice architectural style as follow –

“In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.” — Martin Flower

Let’s breakdown this monolithic definition into micro definitions (pun intended).

But before we do that let’s understand what is monolithic architecture.

Microservices are often seen as a counter-approach to the so called “monolithic architecture”. Monolithic architecture design where all the components and functionalities are interconnected and are usually inside the same codebase.

  • suite of small services, each running in its own process – The complete functionality of the overall application is broken down into small-small business functions or capabilities and individual mini application are developed for each business function.

  • built around business capabilities – Let’s look at Amazon as example; it has many business functionality like user account, order placement, sending notifications to user, product display and management and many more. In a monolithic architecture, all these functionalities will be developed in a single application codebase; but in microservice architecture each business functionality will be developed in its own individual application (codebase).

  • communicating with lightweight mechanisms – Usually these individual microservices exposes RESTful APIs or use messaging systems like RabbitMQ of Kafka to communicate with each other.

  • Independently deployable As microservices are loosely coupled and do not have direct dependency on each other; they can be separately deployed.

  • bare minimum of centralized management of these services Microservices are largely autonomous, capable of being developed, deployed and manged independently and there isn’t a single central entity managing/controlling these services.

  • may be written in different programming languages As microservices are independent of each other, it is not necessary that all these microservices be developed using same programming language. This way each service can be built with different language to leverage the advantages of each programming language.

  • use different data storage technologies Similar to programming language there is no bound to using same Database technology. Each service can have their own database and can use different database technologies. For example – In the case of Amazon (mentioned earlier), it would be good to use structured DB like Oracle/Postgress for user management service. But at the same time Amazon sells so many products each having different attributes, i.e, it’s an unstructured data and thus it would be good to use NoSQL DB like MongoDB.

Monolithic Architecture

Monolithic Architecture Diagram

Microservice Architecture

Microservice Architecture Diagram

Microservice using Spring Boot

Why choose Spring Boot for Microservices?

Spring boot is a powerful framework that simplifies the development of enterprise java applications due to its ready to use production grade feature and tools that makes it a good choice for microservice development.

Rapid Development – Spring boot provides features like autoconfiguration, spring boot starters etc which allow developers to quickly develop application without having to write much boilerplate code.

Lightweight – Spring boot is lightweight and easy to use and makes its excellent choice for building microservice that needs to fast and lightweight.

Modular – Spring boot is by design a modular framework, making it easier to add new features and functionality as needed.

Cloud-native – Spring boot provides many features that makes it easier to develop cloud-native applications. Thus, making it easier for easy deployment to cloud platforms like AWS, Azure and/or GCP.

Extensive community – Spring has a large and active community of developers who are actively contributing to the framework and supporting others.

Implementing Microservice with Spring Boot

Let’s see a simple example of Food ordering webapp. To keep it simple we will create three microservices -Order Management Service, Restaurant Management Service and User Management Service. Along with the three microservices; we will also create an API gateway.

API Gateway - API Gateway is a server-side component which sits between the client and backend microservices and acts as a single-entry point.

Consul Discovery – It is a discovery and configuration management tool. We will use consul discovery to dynamically discover our microservices and not to worry about the IP address and port of each microservice.

Implementing API Gateway

@SpringBootApplication
@EnableDiscoveryClient
public class YummyCallsGatewayApp {

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

    @Bean
    public RouteLocator routeLocator(RouteLocatorBuilder builder) {
        return builder.routes()
                .route("order-route", r -> r.path("/order/**")
                        .filters(f -> f.rewritePath("/order/(?<segment>.*)", "/${segment}"))
                        .uri("lb://yummy-calls-order"))
                .route("restaurant-route", r -> r.path("/restaurant/**")
                        .filters(f -> f.rewritePath("/restaurant/(?<segment>.*)", "/${segment}"))
                        .uri("lb://yummy-calls-restaurant"))
                .route("user-route", r -> r.path("/user/**")
                        .filters(f -> f.rewritePath("/user/(?<segment>.*)", "/${segment}"))
                        .uri("lb://yummy-calls-user"))
                .build();
    }
}

@EnableDiscoveryClient – It enables discovery client implementation. It tells microservice to automatically register itself with the discovery server when it starts. All microservices need to have discovery server properties in application.yml file.

spring:
  application:
    name: yummy-calls-gateway

  cloud:
    consul:
      host: ${CONSUL_SERVER_HOST:localhost}
      port: ${CONSUL_SERVER_PORT:8500}
      discovery:
        enabled:  true
        lower-case-service-id: true

server:
  port: 8085
  • route("order-route", ...): Defines a route with the ID "order-route".

  • r.path("/order/**"): Matches any request path that starts with /order/.

  • filters(f - > f.rewritePath("/order/(?<segment>.*)", "/${segment}")): Applies a filter to rewrite the path by removing the /order prefix and pass the remaining URI path value to microservice.

  • uri("lb://yummy-calls-order"): Forwards the request to the service registered with the name yummy-calls-order in the consul. Point to notice here is we don’t need to know at which IP address or port the service is running on.

Implementing Order Service

application.yml

spring:
  application:
    name: yummy-calls-order
  cloud:
    consul:
      port: 8500
      discovery:
        prefer-ip-address: true
        instanceId: ${spring.application.name}-${random.int[1,99]}
      host: ${CONSUL_DISCOVERY_HOST:localhost}
      config:
        enabled: false
server:
  port: 0
@RestController
@RequestMapping("/api")
public class OrderController {

    @GetMapping("/get-order-list/{userId}")
    public ResponseEntity<?> getOrderList(@PathVariable String userId) {

        String resp = "";

        if ("1".equals(userId)) {
            resp = "{\n" +
                    "    \"userId\": 1,\n" +
                    "    \"orderId\": 123123,\n" +
                    "    \"foodName\": \"Samosa\"\n" +
                    "}";
        } else if ("2".equals(userId)) {
            resp = "{\n" +
                    "    \"userId\": 2,\n" +
                    "    \"orderId\": 233123,\n" +
                    "    \"foodName\": \"Noodles\"\n" +
                    "}";
        }

        return new ResponseEntity<>(resp, HttpStatus.OK);
    }

    @PostMapping("/cancel-order/{userId}")
    public ResponseEntity<?>  cancelOrder(@PathVariable String userId) {
        return new ResponseEntity<>("Your order: " + new Random().nextInt() + " is cancelled.", HttpStatus.OK);
    }
}

Implementing User Service

@RequestMapping("/api")
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/user-profile/{userId}")
    public ResponseEntity<?> getProfileDetails(@PathVariable String userId) {
        return new ResponseEntity<>("{\n" +
                "    \"userId\": 1,\n" +
                "    \"name\": \"Johnnathan\",\n" +
                "    \"age\": 30,\n" +
                "    \"email\": \"johnnathan@mail.com\",\n" +
                "    \"mbNo\": 9876543210\n" +
                "}", HttpStatus.OK);
    }

    @GetMapping("/user-order-history/{userId}")
    public ResponseEntity<?> getUserOrderHistory(@PathVariable String userId) {
        String resp = restTemplate.getForObject("http://localhost:8085/order/api/get-order-list/1", String.class);
        return new ResponseEntity<>(resp, HttpStatus.OK);
    }
}

Implementing Restaurant Service

@RestController
@RequestMapping("/api")
public class RestaurantController {

    @GetMapping("/get-restaurant-list")
    public ResponseEntity<?> getRestaurantList() {
        return new ResponseEntity<>("{\"restaurants\": [{\"restaurantId\": 1, \"restaurantName:\": \"Restaurant 1\" }, {\"restaurantId\": 2, \"restaurantName:\": \"Restaurant 2\" }, {\"restaurantId\": 3, \"restaurantName:\": \"Restaurant 3\" }, {\"restaurantId\": 4, \"restaurantName:\": \"Restaurant 4\" }, {\"restaurantId\": 5, \"restaurantName:\": \"Restaurant 5\" }]}", HttpStatus.OK);
    }

    @PostMapping("/register-new-restaurant")
    public ResponseEntity<?> registerNewRestaurant(@RequestBody Restaurant restaurant) {
        return new ResponseEntity<>("Your restaurant is successfully registered!", HttpStatus.OK);
    }
}

Note: All the services needs to be annotated with @EnableDiscoveryClient and should have Consul server properties in application.yml

Testing

First we need to start the Consul server so our microservices can register themselves. In this example will run the consul server in docker container.

docker run --name='consul-server' -p 8500:8500 hashicorp/consul

Next, start all the microservices one by one. We can check the status of the microservices on Consul at http://localhost:8500/ui/dc1/services.

Sending request to user microservice

Fetching order history of user

The user microservice API is called which in turn calls the Order microservice API to get order history.

Sending request to restaurant microservice

Summary

  • Microservice are suite of small services, each running in its own process and are built around business capabilities.

  • They communicate with each other using lightweight mechanisms like REST APIs or messaging systems.

  • They are independently deployable.

  • Different programming languages can be used to write microservices and also they can use different storage mechanism for data storage.

In the coming articles in the series we will deep dive into the development and implementation of more advance features.

The example code shown here can be found @ Github

0
Subscribe to my newsletter

Read articles from Shailendra Singh directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Shailendra Singh
Shailendra Singh

👨‍💻 Shailendra - Software Engineer 👨‍💻 Hello there! I'm Shailendra, a software engineer with five years of invaluable experience in the tech industry. My coding toolkit primarily consists of Java and Spring Boot, where I've honed my skills to craft efficient and scalable software solutions. As a secondary passion, I've dived into the world of React.js to create dynamic and interactive web applications.