DEV Community

Marcin Piczkowski
Marcin Piczkowski

Posted on

Spring Cloud step-by-step (part 1)

This post is a first in the series about Spring Cloud - a set of Spring libraries for building "cloud native" applications. Originally published at Cloudly.Tech

What are "cloud native" applications? It means applications that have 12 factors.
In short: these are applications that are easy to deploy on available cloud providers, easy to scale up/down, monitor and release (with CI/CD).

I will show you how to start from a simple Spring Web project to create a Spring Cloud application at the end.

The example application which we're going to build is a rest API for renting goods (e.g videos).
It will return price according to a rental plan and duration.

The project is available at github with each step as a separate branch.

In this post we will create spring-cloud-1 branch.

We will first start from standard Spring web application generated with Spring Initializr at http://start.spring.io

Then we will keep adding cloud features to it, such as:

Let's generate a basic Spring web project.

Generate project

Then we will add REST controller, at the beginning it will just respond with zero price.

package com.example.cloud.pricingservice.v1;

import com.example.cloud.pricingservice.v1.dto.PriceRequest;
import com.example.cloud.pricingservice.v1.dto.PriceResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequestMapping(path = "/v1")
public class PriceController {

    @RequestMapping(path = "/prices", method = RequestMethod.POST)
    public ResponseEntity<PriceResponse> getPriceAmount(@RequestBody PriceRequest priceRequest) {

        return new ResponseEntity<>(new PriceResponse(BigDecimal.ZERO), HttpStatus.OK);
    }

}
Enter fullscreen mode Exit fullscreen mode

Start the project with

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

Now we can test it with curl

curl -X POST -H 'Content-Type:application/json' -d '{"plan":"vip", "start": "2011-12-03T10:15:30+01:00","end" : "2011-12-05T12:15:30+01:00"}' http://localhost:8080/v1/prices
Enter fullscreen mode Exit fullscreen mode

It would respond with

{"price":0}
Enter fullscreen mode Exit fullscreen mode

Let's assume we have a static configuration of daily prices per rental in yaml file, like:

plans: 
   BASIC 10
   STANDARD 20
   VIP 50

Enter fullscreen mode Exit fullscreen mode

We will replace application.properties with application.yaml as Spring supports both formats, and add the plans there:

server:
  port=8080

pricing:
  plan:
    basic: 10
    standard: 20
    vip: 50
Enter fullscreen mode Exit fullscreen mode

Personally I find this format more readable than properties, but it's a matter of taste.
You can use properties if you like so.

In order to get these properties as a map in our application we need to write some code:

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.HashMap;

@Component
@EnableConfigurationProperties
@ConfigurationProperties(prefix = "pricing")
public class PlanConfig {

    private HashMap<String, BigDecimal> plan;

    public HashMap<String, BigDecimal> getPlan() {
        return plan;
    }

    public void setPlan(HashMap<String, BigDecimal> plan) {
        this.plan = plan;
    }

}
Enter fullscreen mode Exit fullscreen mode

Now we have a spring-managed bean which we can inject to a service and calculate prices based on it.
Let's create a service class like below:

package com.example.cloud.pricingservice.services;

import com.example.cloud.pricingservice.config.PlanConfig;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.math.BigDecimal;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;

@Component
public class PriceCalculator {

    private final PlanConfig planConfig;

    public PriceCalculator(PlanConfig planConfig) {
        this.planConfig = planConfig;
    }

    /**
     * Calculates price according to plans. The minimal unit of price calculation is one day.
     * If the rent time overlaps next day the whole price for another day is calculated.
     *
     * @return calculated price
     */
    public BigDecimal calculatePrice(ZonedDateTime rentStartDate, ZonedDateTime rentEndDate, String planName) {
        Assert.notNull("Plan name is required", planName);

        long exactDaysCount = ChronoUnit.DAYS.between(rentStartDate, rentEndDate);
        long daysCount = exactDaysCount + (isOverlappingNextDay(exactDaysCount, rentStartDate, rentEndDate) ? 1 : 0);

        BigDecimal pricePerDay = planConfig.getPlan().getOrDefault(planName.toLowerCase(), BigDecimal.ZERO);
        return pricePerDay.multiply(new BigDecimal(daysCount));
    }

    private boolean isOverlappingNextDay(long exactDaysCount, ZonedDateTime rentStartDate, ZonedDateTime rentEndDate) {
        ZonedDateTime exactEndTime = rentStartDate.plus(exactDaysCount, ChronoUnit.DAYS);
        Duration overlappedDayDuration = Duration.between(exactEndTime, rentEndDate);
        return overlappedDayDuration.getSeconds() > 0;
    }

}
Enter fullscreen mode Exit fullscreen mode

Next, let's update our controller and use calculatePrice method in it.

@RestController
@RequestMapping(path = "/v1")
public class PriceController {
    private final PriceCalculator priceCalculator;

    public PriceController(PriceCalculator priceCalculator) {
        this.priceCalculator = priceCalculator;
    }

    @RequestMapping(path = "/prices", method = RequestMethod.POST)
    public ResponseEntity<PriceResponse> getPriceAmount(@RequestBody PriceRequest priceRequest) {

        return new ResponseEntity<>(
                new PriceResponse(priceCalculator.calculatePrice(priceRequest.getStart(), priceRequest.getEnd(), priceRequest.getPlan())),
                HttpStatus.OK);
    }

}
Enter fullscreen mode Exit fullscreen mode

If we now build and start the application again and execute the same curl command the result should be different depending on how many days the rental would last.
E.g.:

curl -X POST -H 'Content-Type:application/json' -d '{"plan":"basic", "start": "2011-12-03T10:15:30+01:00","end" : "2011-12-05T12:15:30+01:00"}' http://localhost:8080/v1/prices
Enter fullscreen mode Exit fullscreen mode

returns

{"price":30}
Enter fullscreen mode Exit fullscreen mode

Now we have a sample REST API implemented with Spring Web and we can work on it in next post to transform it into Spring Cloud application.

Top comments (0)