Microservices: The Architecture of Agility 💿

Introduction 🖥

In today's rapidly evolving tech landscape, the way we develop and deliver applications has undergone a remarkable transformation. This journey through time has seen monumental shifts in methodologies, architectures, and infrastructure. Softwares come in a myriad of varieties ranging from a mere $3000 to multi million dollars projects. In this article let's look at what software architecture remains the pioneering hotline behind the entire Modern Tech Industry.

Evolution of Methodologies : A Historical Outlook Waterfall: The Rigid Beginning 🌊

Application development used to be done using a strict waterfall methodology. Sequential stages resulted in long development cycles and restricted flexibility to modify, from requirements to testing.

Agile: Embracing Flexibility

The Waterfall approach's flaws gave rise to the agile methodology. It revolutionized how teams approach projects by placing a premium on cooperation, flexibility, and iterative development.

The Monolithical Mess 🔥

Monolithic architecture follows a consolidated pattern where everything is cramped into a single name space (or single server ) . However, this proved to be an head-ache for maintenance and development of large code base applications. It's not bad but there are some major problems associated with this primordial architecture paradigm.

● Coordination between teams became difficult when applications became large and complex.

● You cannot scale a specific service, instead, you would need to scale the entire application, which meant higher infrastructure costs.

● The release process takes longer, because for changes in any part of the application in any feature, you need to test and build the whole application to deploy those changes.

● A bug in any module can bring down the entire application.

The Microservices Monogatari ⚡

The solution was simple yet revolutionary “MICROSERVICES” . The term itself is very self explanatory. The entire application is logically distributed into several small (“micro”) components that represent an independent working module or functionality. This means that each service should be able to be developed and deployed independently without affecting the functioning of other services. It is important that these services communicate with each other via established communication methods Usually it is done via 3 popular methods.

1. API calls

2. Messaging brokers like kafka , rabbitmq

3. Service Mesh

Event Driven Neighbors 💢

One major utility of adopting microservices is the flexibility to transit to event driven architecture Imagine a service X dependent on some other service Y requests some data . However it's only useful to request Y, if Y itself has some data to provide. Still X will keep pestering Y periodically without any knowledge of Y’s state. This is so inefficient ! Takes up a lot of server space , memory and computation. But how about this : “Instead of X asking Y repeatedly, let Y take the initiative of calling X when required” . This sounds so simple and uncomplicated. But when will Y call X ? It will simply X call when it has some data , which means that in the event of Y having the data , Y will call X.

To formally put it up , event driven architecture states that the microservices of a software system function only when a certain event change is detected which is caused by an event trigger initiated by event producers. The services work in absolute abstraction and are not required to have any information regarding the state of the other services. Microservices have been phenomenal in providing business flexibility to the companies, by providing the option to scale up “only the burdened services” instead of buying large costly servers for the entire application. Aside from this it provides a convenient option for rapid deployment due to independence of services. It provides a fault tolerant environment at low costs without compromising on the scalability of the application.

The Great Migration ⏭

So now that you have understood that microservices >> monoliths in various scenarios , its time to refactor your application and migrate your codebase to microservice style. The most important task is to understand and list down all the atomic functionalities of your application. These functionalities come along with dependencies , therefore we should be careful while decimating the application and separating the services.

1. Understand the atomic functionalities and group them by similarity

2. Understand the Dependency Graph of these small grouped services

3. Prioritize the services that need to be migrated stepwise , preferably choose the ones with lesser dependence earlier.

4. Choose a scalable cloud infrastructure provider

5. Setup the API gateway and other communications

6. Finally migrate the data !

Dating App ❣

Now let's look at a short example of how microservices are implemented. This is not the replica of an actual real world implementation , but will definitely give you an idea of how domains are segregated and implemented. We are going to see it from a dating app’s perspective.

The above diagram represents a highly simplified overview of a dating app’s architecture. A real world implementation is quite different and complex and this representation is just for our understanding. For the sake of demonstrations let's simplify the Auth service into two simpler microservices and show how these decoupled services work

1. Signup Service

2. Email Service

Using the standard mvc structure lets define the file structure of the signup service

Both services will operate as a separate spring boot application working on ports 8085 and 8086 respectively.

//registration service :- port 8085
package com.example.registration;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan("com.example.registration.entity.User")
@EnableJpaRepositories({"com.example.registration.entity.User" , "com.example.registration.entity.ConfirmationToken"})

@ComponentScan("com.example.registration.repository")
public class RegistrationApplication {

    public static void main(String[] args) {
        SpringApplication.run(RegistrationApplication.class, args);
        //System.out.println("hello world");
    }

}
// email service :- port 8086
package com.example.emailService;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class emailService {

    public static void main(String[] args) {
        SpringApplication.run(emailService.class, args);
        //System.out.println("hello world");
    }

}

