# The Growing Challenges of Complexity in Software Development
Written on
Chapter 1: Understanding Complexity in Software
Managing complexity is fundamental in programming. As Brian W. Kernighan aptly stated, controlling complexity is crucial for creating high-quality code. Increased complexity can make code more challenging to comprehend and modify.
Complexity arises when code performs multiple tasks and has a web of interconnections. Adding a new feature doesn't merely complicate the codebase; it increases the number of related features that also need updates.
Seth Godin's article "Scale vs. Speed" highlights that as organizations grow, they tend to slow down, a phenomenon known as handshake overhead. The formula n*(n-1)/2 illustrates this: while two individuals require one handshake for an introduction, nine people need a staggering 36 handshakes. The implication is clear: more individuals lead to a rise in meetings, approvals, and coordination challenges.
Adding a feature necessitates that the new code interacts with existing components, which illustrates the complexity inherent in software development.
Development may seem straightforward initially, but as the codebase expands and dependencies grow, it becomes increasingly complicated. A developer fixing a bug might unintentionally disrupt another part of the code due to these interconnections. This often leads to an underestimation of complexity; we might account for features A, B, and C but overlook the interactions between them.
The intricate nature of a growing codebase makes it progressively harder to grasp the functionality of individual components and their interrelations. It's akin to navigating a Wikipedia article filled with hyperlinks.
Section 1.1: The Impact of Interconnected Features
In the following example, we see two features (represented in blue) that are interlinked by two connections (shown in red).
This interconnectedness can complicate Agile development. For instance, if modifications are made to Feature A, adjustments may be necessary for Feature B as well. Fortunately, with just two features, there are only two modifications to consider and test.
However, as you continue to develop and introduce additional features, the situation escalates. From a non-technical perspective, adding a single feature may appear to require only half the time it took to develop the previous two, but the reality is far more complex.
Introducing a new feature can necessitate integration with two existing features, potentially resulting in six code updates. As you modify these features, you must understand, update, and test them all.
Implementing quality coding practices and thoughtful design can mitigate these challenges. Automated unit tests can help identify whether changes have caused any issues, while well-structured code minimizes dependencies and employs interfaces to lessen the impact of modifications. Conversely, poorly constructed code often includes large methods that handle numerous tasks, leading to heightened complexity as the codebase expands.
Section 1.2: The Ripple Effects of New Features
With each additional feature, the number of required handshakes can spiral out of control.
A new feature might need to interact with all existing components, resulting in three additional handshakes and potentially six code updates.
The following table illustrates the escalation in handshakes and potential updates required.
Although not all features are interdependent, this table demonstrates how complexity can grow exponentially, illustrating that adding a single new feature can have far-reaching implications.
As the codebase expands, it becomes increasingly challenging for any individual to grasp all its components. Specialization begins to occur as developers focus on specific areas of the code.
Chapter 2: Beyond Code – The Broader Implications
The complexity of code extends beyond just the features; it includes dependencies related to testing, documentation, build/release processes, training, data migration, and integration with other software.
Code consists of numerous interrelated components that encompass essential functions such as security, logging, exception handling, and shared code. The "Don't Repeat Yourself" (DRY) principle advocates for avoiding redundant code, which has the advantage of centralizing updates. However, it also creates numerous connections that complicate the codebase.
Technical debt is an inevitable consequence of Agile development, as new features can alter existing designs in ways that were not initially anticipated. To avoid a complete overhaul of the codebase, developers often resort to patching existing designs, which leads to entropy. The more modifications made, the more the code and design can deteriorate.
Building an Analogy: Consider constructing a one-bedroom bungalow. As requirements evolve, you might add a second bedroom, an extra floor, or even a larger kitchen. Each modification necessitates updates to the electrical and heating systems.
When a client initially requested a simple one-bedroom home but later desired three bedrooms, the result may be a labyrinthine layout that would not have been designed that way from the outset. Such extensions exemplify the challenges of maintaining coherent design amid ongoing changes.