A SAGA microservice application - 5

If you have followed along this series of posts on microservices application, you must have a ready to consume web service infrastructure now. We have designed and coded the small services with JWT security. Registered these services to Eureka and load balanced among them. And we have a gateway to call our services as a doorway. Now we need to implement the MVC coordinator app. We will implement consistenciy checks regarding transaction management and business reuirements with this web app.

This MVC web app is a consumer and could have been a different technology like a mobile application. Our system is ready to use via the gateway. This proves that we have achieved technological indepdencence with this architecture. Our services and clients are communicating over http requests and this brings them to a common ground. The only non-flexible sturcture in this system is the backbone, which consists of Gateway, Eureka and Config Server. I think there would be different solutions for each of these 3 components.

I have to mention something here, but i will cover it largely later. When you research microservices, you often come across with terms like message queue or event sourcing. These are concepts with their own tools and solutions and they are usually used for managing the business requirements among the small services. We have taken this responsibility from the small services and designed them with single reponsibility. MVC web app will take care of these flows.

We have made this desicion by dividing the services into small methods. We haven't implemented business logic inside these services. Are there any alternatives? Of course there are. But if you try to develop "a microservice with everything", you end up sending a cargo plane to your neighbour, instead of just knocking on their door. We will think about the different possible solutions under different circumstances in the last post. You can find all the code on github. Let's put the architecture here.

MVC app

It would be easiest to develop write the MVC client with spring boot since all the services are developed with spring cloud. I won't write all the code of this MVC web app here. Our focus is not developing an MVC project. We will imlement an infrastructure that can provide some level of consistency and it will turn into a ready to develop app skeleton. This way, we will have a working project instead of just saying "there are these tools and solutions for these problems". Let's create a spring boot project with the dependencies below. If you don't have much spring MVC experience, you can get some help from this post.

We have added Spring Web dependency to leverage MVC structure. This library provides us controllers, sessions, requests, response, model and view classes. Thymeleaf dependency is helping us connect the MVC structure to html interfaces. Lombok shortens our code by providing getter and setter methods of the model objects in the background. DevTools will automatacally build and deploy the web app to the embedded tomcat without manually restarting. You need to check the "spring-cloud.version" in the pom file. It must be some 2020... version or later.

There is one more dependency to add. It is the util project. I have created the ab-util project to store the data coming from the services in some POJO classes and to define the constant values. You might have cloned this project if you have followed this series. You can add this dependency in the pom file.

			
<dependency>
	<groupId>com.aldimbilet</groupId>
	<artifactId>util</artifactId>
	<version>0.0.1-SNAPSHOT</version>
</dependency>
			
		

Note that we don't have JPA or Security in this project. We have handled the secuirty inside small services by using JWT tokens. We will be using userservice to retrieve a JWT token. Then we will send JWT headers to other services. We could use the same JWT infrastructure to secure this web app or just utilize spring security easily.

Now let's dig into OpenFeign dependency. This dependency helps us simplify the usage of the restful endpoints. You have to open an http connection, create a get or post request, read the incoming stream as response and map it to the relevant object in order to call a restful service. Instead of writing this code over and over, we use OpenFeign. It used to be just Feign but it is acquired by spring cloud and renamed to openfeign. Careful not to use the wrong dependency.

We will be able to call the restful services like "userservice.login()" in java code with the help of this library. BUT !! There is no less code, there is invisible code. When something happens during these calls, feign will throw them as exceptions and crashes the MVC app. That is why you will see try-catch blocks in the code. We will continue analyzing feing in feign client codes.

Let's begin the codes with application.properties file. This app doesn't have concerns like registering to Eureka or fetching properties from config server. This is the consumer. That is why we didn't need bootstrap either. It will be enough to add these dependencies here.

			
server.port=80
spring.application.name=ab-webservice

gateway.adress.userservice=http://localhost:4441/user/
gateway.adress.activityservice=http://localhost:4441/act/
gateway.adress.paymentservice=http://localhost:4441/pay/
			
		