The user Service provides the functionality for user registration which invokes the email service when required by http calls

// the main logic for the user registration which calls the email sender class
package com.example.registration.service;
import com.example.emailService.service.EmailService;
import com.example.registration.entity.ConfirmationToken;
import com.example.registration.entity.User;
import com.example.registration.repository.ConfirmationTokenRepository;
import com.example.registration.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    ConfirmationTokenRepository confirmationTokenRepository;

    @Autowired
    EmailService emailService;

    @Override
    public ResponseEntity<?> saveUser(User user) {

        if (userRepository.existsByUserEmail(user.getUserEmail())) {
            return ResponseEntity.badRequest().body("Error: Email is already in use!");
        }

        userRepository.save(user);

        ConfirmationToken confirmationToken = new ConfirmationToken(user);

        confirmationTokenRepository.save(confirmationToken);
        confirmationToken.getConfirmationToken();
       HashMap<String, String> map = new HashMap<>();
       map.put("email", user.getUserEmail());
        map.put("token", confirmationToken.getConfirmationToken());
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.postForObject("http://localhost:8086/sendMail", map, String.class);
        return ResponseEntity.ok("mail request sent");
    }

    @Override
    public ResponseEntity<?> confirmEmail(String confirmationToken) {
        ConfirmationToken token = confirmationTokenRepository.findByConfirmationToken(confirmationToken);

        if(token != null)
        {
            User user = userRepository.findByUserEmailIgnoreCase(token.getUserEntity().getUserEmail());
            user.setEnabled(true);
            userRepository.save(user);
            return ResponseEntity.ok("Email verified successfully!");
        }
        return ResponseEntity.badRequest().body("Error: Couldn't verify email");
    }
}

The email service accepts the request from the registration service and sends a confirmation mail to the user. The email of the user is set in “application.properties” file.

// the main logic for email Sender Service 
package com.example.emailService.service;
import java.util.HashMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.mail.SimpleMailMessage;

@Service
public class emailSender {
  @Autowired
  EmailService emailService;

  // @Override
  public ResponseEntity<?> sendMail(HashMap<String, String> map) {
    String email = map.get("email");
    String confirmationToken = map.get("token");

    SimpleMailMessage mailMessage = new SimpleMailMessage();
    mailMessage.setTo(email);
    mailMessage.setSubject("Complete Registration!");
    mailMessage.setText("To confirm your account, please click here : "
        + "http://localhost:8085/confirm-account?token=" + confirmationToken);
    emailService.sendEmail(mailMessage);

    // System.out.println("Confirmation Token: " + confirmationToken);

    return ResponseEntity.ok("mail subject sent");
  }

}

The controller file contains the endpoints for both the microservices

// registration service controller (endpoints)
package com.example.registration.controller;

import com.example.registration.entity.User;
import com.example.registration.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public ResponseEntity<?> registerUser(@RequestBody User user) {
        return userService.saveUser(user);
    }

    @RequestMapping(value = "/confirm-account", method = { RequestMethod.GET, RequestMethod.POST })
    public ResponseEntity<?> confirmUserAccount(@RequestParam("token") String confirmationToken) {
        return userService.confirmEmail(confirmationToken);
    }

}
// email service controller (endpoints)
package com.example.emailService.controller;

import com.example.emailService.service.emailSender;
import com.example.registration.entity.User;
import com.example.registration.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
import java.util.List;



@RestController
public class emailController {

    @PostMapping("/sendMail")
    public ResponseEntity<?> registerUser(@RequestBody HashMap<String, String> map) {
        emailSender sender = new emailSender();
        return sender.sendMail(map);
    }
}

Therefore we can summarize the entire flow of communication through this diagram

Deploy and Scale 🚀

This looks surreal , but what if suddenly your application becomes popular and many single boys start using it ? . Now It's the time to scale your services !. Using Azure’s AKS (kubernetes service) You can easily deploy , scale and manage your services .It enables automatic scaling of applications in response to varying workloads. Leveraging Kubernetes, AKS ensures high availability and reliability. Using the Horizontal Pod Autoscaler (HPA) feature, AKS dynamically adjusts the number of running microservice instances based on defined metrics like CPU utilization or custom metrics. This eliminates the need for manual intervention during traffic spikes or lulls, optimizing resource utilization and cost efficiency. AKS integrates with Azure Monitor for in-depth performance monitoring, allowing users to make informed decisions about scaling policies.

Final Touchups 🤔

Its time to discuss the best practices. I've mentioned some critical points to remember

● It is always advisable to adapt asynchronous communication to enhance decoupling of modules

● Always try to do checkpointing by implementing a robust versioning for services.

● Follow the Single responsibility principle suggests to streamline the services

● Use service discovery tools like Eureka to employ scalability and efficient system management

● It is very important to monitor the service health , load and impudence to ensure best performance and help you decide in scaling the services

THANK YOU