A good program is a small program

Michał Grabowski
Level Up Coding
Published in
6 min readJul 11, 2022

--

It is hard to reason about anything that consists of many different moving parts. It just exceeds our cognitive abilities as humans. It is hard to introduce any change if everything depends on each other. The consequences of such change are always unpredictable. We can not adapt to new requirements because we get bogged down with all that dependencies. Small, seemingly innocent changes in one place may result in disaster on the other end of the system.

Any complex design calls for modularisation. Consider a regular car. The car consists of thousand different parts altogether. But it is not built as a single monolithic instance where everything is intertwined and tangled. Parts are grouped in higher-order structures. We can easily swap tires or add a more powerful supercharger to the engine. The way air is supplied to the combustion chamber is irrelevant from the engine’s perspective. The engine assumes the air to be provided somehow — this constitutes a contract between the „air provider” and the engine. The engine knows nothing about the internal details of the design of any peryferial device that supports its work. Such design is much easier to understand and reason about. It introduces some form of abstraction. From a car design perspective, such an approach is apparent and obvious. Many different divisions or even companies may focus on developing and optimizing just a single part of the overall system. Any detail of other systems does not disturb them at all. They could afford to entirely focus on their problems and make decisions best for them.

The same rules apply to any other complex design out there. But when it comes to software, it is not always the case. Without deliberate commitment, the design of our system tends to strive toward higher entropy. Complexity keeps increasing as time passes. Why does it look that way in the first place? Part of the problem is the fact we work with something quite intangible. Software, by its very root definition, is volatile and transient. Laws of physics such as gravity do not limit software developers. In software engineering, everything seems possible to achieve; it is just a matter of time and resources. Classical engineering can not neglect or ignore physical constraints. Because of all the physical constraints, it is impossible to just come up with a 10-kilometer high skyscraper. Achieving something like this requires considering thousands of different forces and factors. It is not even easy to build elevators fast enough to operate at such distances that remain comfortable for passengers. In software, we may scale our solutions at ease.

Another difference is that requirements for software keep changing all the time. This is not the case when it comes to building skyscrapers. It is impossible to change your mind and decide you ultimately want the building to be twice as high along the way. This is just not feasible. It would require pulling down everything to the ground and starting over again. In software engineering, we are more than accustomed to expecting change. We design against changes. But the change has a specific inherent trait that makes the change cumbersome. Namely, the change is unexpected in its form. We may do our best to be prepared for certain changes, but sooner than later, the new requirement disrupts our assumptions. The sad reality is that we do not possess a crystal ball; we can not predict the future. To make things even worse, we may be sure that any of our design decision ultimately turns out to be at least undesirable — it is just a matter of time.

There are many concepts out there like: modules, components, microservices, actors, and objects that boil down to the idea of splitting an extensive system into many smaller parts. It is not as important as how you are going to name it. The important part is to focus on the need to split things no matter how. Having many separate parts loosely coupled into bigger structures has enormous advantages.

First of all, such parts are interchangeable. If those parts are small enough, we can get rid of the particular implementation and build something better suited from scratch within one week. Instead of refactoring or fighting against technical debt, we may delete the whole code. This is huge. As long we build small parts, we get this for free.

Imagine a regular e-commerce system. To fill an order, many different parts have to be involved. We need to know about the customer, his discounts, group, etc. We have to know about the stock of ordered items in the warehouse. We have to know the price of course. We have to know about possible delivery methods available for that order. Does the supplier operate at the given address, and how much will it cost? How about parcel insurance? A lot of knowledge is required to fill an order, and it is easy to get all such concerns tangled. We need the habit of looking for smaller cohesive responsibilities. Look at such a proposal:

- Order — responsible for all accountancy, customer details
- Product — what we can buy. It manages all presentation data and descriptions of the product.
- Tariff — provides prices based on given circumstances (customer, country, the temperature outside)
- Discount — focused on all forms of discounts to a particular customer is entitled in the context of given items at the given time. It knows how different discounts apply together and when.
- Warehouse — take care of stock information. Can answer when something will be available. Maybe there is a scheduled delivery of this item. Maybe their many warehouses out there — it is not essential from the perspective of its clients. Clients want a simple answer: when you will be ready to supply a particular item.
- Shipment — knows about offers of specific suppliers. What parcel size do they allow, for example, or when they collect goods from a given address.
- Logistic — coordinates work of shipment and warehouse. Sometimes it is cheaper to send something from a particular location, but it also depends on available stock and other competing orders.

Order knows nothing about tariffs, discounts, warehouses, shipment, or logistics. In some circumstances, the order may be concerned about discounts because of accountancy rules, but the idea of discount is part of its model. Tariff knows nothing about anything; it does not even know about the product’s existence — it is irrelevant from its perspective; it manages price logic that is abstracted away from the product itself. It may handle services, digital content, or subscription fees. All those services are loosely coupled; they do not depend on each other. The warehouse or Shipment does not need to know it is about to fill orders. Those services fulfill more generic requirements, and this is what makes them interchangeable. It is all about finding natural contracts between distinct parts.

Business structures and operations will always change at a different pace and for various reasons. We, as developers, love data consistency. If a customer wants to buy five items, but we got only four items in the warehouse, it is simple for us: deny selling five items. The problem is that business does not work that way. Bussiness often builds its competitive advantage based on its operations in such circumstances. The answer to the above problem is not trivial. We may:

- We may ship four items now and promise to send 5th item later on our cost
- We may ask the customer for a few days more to fill the order
- If the customer is essential, we may try to order a lacking item using some expensive and fast transport
- We may offer to ship only four items and notice the customer when something more will be available
- We may promise the customer to reserve that item for him when it is again available

Notice that not single responsibility or shape of proposed services has been changed. The subject of change is how those services cooperate.

--

--