If you don't specify a port here, it will automatically launch at 8080. application.name was not necessary but i wanted to write it so that i don't confuse property files. Because there are a lot of them. But what on earth are the next properties? Well, spring boot lets you create your own custom undefined properties. Normally spring boot doesn't have a "gateway.adress" property. We added these to be able to know where the gateway (which is placed on 4441 port) routes are located. Remember the routelocators defined with /user, /act and /pay paths in gateway for routing to the services. These definitions were necessary because of fegin. I will explain this later.

Feign configuration and feign clients

We need to use @EnableFeignClients annotation in the main class to be able to create feign client beans. The project will create the concrete classes of the feign clients while starting. It creates a Resttemplate and get or post codes. This annotation is crucial. If you forget it spring boot won't create beans and you will get "Consider defining a bean of type '...UserClient' in your configuration." kind of error when you try to inject them.

			
@SpringBootApplication
@EnableFeignClients
public class AldimBiletMvcWebClientApplication
{
	public static void main(String[] args)
	{
		SpringApplication.run(AldimBiletMvcWebClientApplication.class, args);
	}
}
			
		

Then i will create @FeignClient interfaces. Because i know the services, parameters, path and return type of the services from the previous post. Then i can easily write the clients of these service. Let me write the userservice client as an example.

			
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import com.aldimbilet.pojos.UserRegisterPojo;

@FeignClient(url = "${gateway.adress.userservice}", name = "UserClient")
// We need to assign a URL here, otherwise Feign will try to load balance the request itself
// We used Ribbon inside Eureka (lb:// links) to load balance the requests
// If you don't specify the URL, you will get "did you forget load balancer?" kind of error
// If you don't add @FeignClient annotation, bean won't be created and you will get "consider defining bean" error
// gateway.adress property was defined in application.proeprties file
// The methods are filled in in the background by Feign
public interface UserClient
{
	// Just like java method calls, we create a method with the same return type, parameters and name
	// path = localhost:4441/user/hello
	@GetMapping(path = "hello")
	ResponseEntity<String> sayHello(@RequestHeader(value = Constants.HEADER_STRING) String token);

	// It is enough to send a username and password string in json format to login with Spring Security
	// Spring security uses this as default input and returns ResponseEntity<String>
	// Jackson library is handling the mappings in the background
	// path = localhost:4441/user/login
	@PostMapping(path = "login", consumes = MediaType.APPLICATION_JSON_VALUE)
	ResponseEntity<String> login(@RequestBody String user);

	// We can't send ABUser class for registeration, that class belongs to userservice
	// That is why we have a intermadiart pojo (UserRegisterPojo) that carries the data and mapped by jackson
	// @RequestBody parameter is mandatory in post methods, as in "what are you posting?"
	// path = localhost:4441/user/register
	@PostMapping(path = "register", consumes = MediaType.APPLICATION_JSON_VALUE)
	ResponseEntity<String> register(@RequestBody UserRegisterPojo userInfo);

	// If you don't specify consumes information here, feign will try to convert it to a data type by default and could throw errors
	// path = localhost:4441/user/getUserInfo
	// userservice getUserInfo method doesn't have a token parameter but @RequestHeader is transfered in the header and doesn't cause error
	@GetMapping(path = "getUserInfo", consumes = MediaType.TEXT_PLAIN_VALUE)
	ResponseEntity<UserInfoPojo> getUserInfo(@RequestHeader(value = Constants.HEADER_STRING) String token, @RequestParam String username);

	@GetMapping(path = "getUserCard", consumes = MediaType.APPLICATION_JSON_VALUE)
	ResponseEntity<CardInfoPojo> getUserCard(@RequestHeader(value = Constants.HEADER_STRING) String token, @RequestParam Long userId);
}
			
		

Now we have created the exact corresponding methods of the userservice methods here. There is one more feature i want to tell you about Feign. When you are defining @FeignClient, you can also set a custom config class with (configuration = FeignConfig.class). You can write error handling codes in that class with ErrorDecoder. Or you can interfere with the request with RequestInterceptor. Your feign config class will get more sophisticated with this but it will also have to deal with business rules as a side effet.

You can also create feign clients with Feign.builder() methods instead of injecting them. This manual creation doesn't have default encoder - decoder, interceptor, target features that spring framework handles by default. You need to create them too. These are irreleveant points now but you may need to know this if you try to write customized clients and face with these problems.

