"Three properties determine the complexity of an environment," write authors Gökçe Sargut and Rita McGrath in Learning to Live with Complexity in the Harvard Business Review:
- "Multiplicity refers to the number of potentially interacting elements"
- "Interdependence relates to how connected those elements are"
- "Diversity has to do with the degree of their heterogeneity"
"The greater the multiplicity, interdependence, and diversity, the greater the complexity."
Let's explore how these three properties apply to software systems.
Complicated vs. Complex Systems
A complicated system involves many moving parts, but they interact from one to the next in a linear and predictable way.
A complex system also involves many moving parts, but they interact among each other in random and unpredictable ways.
Sargut and McGrath use similar definitions:
Complicated systems have many moving parts, but they operate in patterned ways.
Complex systems, by contrast, are imbued with features that may operate in patterned ways but whose interactions are continually changing.
As I wrote in the Under 100 article, complexity is the enemy of software:
As developers, we should strive to build applications that are merely complicated rather than complex.
Let's explore how we can do that using Sargut and McGrath's framework for understanding complexity.
Multiplicity refers to the number of potentially interacting elements
Multiplicity in software systems refers to the number of elements that can interact with each other.
While the total number of elements might be beyond our control, we can manage the interactions between them. For instance, if a process involves ten sub-tasks, and their order of completion is not essential in the real world but may matter in the software, we can simplify the system by enforcing a fixed order for completion.
By reducing the uncertainties and minimizing the "potentially interacting elements," we effectively lower the overall complexity of the software.
Interdependence relates to how connected those elements are
To limit interdependence, we can use pure functions to implement important business logic:
A pure function is a deterministic method with no side effects. [H]aving no side effects means that you can safely call a pure function without having to worry about the effect it might have on the rest of your application.
Writing to a database is a side effect. Thus, we can't rely completely on pure functions in our application. Instead, we can use pure functions to model the hairiest logic, while relegating our side effect-producing code to relatively simple methods.
This approach to software architecture is known as Functional Core, Imperative Shell.
Diversity has to do with the degree of their heterogeneity
In other words, how different are the elements that are interacting with each other?
To limit complexity in our software, we should isolate unrelated areas of logic. This could take several forms:
- Creating multiple front-ends rather than a single monolith (e.g., separate General Ledger, Accounts Payable, Accounts Receivable, and Fixed Assets applications as opposed to an all-encompassing monolithic Accounting application)
- Segmenting data into different databases or schemas
- Refactoring 100-line do-it-all procedures into multiple methods with a single responsibility each (file handling, data extraction, data manipulation, database interaction, etc.)
- Relying on interfaces rather than implementations to communicate within and among applications
Or, as Jason Alexander would say, "Quarter-pound of beef on the hot-hot side. Crisp lettuce and tomatoes on the cool-cool side. ... Beef stays hot. Cool stays crisp." Classic.
- Portions of this article's body rewritten for style improvements with the help of ChatGPT
Cover image created with Microsoft Designer