Problem: Break Down A Monolith Application Into Microservices
Let’s imagine that you are the latest team lead of a retails store software application company similar to the likes of Shopify and Square. The current application is built in a monolithic architecture and integrates various features into a single, cohesive system. Here are some of the features currently built into the system: Point-of-Sale (POS) System, Inventory Management, Customer Management, Order Processing, E-commerce Integration, and so on.
The problem however, is that, the application is growing exponentially, teams want to effectively agile and add new features immediately in order to compete with the competitive market. So you are required to build an architecture that independently scales and deploys its services seamlessly.
So you have decided to break your services into independent, deployable microservices that can accommodate multiple team members while shipping quickly to production. But how do you clearly identify from each monolithic components which parts will make for an efficient microservice that could be independently scaled and deployed. The solution is Microservices Decomposition Pattern.
In this issue, we will be discussing how to break down monolithic applications into microservices using the microservices decomposition pattern. We will explain:
Why you need decomposition patterns
The Scale Cube
Two ways to decompose your monolithic application: by business capability and by subdomain
Identifying Bounded Context Boundaries for your microservices
Identifying and decomposing our Retail Store business domain
Why do we need to decompose?
The main reason behind microservice decomposition is the ability to scale independently. Microservices architecture is well-known for its ability to scale and deploy each service independently without being too tightly-coupled.
The first step of this benefit is to clearly identify in our application which part requires independent scale and deployment. The requirements for scale vary according to the individual needs of the microservice, so its important to use design principles to determine scalability requirements.
For example, in our retail store application, we have a customer-facing interface and a separate admin interface. In this case, our customer interface may experience a spike in traffic from time to time, while the admin interface will probably maintain a low and predictable traffic across it’s life cycle in our application. As a result, both services clearly require different scalability requirements.
The Scale Cube
At this junction, we have demonstrated that the major reason we are decomposing our microservices is to scale independently. But how do we scale our application? We need to understand the concept of the scale cube and be able to apply it effectively in our architecture.
By applying the Scale Cube model, we as architects and developers can design scalable and resilient software architectures that can grow and adapt to changing demands effectively.
The Scale Cube model provides a framework for understanding and addressing scalability challenges in software architecture by considering three dimensions of scalability: X-axis scaling, Y-axis scaling, and Z-axis scaling.
X-axis Scaling (Horizontal Scaling):
X-axis scaling involves replicating application components horizontally to handle increased load. Each instance of the application component operates independently and serves a portion of the overall workload.
This is usually referred to as a scale-out operation which can be handled with the likes of Kubernetes or any public cloud providers like AWS, GCP, and Azure. The application will mostly be running multiple copies behind a load balancer.
With X-axis scaling, instead of running a single server to handle all traffic, you could deploy multiple identical servers behind a load balancer. When a user sends a request, the load balancer distributes the request to one of the available servers.
Y-axis Scaling (Functional Decomposition):
Y-axis scaling involves partitioning application functionality across multiple service instances, each responsible for a subset of functionality. This approach is also known as functional decomposition or microservices architecture.
This involves decoupling our architecture into functions; microservices is an example of functional decomposition. It splits the application into multiple, different services. Each service is responsible for one or more closely related functions.
For example, our retail store application encompasses various functionalities such as inventory management, order processing, and customer management and more. We can decompose each of this functionalities into smaller, autonomous deployable units.
Z-axis Scaling (Data Partitioning):
Z-axis scaling involves partitioning data across multiple instances based on specific criteria, such as customer ID, geographic location, or data range. Each partition is served by a separate instance, allowing for efficient data distribution and access.
The Z-axis is similar to the X-axis. In both cases, multiple server instances are used to handle increased load. The key difference lies in what each server instance is responsible for.
In X-axis scaling (horizontal scaling), multiple server instances are created to handle increased load for the same set of functionalities or services. While in Z-axis scaling, multiple server instances are created to handle increased load by partitioning data across them. Each server instance is responsible for serving a specific subset of data.
In our scenario, we will decompose our microservices by applying the Y-axis scalability.
Decompose by Business Capability
Decomposing our microservices by business capability simply means designing software architecture where the system's components are organized around specific business capabilities or functionalities.
In this approach, each microservice is responsible for implementing a distinct business capability, such as product catalog management, order processing, or customer relationship management.
By business capability, it means each business service should be capable of generating value as an independent cohesive and loosely coupled service.
Let's demonstrate this approach with our retail store application:
Product Catalog Management Microservice:
This microservice is responsible for managing the store's product catalog. It takes care of things like adding new items, updating product details, and classifying products. The microservice makes available APIs for product suggestion retrieval, product search, and product detail queries.
Order Processing Microservice:
This microservice is responsible for processing customer orders. It handles tasks such as creating new orders, calculating order totals, and updating inventory levels. The microservice exposes APIs for placing orders, updating order statuses, and generating invoices.
Customer Management Microservice:
This microservice is responsible for managing customer information and interactions. It handles tasks such as creating customer accounts, managing customer profiles, and tracking customer orders. The microservice exposes APIs for registering new customers, updating customer details, and retrieving order history.
Inventory Management Microservice:
This microservice is responsible for managing the store's inventory. It handles tasks such as tracking stock levels, monitoring product availability, and managing supplier relationships. The microservice exposes APIs for updating inventory quantities, receiving new shipments, and notifying when stock levels are low.
Payment Processing Microservice:
This microservice is responsible for handling payment transactions. It integrates with payment gateways, processes payment requests, and ensures secure payment processing. The microservice exposes APIs for processing payments, handling refunds, and generating payment reports.
Decompose by Subdomain
Another way we can decompose our microservice is by subdomains. This pattern recommends decomposing by subdomains. It is recommended to define each cohesive and loosely coupled services should be defined corresponding to domain-driven design (DDD) subdomains.
DDD refers to the application’s problem space (the business) as the domain. A domain consists of multiple subdomains. And each subdomain corresponds to a different part of the business.
In this approach, the application's components are organized around specific subdomains of the business domain. Each microservice is responsible for implementing the functionality related to a distinct subdomain, allowing for better alignment between the software architecture and the business domain.
The challenge however, is to properly identify the subdomains.
To be able to identify these subdomains, we need to understand bounded context and context mapping and how we can use both concept to properly decompose our microservices.
What is DDD Bounded Context Pattern & Context Mapping?
Bounded Context and Context Mapping are both concepts from Domain-Driven Design (DDD) used to model and manage complex software systems, but they serve different purposes and focus on different aspects of system design.
Bounded Context refers to the specific boundaries within a software system where a particular model or set of terms applies consistently. It defines the scope within which a shared understanding of the domain exists, with clear definitions, rules, and concepts.
For example, in our retail store application, the Product Management Bounded Context focuses on managing product information, such as adding new products, updating prices, and categorizing items. The Order Management Bounded Context, on the other hand, focuses on handling customer orders, including creating, processing, and fulfillment of orders. Each bounded context has its own set of rules and terminology specific to its domain.
Context Mapping, on the other hand, is used to manage the relationships and interactions between the different bounded contexts within a system. We use the context mapping to understand how information flows between bounded contexts, how they collaborate, and how they integrate with each other.
In our retail store application, the Order Management Bounded Context needs to interact with the Inventory Management Bounded Context to check product availability before placing an order. Context Mapping would help identify this relationship and define how information flows between the two contexts. It might involve defining APIs or message formats for communication between the contexts, specifying what data is exchanged and how it is processed.
Bounded Context defines the scope and boundaries within which a shared understanding of the domain exists, while Context Mapping helps manage the relationships and interactions between different bounded contexts within a system.
Decomposing Our Retail Store Application
Let’s try to decompose our retail store application with the Subdomain pattern using these concepts we have explained.
1. Identifying Bounded Contexts
The first step is to identify the bounded contexts within our application. Each bounded context represents a specific area of the business domain with its own language, rules, and concepts.
In our retail store application, we can define the following as our bounded context:
Product Management Bounded Context:
This bounded context encompasses all aspects related to managing products within the retail store. It includes entities such as Product, Category, and ProductVariant, as well as CRUD functionalities for adding, updating, and retrieving product information. The Product Management Microservice represents this bounded context.
Order Management Bounded Context:
The Order Management Microservice represents this bounded context. This bounded context focuses on managing customer orders within our retail store. It includes entities such as Order, OrderItem, and OrderStatus, as well as functionalities for creating, updating, and fulfilling orders.
Customer Management Bounded Context:
This bounded context deals with managing customer information and interactions with the retail store. It includes entities such as Customer, Address, and PaymentMethod, as well as functionalities for creating, updating, and managing customer accounts. The Customer Management Microservice represents this bounded context.
Inventory Management Bounded Context:
This bounded context revolves around managing the inventory of products within our retail store. It includes entities such as InventoryItem, StockLevel, and Supplier, as well as functionalities for tracking stock levels, receiving shipments, and updating inventory quantities. The Inventory Management Microservice represents this bounded context.
Payment Management Bounded Context:
This bounded context focuses on handling payment transactions for orders placed in the retail store. It includes entities such as Payment, PaymentStatus, and PaymentMethod, as well as functionalities for authorizing payments, capturing funds, and processing refunds. The Payment Management Microservice represents this bounded context.
2. Define Context Boundaries
Now that we have identified our retail store’s bounded contexts, we need to define clear boundaries between them. These boundaries determine where one bounded context ends and another begins. Boundaries can be defined based on business processes, data ownership, or communication patterns.
Let’s define our boundaries based on business processes. Our retail store application context boundaries will be mapped into four business process: Product Catalog Management, Order Processing, Customer Relationship Management (CRM), Inventory Control.
Product Catalog Management — Business Process
This means when a new product is added to the store, the Product Management Bounded Context handles tasks such as assigning a unique product ID, specifying product attributes (e.g., name, description, price), and categorizing the product into appropriate product categories. This is its clear logical context boundary.
Order Processing — Business Process
It includes tasks such as creating new orders, updating order statuses, and calculating order totals. When a customer places an order on the retail store's website, the Order Management Bounded Context handles tasks such as verifying product availability, calculating order totals (including taxes and shipping costs), and updating the order status as it progresses through different stages (e.g., pending, processing, shipped).
Customer Relationship Management (CRM) — Business Process
It includes tasks such as creating new customer accounts, updating customer details, and tracking customer orders. When a new customer creates an account on the retail store's website, the Customer Management Bounded Context handles tasks such as validating account information, storing customer details (e.g., name, email address, shipping address), and managing customer preferences (e.g., communication preferences, saved payment methods).
Inventory Control — Business Process
It includes tasks such as tracking stock levels, receiving new shipments, and updating inventory quantities. When new inventory arrives at the retail store's warehouse, the Inventory Management Bounded Context handles tasks such as recording incoming shipments, updating stock levels for each product, and notifying the relevant departments (e.g., fulfillment, purchasing) about changes in inventory status.
3. Mapping Relationships Between Bounded Contexts
After defining the boundaries, we need to map the relationships between the bounded contexts.
Mapping relationships between bounded contexts involves understanding how different parts of our application interacts and collaborate with each other. This will help us identify dependencies, communication patterns, and integration points between bounded contexts. Here's how we can map the relationships between the bounded contexts in our retail store application:
Product Management Bounded Context Relationships:
Customer Management Bounded Context: The Product Management Bounded Context provides product information to the Customer Management Bounded Context to display product details to customers when they browse the store.
Order Management Bounded Context: The Product Management Bounded Context informs the Order Management Bounded Context about product availability and pricing when processing customer orders.
Order Management Bounded Context Relationships:
Product Management Bounded Context: The Order Management Bounded Context relies on product information provided by the Product Management Bounded Context to fulfill customer orders accurately.
Customer Management Bounded Context: The Order Management Bounded Context interacts with the Customer Management Bounded Context to retrieve customer details and update order statuses.
Inventory Management Bounded Context: The Order Management Bounded Context communicates with the Inventory Management Bounded Context to ensure that products are available in stock before processing orders.
Customer Management Bounded Context Relationships:
Product Management Bounded Context: The Customer Management Bounded Context interacts with the Product Management Bounded Context to retrieve product information for display to customers.
Order Management Bounded Context: The Customer Management Bounded Context provides customer details to the Order Management Bounded Context for order processing and updates.
4. Handle Communication Across Contexts:
Finally, we need to determine how communication will occur between the bounded contexts. This might involve defining APIs, message formats, or integration protocols. We have to ensure that communication is robust, reliable, and aligned with the needs of both contexts. We will discuss this in details in our upcoming issue where we talk about microservices communication patterns. Subscribe to stay tuned.
Conclusion
Decomposing a legacy monolithic application into a microservices architecture is a formidable endeavor, demanding the expertise of engineers deeply versed in software architecture and engineering. This undertaking requires a wealth of experience and a profound grasp of fundamental engineering principles. Among these principles, understanding the microservices decomposition pattern stands out.
We discussed the microservices decomposition pattern, covering key concepts essential for practical implementation. Using our retail store application as an example, we demonstrated its real-world relevance, showcasing its effectiveness in modern software engineering practices.
To stay updated on the latest insights, trends, and best practices in software architecture and development, subscribe to our newsletter today. Don't miss out on valuable content that will empower you to build resilient and scalable software solutions for the future.
Happy learning!