Centralizing the decision-making

#architecture Aug 24, 2023 2 min Mike Kowalski

Imagine we’re building yet another e-commerce platform. One of its crucial business processes is, of course, processing an order. After a successful payment, the Orders module (domain) has to call Warehouse asynchronously to prepare purchased goods. Yet, they may not be there. Usually, it’s not a big deal, since we can get them from our suppliers. But what if any of the items are not available anymore? The order has been already placed! Money has changed hands. Our customer is already waiting for delivery. We have no choice - the order has to be canceled.

Let’s assume Warehouse has to let the Orders know about it. Putting deployment strategy and communication medium aside, how should we name such an operation on the API level? Should it be cancelOrder or handleItemUnavailable? One would say it’s just a matter of preference, but I disagree with that. In fact, it’s not about these two names in particular, but about who’s making the decision. Surprisingly, our choice has serious consequences for the overall design and long-term maintainability.

A diagram illustrating the collaboration between the Orders and Warehouse modules
In our case, the Warehouse "invokes" the operation on the Orders side. Yet, it doesn't mean they are separately deployed components communicating via network! Exactly the same problem can occur in the monolithic application when one class/module/domain collaborates with another.

To achieve high cohesion and loose coupling we need to centralize the decision-making. This means that each module (domain) should be the only decision-maker when it comes to its area of responsibility. At the same time, these modules should know only what is absolutely necessary about each other.

Choosing cancelOrder as a name makes the Warehouse deciding about when to cancel an order. In fact, Warehouse may not need the order concept at all! From its perspective, the requested item is permanently unavailable, that’s all. Avoiding importing foreign concepts supports loose coupling.

Orders should be the only one caring about, well… orders. The whole order lifecycle management should be encapsulated here. Encapsulation improves cohesion, which is desired. Choosing handleItemUnavailable means it’s up to Orders to make the next move.

Centralizing the decision-making increases long-term maintainability. To improve the customer experience, we may avoid immediate cancellation in such cases. Instead, we could offer a similar product at a reduced price. With cancelOrder as a name, we would need to touch at least two modules (domains) to introduce such a change. Choosing handleItemUnavailable makes the change transparent to the Warehouse.

In the event-driven world, we could even turn this call into a more general WarehouseItemUnavailable event. This would not only eliminate the point-to-point communication, but also allow plugging additional consumers later on. Once the item is no longer available we may want to update our catalog to prevent further purchases, or notify our sales department…

The described approach scales well across different deployment strategies. Orders and Warehouse could be two different files or modules of the same application, as well as separate services. Depending on the context, the operation name may slightly differ. Yet, the reasoning behind it stays the same. We want to centralize the decision-making.

Mike Kowalski

Software engineer believing in craftsmanship and the power of fresh espresso. Writing in & about Java, distributed systems, and beyond. Mikes his own opinions and bytes.