Controller and SAGA

It is finally the time to develop the MVC app interfaces and the controller methods. I won't write the codes of Thymeleaf. You can find them on github. The flow of the interface is like this:

  • User logs in
  • Index page shows the situation of the services
  • User enters the events page
  • Selects an event and puts it in the basket
  • Next page is the payment information and pay button ends the process with payment

The system first receives the payment. Then it checks for available seats since there is no logic of reserving a seat. This is implemented this way to prevent bot users locking seats. Our purpose is not to come up with a logical business solution. We want to simulate a fault tolerance and a SAGA for business requirements. We are managing business rules here because we didn't load the small services with business flows. We will be maintaining consistency with these checks. This is what we mean by a SAGA. It is a story with a beginning and ending with fault tolerance.

We will also have exception handling for programmatic erorrs or service outages. Because feign client classes will throw exception when a response returned by a status code different than 200. I have mentioned a configuration class for feign clients. You can also create @RestControllerAdvice class and define @ExceptionHandler beans to gather all the exception together. But we are going to write our own try-catch block and handle exceptions. Let me explain the most important methods in the controller class.

			
import java.util.List;
import java.util.Random;
import javax.servlet.http.HttpServletRequest;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.servlet.ModelAndView;
import com.aldimbilet.pojos.ActivityPojo;
import com.aldimbilet.pojos.BasketPojo;
import com.aldimbilet.pojos.CardInfoPojo;
import com.aldimbilet.pojos.UserInfoPojo;
import com.aldimbilet.pojos.UserRegisterPojo;
import com.aldimbilet.util.Constants;
import com.aldimbilet.website.feign.ActivityClient;
import com.aldimbilet.website.feign.PaymentClient;
import com.aldimbilet.website.feign.UserClient;
import com.aldimbilet.website.util.SessionConstants;
import feign.FeignException;
import lombok.AllArgsConstructor;

@Controller
@AllArgsConstructor
public class MikroServiceClientController
{
	private UserClient userClient;

	private ActivityClient activityClient;

	private PaymentClient paymentClient;

	@PostMapping(path = "login")
	public ModelAndView login(HttpServletRequest req)
	{
		String username = req.getParameter("username");
		String password = req.getParameter("password");
		// Spring Security expects a User information in Json format
		// If the username and password is correct, we receive a JWT token as response body
		// You can also create a json format or a POJO and let the Jackson handle the conversion
		String user = "{\"username\":\"" + username + "\",\"password\":\"" + password + "\"}";
		String token = "";
		try
		{
			ResponseEntity responseEntity = userClient.login(user);
			token = responseEntity.getBody();
			String bearer = token.substring(token.indexOf(" ") + 1);
			user = token.substring(token.indexOf("(") + 1, token.indexOf(")"));
			req.getSession().setAttribute(SessionConstants.BEARER, bearer);
			req.getSession().setAttribute(SessionConstants.USERNAME, user);
			return new ModelAndView("redirect:/index");
		}
		catch (FeignException e)
		{
			// At this point, the service may not be available
			if (e.status() == HttpStatus.SERVICE_UNAVAILABLE.value())
			{
				return new ModelAndView("redirect:/login?err=2");
			}
			else if (e.status() == HttpStatus.UNAUTHORIZED.value())
			{
				// Or the username and password may not match
				// Userserice returns UNAUTHORIZED inside unsuccessfulAuthentication
				return new ModelAndView("redirect:/login?err=3");
			}
			else
			{
				return new ModelAndView("redirect:/login");
			}
		}
	}

	@PostMapping(path = "signup")
	public ModelAndView signup(@ModelAttribute(name = "userregisterpojo") UserRegisterPojo pojo)
	{
		try
		{
			ResponseEntity responseEntity = userClient.register(pojo);
			// the response is determined by the userservice
			// it is always ideal to use a documentation tool like SWAGGER 
			return new ModelAndView("redirect:/login");
		}
		catch (FeignException e)
		{
			// We use feign and it considers all the exceptions as feign exception
			// If the service is unavailable, you can change the flow too
			if (e.status() == HttpStatus.SERVICE_UNAVAILABLE.value())
			{
				// 503
				return new ModelAndView("redirect:/signup?err=2");
			}
			else
			{
				// General errors
				return new ModelAndView("redirect:/signup?err=1");
			}
		}
	}

