How to modularize a system and avoid 'Siamese twins'
In software architecture, we encounter the term “Siamese twins.” It refers to two or more modules that are inextricably linked. This situation occurs when a change in one module automatically forces changes in the other. Imagine a warehouse where one person, let's call him John, has to process all types of messages: orders, returns, and discounts. If the sales department introduces a new promotion, John has to adjust his processes, even if the discount information is completely unnecessary to him. This is an example of “domain leakage” and the creation of a distributed monolith.
Architecture anti-patterns that lead to disaster
We often see architectures that complicate life instead of making it easier. One of the biggest mistakes is “State-Transfer Events.” In this approach, an event transfers the entire state of an object - for example, “order status changed to confirmed” - which leads to strong coupling and makes systems dependent on each other. Another problem is when the system architecture begins to dictate communication within the organization. This is a reversal of Conway's Law. It makes the system rigid and difficult to change.
Solution: contextual modularization
True modularity involves extracting independent Bounded Contexts. Each context should have its own model and its own language, tailored to a specific problem. For the sales department, a chair is a product with a price and a discount; for the warehouse, it is simply a box with dimensions. Do not try to create a universal “uber-model” that fits everything. Instead, consider what questions a given context needs to answer. In John's case, the question is, “Can I release the goods?” His module should only be interested in the ID and quantity, ignoring all other details.
Implementation and communication patterns
In practice, bounded contexts can be implemented as microservices, keeping in mind that a microservice is an implementation unit and a context is a logical module. To enable contexts to communicate, use proven patterns:
- Open Host Service: A module (e.g., warehouse) provides an API in its own language (e.g.,
issue_goods
), and other modules must adapt to it. - Published Language: We create an independent, standardized language for communication, e.g., instead of “user bought something,” we use “unit came into possession of goods.”
Here is an example in Ruby of what a simple implementation of this approach might look like.
# The "Sales" bounded context
module Sales
class Order
# Represents a customer's order
attr_reader :items, :customer_info, :discount
def initialize(items:, customer_info:, discount:)
@items = items
@customer_info = customer_info
@discount = discount
end
def publish_event
# This event contains only the necessary information for the "Warehouse" context.
# It's an example of a "Published Language"
# The warehouse doesn't need to know about discounts or customer info
payload = {
'items' => @items.map { |item| { 'item_id' => item[:id], 'quantity' => item[:quantity] } }
}
# A hypothetical method to send the event to a message broker
send_event_to_broker('item_possession_event', payload)
end
end
end
# The "Warehouse" bounded context
module Warehouse
class GoodsIssuer
# This module only cares about the goods to be issued
def receive_event(event_payload)
# The payload only contains item IDs and quantities.
# This prevents "domain leakage" and tight coupling.
event_payload['items'].each do |item|
puts "Checking stock for item #{item['item_id']} and quantity #{item['quantity']}..."
# Logic for issuing goods...
end
end
end
end
The code above shows how the Sales
module can send an event to the Warehouse
module without sharing unnecessary information. This makes the system more resilient to change.
Finally: avoid 'cargo cult'
The biggest pitfall is following popular trends, such as microservices or Kafka, without understanding why we are actually using them. Don't do what others are doing just because it's trendy. Always think about the business context and the problems you are trying to solve. Remember, the right architecture serves the business, not the other way around.
Happy modularizing!