Microservice patterns are recurring solutions to common problems that arise when designing, implementing and maintaining microservices-based architectures. These patterns help to address specific challenges and make informed decisions to build scalable, maintainable and flexible systems.
In a previous post, we told you what a microservice architecture is and we went from some more traditional ones such as MVC to Clean Architecture, with all the importance it has in microservice architectures today, to finish with the Database per Microservice pattern.
We continue this series of microservices architecture patterns with the second part, where we look at architecture patterns that focus on the organisation and structure of microservices.
We continue with DDD, Clean Architecture and hexagonal architecture, these 3 patterns can go hand in hand, so we will see what they can bring together. Finally, we will look at serverless architecture in microservices.
Organisation and structure of microservices
DDD
DDD (Domain-Driven Design) is an approach to software development that focuses on modelling the problem domain comprehensively and enriching communication and collaboration between development teams and domain experts, such as users and stakeholders.
DDD provides a set of principles and patterns that help create software that accurately reflects business rules and processes, leading to more effective and maintainable solutions.
At its core, DDD is based on the following key ideas:
- Ubiquitous language: the aim is to establish a common language between development teams and domain experts. The terms used in the code should match the terms used by users and stakeholders.
- Domain modelling: a model of the domain is created that reflects the key business rules and concepts. This model becomes the core of the business logic and guides the structure of the software.
- Sub-domain partitioning: the domain is partitioned into smaller, more manageable sub-domains, each with its own model and logic. This allows complex parts of the business to be addressed more effectively.
- Aggregates and entities: the key concepts of the domain are modelled as entities and aggregates. Aggregates are coherent sets of entities that are treated as a unit.
- Domain services: domain services are created to encapsulate complex logic that does not fit within individual entities or aggregates.
- Domain events: domain events capture important changes and events in the system. These events can trigger actions in other components of the system.
- Bounded contexts: bounded contexts are defined to separate different subdomains and components. Each context has its own model and clear boundaries
- Application layers: DDD proposes a layered architecture where the application layer coordinates the interaction between domain objects and application services.
- Let’s look at an example:
- Let’s say we are developing an e-commerce application and we want to use DDD to model the checkout process.
- Ubiquitous language: we use the common language of “Client”, “Cart”, “Product”, “Order” in code and in discussions with domain experts.
- Domain modelling: we create entities such as “Client”, “Product”, “Order” and “Cart”, each with its own properties and methods that reflect the business.
- Aggregates and entities: we create an aggregate “Order” which includes the entities “Client”, “Product” and “Cart”. Business rules, such as checking product availability, are handled in the aggregate.
- Domain services: we create a domain service to calculate the order total, as this logic does not directly belong to any entity or aggregate.
- Domain events: we can create an “OrderConfirmed” domain event when an order is completed, which can trigger other actions such as notifying the customer or updating the inventory.
- Bounded contexts: we partition the domain into contexts such as “Order Management” and “Product Management”, each with its own model and logic.
When partitioning domains, you can follow different strategies (e.g. Logical Entities vs Business Processes) and have different considerations (e.g. The subdomains of the global business, their associated characteristics, development team, technology family, dependency constraints...)
DDD offers a structured way to address complex domain issues and provides clarity in communication between development teams and business experts. The focus on domain modelling and cooperation between different stakeholders contributes to the creation of high quality software that is better aligned with business needs.
Here, a diagram of the simplified and limited example, methods and events could be adapted and extended according to the needs of the e-commerce application and the specific domain model.
It is possible to observe the different domains that would emerge a priori with their entities.
Hexagonal architecture
The hexagonal architecture proposes that our domain is the kernel of the layers and that it is not coupled to anything external. Instead of making explicit use of the dependency inversion principle, we bind to contracts (interfaces or ports) rather than concrete implementations.
Also known as port and adapter architecture, the design is based on separation of concerns and independence of layers. It seeks to isolate the kernel of the application, where the business logic resides, from interactions with external components such as user interfaces and databases.
Roughly, and without going into too much detail, it suggests that we think of our kernel as an API with well-specified contracts. Defining ports or entry points and interfaces (adapters) so that other modules (UI, DB, Test) can implement them and communicate with the business layer without the business layer having to know the origin of the connection.
In e-commerce, this architecture is essential to ensure the flexibility, maintainability and evolution of the system.
These are called ports and adapters, which could be defined as follows:
- Port: definition of a public interface.
- Adapter: specialisation of a port for a specific context.
Key components:
- Kernel domain: this is the core business logic, including e-commerce specific entities, aggregates, rules and use cases.
- Ports. Ports are interfaces that define how the kernel interacts with external components. In e-commerce:
- Product repository port: defines how products are accessed and stored.
- Payment service port: defines how payments for orders are handled.
- Adapters: adapters implement the ports and connect the kernel to external components:
- Product database adapter: implements the product repository port and interacts with the database.
- Payment gateway adapter: implements the payment service port, connecting to external payment providers.
- User interfaces: user interfaces allow users to interact with the system:
- Web user interface: allows customers to view products, add to shopping carts and place orders.
An example of code would be:
- Port
Product management service port:
public interface ProductService {
Product createProduct(ProductDTO productDTO);
List<Product> getAllProducts();
}
- Kernel domain
Product entity:
public class Product {
private String id;
private String name;
private double price;
// Builders, getters and setters
}
DTO for product creation:
public class ProductDTO {
private String name;
private double price;
// Builders, getters and setters
}
- Adapter
Product controller:
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody ProductDTO productDTO) {
Product createdProduct = productService.createProduct(productDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(createdProduct);
}
@GetMapping
public ResponseEntity<List<Product>> getAllProducts() {
List<Product> products = productService.getAllProducts();
return ResponseEntity.ok(products);
}
}
- Service implementation
@Service
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
public ProductServiceImpl(ProductRepository productRepository) {
this.productRepository = productRepository;
}
@Override
public Product createProduct(ProductDTO productDTO) {
Product product = new Product();
product.setName(productDTO.getName());
product.setPrice(productDTO.getPrice());
// Logic additional, validations, etc.
return productRepository.save(product);
}
@Override
public List<Product> getAllProducts() {
return productRepository.findAll();
}
}
- Organisation is important
A good example of how it would need to be organised in packages would be:
Hexagonal + DDD + Clean Architecture
Using hexagonal architecture, Domain-Driven Design (DDD) and Clean Architecture together can lead to highly efficient and business-oriented software design.
We will explore how these three approaches combine to create a robust and modular structure.
- Hexagonal architecture focuses on separating the core business logic from external interactions such as user interfaces and databases. The key is to define clear interfaces (ports) through which the kernel communicates with external components, and then implement adapters that connect these interfaces to the external infrastructure. This ensures two-way independence and allows changes in one part of the system to not affect the others.
- DDD focuses on capturing and modelling business knowledge in code. It fits perfectly with the hexagonal architecture because as the domain kernel is at the centre of the architecture and remains completely independent of external implementation details. The entities, aggregates, value objects and services of the domain form the core of the business and are modelled according to the domain language.
- The Clean Architecture establishes a layered structure, with the core business at the centre and the outer layers representing the implementation details. The core layer contains the pure business logic and is independent of external technologies. External layers, such as adapters and frameworks, interact with the kernel through defined interfaces. This ensures that the kernel is not dependent on technical details and can evolve smoothly.
Relationship between hexagonal architecture, DDD and Clean Architecture
Hexagonal architecture and Clean Architecture share similar design principles. Both emphasise the separation of responsibilities and the creation of flexible and maintainable code.
Hexagonal architecture and DDD complement each other well, as DDD provides a solid guide to modelling the domain kernel, while hexagonal architecture helps to implement that domain logic in a flexible, decoupled design.
In short, these three methodologies and approaches work together to produce software systems that are understandable, maintainable and adaptable, while faithfully reflecting the complexity and business rules of the domain they address.
Joint implementation
Combining these approaches gives you:
- Complete independence. The domain kernel is completely isolated from the external layers, allowing technology changes to be made without affecting the business, and vice versa.
- Domain modelling. DDD allows business rules and key domain concepts to be represented in code, creating a common language between developers and business experts.
- Separation of concerns. The separation of layers and the definition of clear interfaces ensure a clear separation of concerns and a modular structure.
- Maintainability. The modular structure and independence from external technologies facilitate the evolution and maintenance of the system over time.
In summary, the application of hexagonal architecture, Domain-Driven Design and Clean Architecture together provide a solid foundation for software design. This combination promotes flexibility, business focus and adaptability, resulting in robust and maintainable applications over the long term.
Serverless
Serverless computing technology has transformed the way applications are deployed and scaled in the cloud.
While the term serverless can be somewhat misleading (as there are still servers running), it refers to an abstraction of the underlying infrastructure that allows developers to focus on the code and logic of their application rather than managing servers and resources.
What is serverless computing?
Serverless computing is a cloud development and deployment model where the cloud provider is responsible for managing the underlying infrastructure.
Developers simply upload their code to the cloud and the platform takes care of executing it in response to events such as HTTP requests, database changes or queue loads.
This allows applications to scale automatically on demand without the need to worry about server provisioning.
The benefits of serverless computing:
- Save time and effort. Developers are freed from server administration, patching and scalability tasks so they can focus on writing code and developing features.
- Automatic scalability. Serverless applications can automatically scale on demand. Resources are automatically allocated on demand.
- Cost efficiency. By paying only for actual uptime, serverless computing can be more economical than traditional infrastructure that requires servers to be kept running all the time.
- Rapid development. Abstracting from infrastructure allows developers to deploy and test new features faster.
Focus on business. Teams can focus on solving real business problems rather than worrying about infrastructure.
Use cases:
- Web applications.
Serverless is ideal for web applications with variable load, as they can automatically scale according to traffic.
- Data processing.
Batch or real-time data processing is efficient with serverless because you can run task-specific functions.
- Microservices.
Each microservice can be implemented as a serverless function, making it easier to maintain and scale independently.
- Automatic scaling: one of the key benefits of serverless computing is its automatic scaling. When an event triggers the execution of a function, the serverless platform automatically adjusts the resources needed to handle the load. This is essential for dealing with fluctuations in e-commerce traffic, such as sales spikes during promotions.
- Clusters and orchestration: although serverless computing is often associated with individual microservices, it is important to remember that multiple serverless functions can work together as a distributed system. This is where event orchestration comes in, which can be managed by tools such as Amazon Step Functions or Azure Logic Apps.
- Self-discovery: in a serverless environment, you don’t have to worry as much about self-discovery of services because serverless functions are triggered in response to specific events.
- Circuit breaker: if a function repeatedly fails due to a problem, a circuit breaker can prevent further invocation, protecting the system as a whole.
Challenges:
- Runtime: serverless functions have runtime limits, which can be a challenge for time-consuming tasks.
- Communication between functions: communication between functions may require careful planning, as each function is executed in isolation.
Example of serverless computing:
Let’s say you have an online store and you want to perform an action every time an order is placed, such as sending a confirmation e-mail to the customer and updating the inventory. You can take advantage of serverless computing to handle these actions efficiently.
// Lambda function to process orders
exports.handler = async (event) => {
// Parse on event (which could be an order in JSON format)
const order = JSON.parse(event.body);
// Simulate the sending of a confirmation email
await sendConfirmationEmail(order.customerEmail, order.orderId);
// Update the inventory
await updateInventory(order.products);
// Response successful
return {
statusCode: 200,
body: JSON.stringify({ message: order processed successfully' })
};
};
// Function to send a confirmation email
async function sendConfirmationEmail(email, orderId) {
// Logic to send the email
console.log(`Confirmation email sent to ${email} for the order ${orderId}`);
}
// Function to update the inventory
async function updateInventory(products) {
// Logic to update the inventory of the products
console.log('Inventory updated for the products:', products);
}
How does it work?
- A customer places an order in your online store.
- Your application creates an event containing the order details and sends it to the Lambda function.
- The Lambda function processes the order, sends a confirmation e-mail to the customer and updates the product inventory.
- The Lambda function returns a response indicating that the request has been processed.
This example shows how you can use serverless computing to handle specific tasks in your e-commerce application without having to keep servers running all the time. This saves resources and allows you to focus on delivering a great customer experience.
What did you think? Got any questions? Feel free to leave a comment - we hope you find it useful!
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.