In today’s technology, where software development is characterised by distributed systems and generally implemented in microservices, the adoption of patterns and designs for generic solutions seems almost a necessity. These patterns solve common problems such as migration from a monolith to microservices, coexistence of a legacy system with a current one, production staging, communication, resilience, scalability, etc.
Microservice architecture patterns offer a number of advantages and benefits for application design and development. These patterns are designed to address the specific challenges of building microservices-based systems.
Below, we propose a series of posts that will guide us through the different patterns that solve all these cases. The idea is to create an intermediate vision where it is clear what each point consists of (without going into too much detail, which would be long enough to write a book).
Today we are going to focus on architecture patterns focused on the organisation and structure of microservices.
What is microservices architecture?
Microservices architecture is a modern and flexible approach to application design. Instead of building a single monolithic application, we break it down into small independent services. Each service focuses on a specific task and communicates with other services through well-defined interfaces.
The idea is that each microservice is like a Lego piece that fits perfectly into the puzzle of the whole application. Each service can be developed, tested, deployed and scaled independently, facilitating agility and continuous software delivery.
A microservice can use its own technology and programming language, giving us the freedom to choose the best tool for the job. In addition, by having small, independent services, we can improve the scalability and availability of the system, as each service can be replicated on demand.
However, there are also challenges associated with microservices architecture. The complexity of managing multiple services, the communication between them, and the need to establish a robust infrastructure for monitoring and management are important aspects to consider in service governance.
In short, microservices architecture is an excellent choice for building modern, scalable applications. With proper planning and design, we can take full advantage of this architecture and deliver robust and adaptable solutions to today’s software development challenges.
When designing an application’s architecture, we need to think about the roadmap and what our future needs will be. You also need to be aware of the different types of microservices architectures and know what questions to ask when implementing them.
Let’s look at an example of a very simple basic microservices architecture. We will need to make changes depending on our needs.
As you can see in the diagram, we have several elements:
- The Gateway/API: it acts as an entry point for client requests and routes the requests to the appropriate microservices.
- User Service: the first microservice, which will handle user management.
- Product Service: this will be another microservice and will take care of everything related to products.
- Order Service: this will manage purchase orders, also as a microservice.
- Payment Service: as another microservice that handles payments. This microservice will be called directly by the order microservice, we will see later that this can be done in several ways.
- DB: each microservice has its own DB, they can also share it and split schemas, tables, collections, etc.
- Client: finally, the client(s) that interact with the microservices through the API Gateway.
The microservices architecture allows for service independence and scalability, where each box in the diagram represents a self-contained service that can be developed, deployed and scaled independently. In addition, the API Gateway acts as a façade for microservices and helps manage communication between clients and services.
MVC Pattern
The Model-View-Controller (MVC) pattern is one of the most well-known and widely used architectural patterns in web and desktop application development. MVC divides the application into three main components: Model, View and Controller. Each component has a specific function and is designed to maintain a clear separation of concerns.
Components of the MVC:
- Model: it represents the business logic and underlying data. It manages the state of the application and provides methods for accessing and handling data.
- View: this is the visual representation of the data in the Model. It presents information to the user and handles the presentation and user interface.
- Controller: it acts as an intermediary between the Model and the View. It processes user interactions and decides how to update the Model and View accordingly.
In a Spring application, this would be an example:
Model:
package com.ecommerce.model;
public class Product {
private Long id;
private String name;
private double price;
// Getters y setters
}
package com.ecommerce.repository;
import com.ecommerce.model.Product;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class ProductRepository {
private final Map<Long, Product> products = new HashMap<>();
public Product findById(Long id) {
return products.get(id);
}
public List<Product> findAll() {
return new ArrayList<>(products.values());
}
public void save(Product product) {
products.put(product.getId(), product);
}
}
View:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<title>Detalles del Producto</title>
</head>
<body>
<h1>Detalles del Producto</h1>
Nombre: ${product.name}
Precio: ${product.price}
</body>
</html>
Controller:
package com.ecommerce.controller;
import com.ecommerce.model.Product;
import com.ecommerce.repository.ProductRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@Controller
public class ProductController {
private final ProductRepository productRepository;
public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@GetMapping("/product/{id}")
public String getProduct(@PathVariable Long id, Model model) {
Product product = productRepository.findById(id);
model.addAttribute("product", product);
return "product"; // Nombre de la vista (product.jsp)
}
}
In this example, the ProductRepository is responsible for storing and managing the products. The ProductController communicates with the repository to retrieve the product data and pass it to the view.
Note that this is a very simple example and that, in more complex applications, you may use real databases instead of an in-memory data structure.
MVC is traditional and is being replaced by more modern and versatile architectures where the front end is completely decoupled. It often appears in architectures as a source to migrate to a microservices architecture.
Clean Architecture
When talking about architectural patterns, it is very useful to mention the concept of clean architecture. Clean Architecture is a software design pattern that aims to produce applications with clean, well-structured and maintainable code. It was proposed by Robert C. Martin also known as ‘Uncle Bob’. The main objective of this architecture is to separate the different layers of an application, so that each has a clearly defined responsibility and is decoupled from the others.
Clean Architecture follows the principle of UI (User Interface) Independence, which means that the innermost layers are nor dependent on the outer layers. This means that changes to the user interface or technical details do not affect the core business logic, making the application more flexible and easier to adapt.
The main layers of Clean Architecture are:
- Entities: this layer contains the basic entities and business rules of the application. It represents the core of the business logic, independent of any technical details or user interface.
- Use Cases: these are the high-level interactions that the application can perform. Use cases represent the application logic and use the domain entities to execute workflows and processes.
- Interface Adapters: these adapters are responsible for communicating with the use cases and translating user interface data and requests into a format that the business logic can understand. They may include web drivers, presenters, database adapters, etc.
- Frameworks and Drivers: this layer contains the external tools and frameworks that the application uses to interact with the outside world, such as databases, web services and so on. The adapters in this layer are responsible for converting the data and requests into a format suitable for the rest of the application.
Suppose we are building an e-commerce system based on microservices. Each microservice represents a specific functionality, such as product management, order processing and user authentication.
We are going to apply Clean Architecture to this situation, taking into account that we are going to implement the product management microservice:
- Entities: we define the “Product” class, which represents a product in the catalogue.
public class Product {
private String name;
private double price;
private int stock;
// Getters and setters
}
- Use Cases: we create the use cases for each functionality of the application.
public interface AddProductUseCase {
void addProduct(Product product);
}
public interface PurchaseUseCase {
void purchaseProduct(Product product, int quantity);
}
public interface ViewAllProductsUseCase {
List<Product> getAllProducts();
}
- Interface Adapters: we implement the adapters that communicate with the use cases from the user interface.
public class ProductController {
private AddProductUseCase addProductUseCase;
private PurchaseUseCase purchaseUseCase;
private ViewAllProductsUseCase viewAllProductsUseCase;
//Constructor e inyección de dependencias
@PostMapping("/products") public
ResponseEntity<String> addProduct(@RequestBody Product product) {
addProductUseCase.addProduct(product);
return ResponseEntity.ok("Product added successfully");
}
@PostMapping("/purchase")
public ResponseEntity<String> purchaseProduct(@RequestBody PurchaseRequest purchaseRequest) {
Product product = getProductById(purchaseRequest.getProductId());
purchaseUseCase.purchaseProduct(product, purchaseRequest.getQuantity());
return ResponseEntity.ok("Purchase successful");
}
@GetMapping("/products") public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = viewAllProductsUseCase.getAllProducts();
return ResponseEntity.ok(products);
}
}
- Frameworks and Drivers: we use a database to store the products and an adapter to interact with it.
@Repository
public class ProductRepositoryImpl implements ProductRepository {
private List<Product> productList = new ArrayList<>();
@Override
public void save(Product product) {
productList.add(product);
}
@Override
public List<Product> findAll() {
return productList;
}
}
Clean Architecture gives us a clear organisation of our application. Use cases and entities are isolated from the technical details and user interface, giving us the flexibility to make changes and improvements without affecting other parts of the application.
Clean Architecture brings several significant benefits to the design and development of microservices in a system. These benefits come from its focus on separating of responsibilities and creating modular and maintainable code. Here are some of the ways in which Clean Architecture adds value to microservices:
- Decoupling and flexibility: Clean Architecture promotes strong decoupling between the different layers of a microservice. This allows each layer to operate independently and changes to one layer do not directly affect other layers. As a result, microservices can more easily evolve and adapt to changing requirements without causing unwanted side effects.
- Scaling: microservices typically scale independently to handle the load. Clean architecture facilitates scalability because each microservice can be optimised and scaled according to its specific needs without affecting other parts of the system.
- Facilitates collaboration: with a clear and well-defined layered structure, multiple development teams can work on different microservices in parallel without impacting each other’s functionality. This promotes agile collaboration and enables the efficient construction and evolution of microservices.
- Improves maintainability: emphasises the separation of core business logic from technical details. This makes it easier to maintain, as changes to the technical implementation do not require changes to the business logic. In addition, because the code is organised into layers, it is easier to understand and updates can be made with greater confidence.
- Change management: the modular nature of Clean Architecture allows specific changes to be made without affecting the overall functioning of the microservice. This is particularly useful in a microservices environment where continuous and evolutionary upgrades are common.
- Technology independence: each layer of Clean Architecture can use technologies and tools that are fit for purpose. This is essential in microservices, where different microservices may be written in different programming languages or frameworks, depending on requirements.
- Easier testing: the separation of layers allows testing to be isolated and effective. Unit tests can focus on business logic, while integration tests can evaluate how different layers interact with each other.
- Adherence to SOLID principles: Clean Architecture aligns well with SOLID software design principles (SRP, OCP, LSP, ISP, DIP), resulting in cleaner, more structured and easily extensible code.
Clean Architecture provides a solid foundation for the design and development of microservices by promoting modularity, decoupling and flexibility. These attributes are critical to creating microservices that are maintainable, scalable and adaptable in an ever-changing environment.
Database per Microservice
The Database per Microservice pattern is an architectural strategy that suggests that each microservice has its own independent and dedicated database. Instead of sharing a single centralised database, each microservice has its own schema and database, allowing microservices to be more autonomous and avoiding coupling between them in terms of data storage.
This approach aims to achieve greater independence and scalability between microservices. Each microservice can choose the technology and type of database best suited to its purpose, without impacting the other services. In addition, the dedicated database facilitates individual scaling of microservices according to their load and data access needs.
The Database per Microservice pattern has several advantages:
- Technology independence: each microservice can use the database technology that best suits its requirements, without being constrained by a single shared technology.
- Low coupling: microservices do not share schemas and tables in the database, which eliminates the need for database changes when a microservice is updated.
- Individual scalability: each microservice can scale its database as needed, without impacting other microservices.
- Ease of maintenance: changes to one microservice do not impact other microservices, making it easier to maintain and evolve each one individually.
- Resilience and fault tolerance: if one microservice fails, the database integrity of other microservices is not affected.
- Enforcement of domain boundaries: each microservice can define its own security restrictions and policies in its own database.
Despite the benefits, the Database per Microservice approach has its challenges and considerations:
- Data consistency: consistency between microservices can be more difficult to achieve because not all data resides in a single database.
- Data integration: a strategy is required to integrate data between microservices when required to perform certain functions.
- Management complexity: maintaining multiple databases can increase administrative and backup complexity.
- Cost: database infrastructure and administration of multiple databases can be more expensive.
Let’s look at an example:
Suppose we have several microservices in an e-commerce application. We will continue with the example of an e-commerce.
In this example, we will see what reasons would lead us to choose one database or another; there is not always a single good solution, so everything is debatable and can change depending on the requirements of the type of application. Another important point when making decisions is the cost, having different databases, licences, clusters can make a project economically unviable, so it is also possible to grow as the need arises. Let’s not forget that we are giving an example based on the pattern described.
- User Service: for the services that support the user management functionality, we could opt for a PostgreSQL database. Especially as it relates to other entities, such as orders and products.
- Product Service: orders are also directly related, but it would be an opinion to opt for a NoSQL database because each product can have its own fields and a strict schema is a limitation.
- Order Service: as with users, the choice of a relational database would be very valid.
- Cart Service: in the case of the shopping cart, a cache would be useful because it would have a lifespan and we also don’t want it to penalise the databases with the movement of users when adding and deleting items.
- Search Service: within our e-commerce, if we want to be able to do semantic searches, part of the content of the products could be stored in elasticsearch.
Each microservice can choose the database technology that best suits its needs. This allows development teams to work independently and prevents changes in one microservice from impacting the database of another. However, if combined product and order information is required, a data integration strategy must be implemented.
In summary, the Database per Microservice pattern is a strategy that provides independence and scalability to microservices, but also introduces challenges of consistency and data management. The choice of this pattern should be based on the specific needs and requirements of the application.
The Bottom Line
In this post we have taken a first approach to microservices architecture. We have reviewed what it is, its advantages and what it can do for us. In addition, we have discussed some of the microservice architecture patterns such as MVC pattern, Clean Architecture or Database per Microservice.
But there is much more to come:
We will review the organisational architecture patterns and structure of microservices, the communication and coordination of microservices and, later on, we will look at DDD, Hexagonal Architecture, as well as what they can contribute in conjunction with Clean Architecture or Serverless.
Comments are moderated and will only be visible if they add to the discussion in a constructive way. If you disagree with a point, please, be polite.
Tell us what you think.