Decomposition — it's the process of splitting a large system (for example, a monolith) into smaller, independent components (in our case — microservices). These components should be self-contained, manageable, and maintainable on their own. But it's not that simple: slicing an app into microservices isn't the same as cutting a pie into equal pieces. You can't act randomly here — you need to consider business logic, bounded contexts, and minimize coupling between services.
If you've ever tried to assemble an IKEA cabinet without the instructions, you have an idea of the kind of chaos that can happen with bad decomposition. We need to make sure each part of the system has a clear responsibility and talks to other components as little as possible, so we don't end up with a distributed monolith.
Principles of decomposition
- Split by business capabilities, not technical layers. One of the most common anti-patterns when moving to microservices is trying to decompose the app by layers (UI, business logic, database). That multiplies dependencies between services and creates extra headaches for independent deployments. For example, instead of making one service for all customer-related data and another for all order-related data, it's better to create separate services like Customer Service and Order Service. Each will own and handle its data, and inter-service communication will be minimal.
- Avoid "fat" microservices. Sometimes it's tempting to dump all related logic into one microservice. But then that service becomes hard to scale and maintain, turning into a monolith inside your microservice architecture. It's like a suitcase without a handle: awkward to carry and you don't want to throw it away.
- Loose Coupling. Microservices should be as independent as possible. Any interaction between them should be minimized and formalized via interfaces like APIs or events. That makes testing, upgrading, and scaling individual components much easier.
- High Cohesion. Code inside a microservice should be united by common goals. It's like friends: with one group you go to the movies, with another you talk about microservices. Don't mix them too much — someone will get bored.
Defining microservice boundaries
The most popular approach to decomposing microservices is using the Bounded Context concept from Domain-Driven Design (DDD). Each microservice should serve a specific domain (or a subset of it), exposing clearly defined ways to interact with other services.
Example:
- Customers (Customer Service)
- Orders (Order Service)
- Payments (Payment Service)
- Notifications (Notification Service)
This could be how an online store is organized. Instead of one monolith doing everything (managing customers, orders, payments, notifications), we build separate microservices where each addresses a specific business need.
How to avoid common anti-patterns?
- Anarchic decomposition. When there's no strategy, developers just create microservices "however it turns out." The problem is this leads to distributed monoliths where systems are too tightly coupled and scaling becomes painful.
- Data duplication. Data duplication between services is inevitable, but it should be minimized. For example, the Order Service shouldn't store full customer profiles — an ID is enough so it can fetch details from Customer Service when needed.
- Over-fragmentation. Don't create services that are too tiny. If you try to make a separate service for every single action (like a dedicated service just for "add item to cart"), you'll incur massive communication overhead.
Practical exercise: decomposing an online store
Let's say you're building a large online store. How would you split it into microservices?
1. Identify main business domains (Business Domains):
- Customer Management
- Product Catalog
- Order Management
- Payment Processing
- Notifications
2. Separate the data: Each microservice should own its data:
- Customer Service stores customer info (name, email, order history, etc.).
- Product Catalog Service manages product info (title, description, price, stock availability).
- Order Service processes orders (order details, statuses).
- Payment Service handles transactions (payments, refunds).
- Notification Service sends notifications (email, SMS).
3. Define interactions:
- Customer Service provides customer info to Order Service.
- Order Service requests product data from Product Catalog Service.
- Order Service sends payment requests to Payment Service.
- Payment Service reports successful payments back to Order Service.
- Events can trigger notifications via Notification Service.
4. Choose interaction styles
- For synchronous calls use REST API. For asynchronous, use something like Kafka: events such as "new order created" or "payment succeeded" can be published to Kafka so other microservices can handle them.
Steps to break a monolith into microservices
- Analyze the current system
- Try to identify the main business functions of your monolith.
- Define responsibility areas
- Group related functional modules into a single responsibility area.
- Split databases
- Distribute data across services so each service manages its own database.
- Ensure interaction
- Choose between synchronous (REST) or asynchronous (Kafka) interactions.
- Migrate functionality
- Gradually move functionality from the monolith into microservices.
- Testing
- Make sure new modules work correctly and interact with each other.
Example: REST API between services
For interaction between Order Service and Product Catalog Service:
// REST client in Order Service to retrieve product data
@RestController
@RequestMapping("/order")
public class OrderController {
private final RestTemplate restTemplate;
public OrderController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@PostMapping
public ResponseEntity<String> createOrder(@RequestBody OrderRequest orderRequest) {
// Sending a request to Product Catalog Service
ResponseEntity<Product> productResponse = restTemplate.getForEntity(
"http://product-service/product/" + orderRequest.getProductId(),
Product.class
);
if (productResponse.getStatusCode().is2xxSuccessful()) {
// Process the order
return ResponseEntity.ok("Order created successfully!");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Product not found");
}
}
}
For asynchronous event delivery use Kafka:
// Publishing the "Order Created" event to Kafka
@Service
public class OrderEventPublisher {
private final KafkaTemplate<String, String> kafkaTemplate;
public OrderEventPublisher(KafkaTemplate<String, String> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
public void publishOrderCreatedEvent(Order order) {
kafkaTemplate.send("orders", order.toString());
}
}
Summary
Good decomposition requires a deep understanding of your business processes, how the system works, and where the bottlenecks might be. A service should be large enough to do a meaningful job, but small and simple enough for development, testing, and deployment. Keep the main rule in mind: one microservice — one business capability.
Like a well-run restaurant kitchen where each cook owns their station, microservices independently do their jobs but together form a complete system.
GO TO FULL VERSION