	@GetMapping(path = "payment")
	public ModelAndView payment(HttpServletRequest req)
	{
		ModelAndView payment = new ModelAndView("payment");
		BasketPojo basket = (BasketPojo) req.getSession().getAttribute(SessionConstants.BASKET);
		String bearer = (String) req.getSession().getAttribute(SessionConstants.BEARER);
		String cardInfo = req.getSession().getAttribute(SessionConstants.CARD_NUMBER).toString();
		Boolean sold = paymentClient.makePayment(Constants.TOKEN_PREFIX + bearer, cardInfo).getBody();
		// makePayment randomly returns true or false, simulating credit card denials or various issues
		if (sold)
		{
			Boolean resp = activityClient.checkActivitySeatAvailable(Constants.TOKEN_PREFIX + bearer, basket.getActId()).getBody();
			if (!resp)
			{
				payment.addObject("status", "Event is full, sorry");
				// Returning payment is another aspect of fault tolerance
				paymentClient.returnPayment(Constants.TOKEN_PREFIX + bearer, cardInfo);
			}
			else
			{
				activityClient.sellSeat(Constants.TOKEN_PREFIX + bearer, basket.getActId());
				payment.addObject("status", "Payment done, you bought the ticket");
			}
		}
		else
		{
			payment.addObject("status", "Payment error, sorry");
		}
		return payment;
	}
}
			
		

We have implemented basic exception handling in login, signup ve payment methods. All the methods have 503 unavailable handling which comes from the failover services. This is resilliance. In case of username password mismatch, we caught UNAUTHORIZED status. We have returned the payment in case of all the seats are sold, since we don't have a reservation logic. This was a business SAGA and coordinated by MVC app, providing consistency. If we were to take a step further and develop a temporary rezervation table, we would have to delete that right after the payment. We wanted the most basic consistency and reliability. We have implemented 3 kinds of fault tolerance, services being unavailable, throwing exceptions or business rules being violated. I see them as 3 different kinds of SAGAs. We will go deeper on this subject in the last post :)

At this point i should also mention my own mistakes here. When i first tried to use Feign, i couldn't set a dynamic JWT header. Because i have used the wrong annotation. So i have created a config class and the Feign.build(). Then i had to write interceptor, encoder, decoder for the requests manually. This caused me to write an encoder and decoder for all the data types in request or response. Because it didn't leverage Jackson mappings automatically. On top of this, it defaulted to a certain consume information when i tried to post some data with requestbody. It tried to turn a Long value into Json or something. This all taught me how Feign library is working in the background :) In the end, i was able to add a dynamic JWT header and match the consumes information. Jackson is able map the return types now. I can use the FeignClient interface without writing any code.

Let me give you some information about Thymeleaf. While the modelandview object is returned by spring framework and being created, thymeleaf kicks in. For example, when you say "return new ModelAndView("redirect:/login");" thymeleaf looks for a login.html file inside the template folder. The default file type is html. It can access the data that spring framework produces. You can write if conditions with variables, session or requestparam inside the thymeleaf html files. This knowledge would help you if you clone the app and customize it..

Enough coding, let's run it

Now we have seen the basic functions of the MVC app. Even though the main purpose of our microservice journey is not the MVC app, we have coordinated SAGAs with certain exception handling and created a working project. Of course there are different solutions to implement regarding these subjects and this is one of the reasons you would get confused while learning microservices. The last post of the series will ponder on what could be done after this point, what did we gain or sacrificed and look for alternative technologies with some philosophy.

After all this coding, we won't just keep it in the attic. You can run the whole system by Eureka -> Config Server -> Gateway -> Small services -> MVC order. I will write the details of this process and the easy way to run it in the next post. If you are new to microservices, it would teach you a lot to run this system, produce your own ideas and face with different errors and situations by yourself. See you at the next post :)


Leave a comment