Software Development

Complexity Has a Ratchet: Why Software Systems Almost Never Get Simpler Over Time and What That Implies for How We Design Them

Every abstraction layer was solving a real problem. Every configuration flag handled a genuine edge case. Every service boundary separated a concern that was genuinely tangled. And yet here we are — maintaining a system that no single person understands, that takes new engineers months to navigate, and that resists every attempt at simplification because every part of the complexity is somebody’s dependency.

The Ratchet Mechanism

A ratchet is a mechanical device that allows motion in only one direction. It clicks forward easily; it cannot be turned back without deliberately disengaging a component that is not designed for daily use. The analogy to software complexity is almost embarrassingly precise. Systems accumulate complexity with every delivery cycle. They almost never shed it. The asymmetry is not accidental — it is structural, arising from incentive gradients, organizational dynamics, and the basic physics of how software dependencies work.

Understanding this is not an academic exercise. It is the prerequisite for understanding why the usual prescriptions for managing complexity — code reviews, architectural standards, refactoring sprints, technical debt backlogs — consistently underperform. These are all mechanisms for slowing the ratchet or occasionally reversing one tooth at a time. None of them changes the fundamental directionality of the device. And as long as we design systems that have robust extension mechanisms but no equally robust simplification mechanisms, the ratchet will keep clicking forward regardless of how disciplined the team is.

Systems have excellent extension mechanisms built in from day one. They almost never have simplification mechanisms designed in at the same time. That asymmetry is a design choice, and it has consequences.

This article is an argument for treating simplification as a first-class engineering concern — not a cleanup task for a future sprint that never arrives, but a structural property that has to be designed for explicitly, the same way you design for observability, for fault tolerance, or for horizontal scaling. Because the ratchet will not stop clicking on its own. You have to build a mechanism to turn it back, and you have to build it while the system is still young enough for it to be buildable at all.

The Local Rationality Trap

The most important thing to understand about how complexity accumulates is that almost none of it arrives through negligence. A senior engineer who takes a shortcut to meet a deadline is contributing to the problem, certainly. But that is not the dominant mechanism. The dominant mechanism is thousands of decisions that were individually sensible, made by engineers who were doing their jobs well, accumulating into a global state that is nobody’s explicit intention and that nobody would have chosen if they had been designing the system in one go.

Consider the trajectory of a typical service. It starts simple. A few months in, it needs to support a new integration, so a configuration property is added. A few months after that, there are two deployment environments with slightly different requirements, so a second configuration layer is added to handle overrides. A year in, there are five deployment contexts, three integration modes, a feature flag system introduced to support gradual rollouts, and a properties file that no one person can hold in working memory at once. Each of those additions was correct at the time it was made. Together, they constitute an accidental configuration management system that was never designed as such and cannot be reasoned about as such.

This is what makes the complexity problem deeply different from a quality problem. Quality failures — bugs, security vulnerabilities, performance regressions — arise from someone doing something wrong, and they can in principle be prevented by doing it right instead. Complexity of the kind described here arises from everyone doing things right. There is no error to find, no bad actor to hold accountable. There is only the aggregate of correct decisions producing an incorrect system.

 Software systems grow the way cities do — not through a single act of design, but through the accretion of individually rational decisions made under local information. A city is not designed to have seven overlapping transit systems and thirty distinct zoning categories. It accumulates them because each one solved a real problem at the time it was introduced. Simplification, in both cases, requires a different kind of authority and a different time horizon than the decisions that created the complexity.

Fifty Years of Evidence: What Lehman’s Laws Actually Say

The observation that software systems tend toward complexity is not a recent insight. Manny Lehman, a computer scientist at Imperial College London, formulated what are now known as Lehman’s laws of software evolution in 1974 — over fifty years ago. The second of his eight laws, “Increasing Complexity,” states directly that as an E-type system evolves, its complexity increases unless explicit work is done to maintain or reduce it. An E-type system, in Lehman’s taxonomy, is software that is embedded in a real-world environment and must adapt to changes in that environment — which is a description of essentially every non-trivial piece of production software.

What Lehman observed through empirical study of large software projects is that complexity growth is not aberrant behavior. It is the default behavior. It is what happens when a system operates under continuous delivery pressure in a changing world. The first law, “Continuing Change,” explains why: systems must keep adapting to remain useful, and each adaptation adds structure. The fifth law, “Conservation of Familiarity,” explains another facet of the ratchet: the people building and maintaining a system cannot absorb change faster than a certain rate, which means that each increment of complexity reduces the bandwidth available for the next one. The system grows harder to change precisely because it has already changed so much.

What is striking about Lehman’s laws, fifty years after their publication, is how completely they have failed to reshape the way software is designed. Engineering education still treats complexity as a risk to be managed rather than a physical property of E-type systems to be designed around. Architecture reviews still focus overwhelmingly on extension — what new capability a design enables — and minimally on simplification — what existing capability a design retires or consolidates. The ratchet is understood. The appropriate response to it is not.

Relative cost of adding vs. removing a structural component over system lifetime

Cost of addition stays roughly flat; cost of removal grows with age as dependencies accumulate around the component

Essential vs. Accidental Complexity — And Why the Distinction Has Limits

The most useful conceptual framework for thinking about complexity in software comes from two papers separated by twenty years. In 1986, Fred Brooks published No Silver Bullet, distinguishing between essential complexity — the inherent difficulty of the problem being solved — and accidental complexity — the extra difficulty introduced by our tools, languages, and implementation choices. Brooks’s argument was that most of the complexity in software is essential and therefore irreducible: no tool or technique would eliminate it, because it reflects the genuine intricacy of the real-world domain being modeled.

In 2006, Ben Moseley and Peter Marks published Out of the Tar Pit, revisiting Brooks’s distinction and reaching a more optimistic conclusion. They disagreed that most complexity is essential, arguing instead that a large fraction of the complexity in real systems is accidental — a consequence of mutable state, implicit control flow, and implementation details leaking through abstractions — and could in principle be eliminated through better language and architectural choices. Their proposed solution, Functional Relational Programming, has not swept the industry. But their taxonomy remains important: it distinguishes the complexity you cannot avoid from the complexity you chose, and it frames the design question correctly.

The limitation of the essential/accidental distinction, however, is that it implies a fixed classification. In practice, complexity does not stay in one category. What begins as an accidental complexity — a workaround for a specific version of a library, a configuration flag introduced to handle a temporary constraint — often becomes load-bearing over time. Other code grows up around it. Teams learn to work with it. The workaround becomes canonical. What was accidental becomes, from the perspective of anything depending on it, essential. This is the ratchet operating at the conceptual level: accidental complexity converts to essential complexity through the accumulation of dependencies, and that conversion is largely irreversible.

The Four Structural Forces That Lock Complexity In

To design systems that resist the ratchet, you need to understand the four structural forces that make complexity so much easier to add than to remove. These are not personality failings or cultural deficiencies. They are properties of how software organizations and software systems work under continuous delivery pressure.

1. Dependency gravity

Every component that exists in a system attracts dependencies over time. This is not a metaphor — it is a direct consequence of how engineers solve problems. When a new feature needs some behavior that an existing component already provides, using that component is locally rational: it is faster, it reuses tested code, and it avoids duplication. The problem is that each new dependency makes the component harder to remove or significantly change, because now removal requires refactoring everything that depends on it. The component’s dependency count increases monotonically. It essentially never decreases. And a component with many dependents cannot be simplified without a coordinated effort that none of the individual teams owning those dependents has any incentive to organize.

2. The cost asymmetry of deletion

Adding code is a local operation. You write new files, new classes, new methods. Nothing existing has to change. Removing code, by contrast, is a global operation. You have to find everything that uses what you are removing, verify that removing it does not break anything, coordinate with the teams whose code depends on it, and often provide a migration path so that dependents can remove their dependency before you can remove yours. The coordination cost of deletion scales with the size of the system. The coordination cost of addition does not. This asymmetry means that even when teams agree that something should be removed, the friction of the removal process reliably causes it to be deferred — and deferred removal is no removal at all.

3. Knowledge decay and the cost of comprehension

Simplification requires understanding what you are simplifying. As a system grows and its original authors move on, the cost of gaining sufficient understanding to safely remove or consolidate a component grows correspondingly. A subsystem that could have been refactored in two weeks by its original author might take three months of archaeology by the team that inherited it — not because the refactoring is harder, but because the understanding required to do it safely is no longer resident in anyone on the team. Research on structural complexity consistently finds strong relationships between complexity and maintenance effort: the more complex a component, the more effort is required to understand it well enough to change it, which in turn reduces the probability that any given team will invest in simplifying it.

4. Organizational boundary crystallization

Software systems tend to mirror the organizational structures that produced them — a phenomenon described by Conway’s Law. Once a system has been running long enough that different parts of it are owned by different teams, the boundaries between those parts become institutional as well as technical. Simplification that crosses team boundaries requires negotiation, alignment on priorities, and often a sponsor with sufficient authority to override the local incentives of each team to keep its part stable. In practice, cross-team simplification almost never happens without an explicit organizational mandate. And explicit organizational mandates for simplification are rare, because the value of simplification is diffuse and long-term while the cost is immediate and concentrated in specific teams.

ForceMechanismWhy it favors additionWhy it resists removal
Dependency gravityEvery component attracts callers over timeUsing existing code is locally rationalRemoving a component requires migrating all callers
Deletion cost asymmetryAddition is local; removal is globalNew code touches nothing existingRemoval requires coordinated change across all dependents
Knowledge decayOriginal authors leave; context erodesAddition only requires local contextSafe removal requires deep system understanding
Org boundary crystallizationSystem structure mirrors org structureWithin-team changes need no negotiationCross-team simplification needs explicit mandate

Designing Simplification Mechanisms In from the Start

Here is the central claim of this article, stated plainly: the only way to counteract the ratchet is to design counter-ratchet mechanisms into the system at the same time you design the system itself. Simplification mechanisms added retroactively face a systems in which every component already has dependencies, in which knowledge has already partially decayed, and in which organizational boundaries have already crystallized. Designing them in from the start means building them before those conditions obtain, while the cost of structural change is still low.

What does this mean in practice? It means treating a system’s capacity for its own simplification as a non-functional requirement, the same way you treat capacity for its own observation or its own recovery from failure. It means asking, at every design review, not only “how will new functionality be added?” but also “how will this be removed when it is no longer needed?” And it means building specific, concrete mechanisms — not good intentions or cultural norms, but actual technical and organizational structures — that make simplification cheaper relative to extension than it would be by default.

Ten years ago, teams routinely shipped systems with no structured logging, no metrics, and no distributed tracing, and then added these things later — under crisis conditions, after production incidents, at three times the cost. The industry eventually learned to treat observability as a day-one requirement. The same transition needs to happen for simplification capacity. Systems that have no designed-in simplification mechanisms will have the same relationship to simplification that unobservable systems have to debugging.

The Asymmetry of Effort: Adding Versus Removing

To make the argument concrete, consider the actual effort distribution in a typical software organization over a year. Feature work — adding new behavior — accounts for the large majority of engineering time. Refactoring and simplification, when it happens at all, is typically a percentage of total effort small enough that it cannot keep pace with the complexity being added by the feature work. The ratchet advances by one tooth with every sprint. It might be partially reversed once per quarter if the team is disciplined. The mathematics are not in favor of stasis, let alone simplification.

Engineering effort allocation in a typical delivery-focused team

Simplification competes for capacity in a context where it has no natural constituency and delivers no immediately visible user value

This effort distribution is not pathological. It is the natural result of how software delivery is incentivized. Features have visible stakeholders. Bug fixes have visible symptoms. Infrastructure has visible failure modes. Simplification and removal have diffuse, future, probabilistic benefits that are essentially invisible to anyone outside the engineering team, and often invisible to the engineering team itself. There is no metric that goes up when you remove a component. There is no stakeholder whose immediate problem is solved by reducing the number of configuration layers. The value of simplification is real but it is structural, and structural value does not translate easily into sprint priorities.

The StackOverflow Developer Survey consistently ranks technical debt as the most common frustration among developers — cited by more engineers than hiring difficulties, deployment friction, or tool limitations combined. Estimates suggest it wastes up to eight hours per developer per week. That is a full working day per person per week spent navigating complexity that should not exist. The cost is enormous and precisely measured. The investment in prevention is treated as a luxury.

Practical Principles for Simplification-Aware Design

Given everything above, what does simplification-aware design actually look like? These are not abstract principles — they are concrete practices, each targeting one of the four structural forces that lock complexity in.

Expiry dates for architectural decisions

Every significant architectural decision creates a candidate for future simplification. The problem is that decisions, once made, tend to persist until someone actively questions them — which almost never happens. A deliberate counter-mechanism is to attach explicit review triggers to significant decisions at the time they are made: a flag introduced to handle a transitional state should have a documented condition under which it will be removed. A compatibility shim introduced to bridge two API versions should have a documented target date for retirement. The decision log becomes a simplification backlog. This does not guarantee simplification happens, but it creates the preconditions for it to be a planned event rather than a heroic effort.

Designing for deletability

Components that are designed to be deleted have different structural properties than components that are designed only to be used. A deletable component has a bounded public interface rather than a sprawling one. It manages its own dependencies rather than accumulating them from context. It has an API surface that is versioned and formally deprecated before removal rather than simply removed. These are not radical design choices — they are refinements of existing good practice. But making deletability an explicit goal at design time changes the questions that get asked: not only “what does this expose?” but also “what would it take to remove this in eighteen months?”

Dependency ceilings

One of the most underused simplification mechanisms is a hard limit on how many other components a given component may depend on. This sounds constraining, but its effect is to force design conversations earlier, when they are cheaper, rather than allowing dependency accumulation to proceed unchecked. A component that has reached its dependency ceiling cannot grow more complex without an explicit architectural decision to refactor it. The ceiling creates friction for addition and that friction, applied consistently, reduces the rate at which complexity accumulates without requiring any heroic cleanup effort.

Staged deprecation as a first-class concern

The Java language itself demonstrates what serious deprecation practice looks like: elements are marked @Deprecated, then @Deprecated(since=...), then @Deprecated(forRemoval=true), across multiple releases, giving callers a planned migration path. Most application code treats deprecation as an afterthought — something you add after you have already decided to remove something, rather than a phase of the component lifecycle that begins long before removal. Treating deprecation as a first-class phase means planning the migration path at the time of the original decision, not retroactively under pressure.

Simplification as a funded capacity

Perhaps most importantly: if simplification is not a funded, tracked, reported engineering activity, it will not happen at scale. Teams that invest 20% of their engineering capacity in simplification and removal as a standing commitment — not a situational cleanup, but a regular, budgeted activity — consistently report lower onboarding times for new engineers, faster delivery cycles, and lower defect rates than teams that treat simplification as a nice-to-have. The investment is not free. But Lehman’s second law tells us that complexity will increase unless explicit work is done to reduce it. “Explicit work” requires explicit capacity. There is no passive version of this.

Mechanism 1- Expiry triggers on decisions

Attach review conditions to flags, shims, and workarounds at creation time. Turns the decision log into a simplification backlog.

Mechanism 2- Design for deletability

Bounded interface, owned dependencies, versioned API surface. Ask “what would removal cost?” at the same time as “what does this expose?”

Mechanism 3- Dependency ceilings

Hard limits on dependency count per component. Creates friction for accumulation without requiring cleanup. Forces design conversations early.

Mechanism 4- Staged deprecation policy

Deprecation as a lifecycle phase, not an afterthought. Plan migration paths before removal, not during. Model: @Deprecated(forRemoval=true) as standard practice.

Mechanism 5- Funded simplification capacity

A standing budgeted allocation — not a cleanup sprint, not a hackathon — for systematic removal and consolidation. Reported, tracked, reviewed at the same cadence as feature delivery. Without budget, the ratchet wins by default.

What We Have Learned

Complexity accumulates in software systems the way sediment accumulates in a delta — not through negligence or bad intent, but as the natural byproduct of a system doing what it is supposed to do: adapting continuously to a changing world. Lehman described this in 1974 and his second law has been empirically validated many times over. It is not a hypothesis. It is the observed behavior of E-type systems under real-world conditions.

The ratchet mechanism is structural. It arises from four interacting forces — dependency gravity, the asymmetric cost of deletion versus addition, knowledge decay, and the crystallization of organizational boundaries — none of which is a failure mode and none of which is corrected by discipline alone. These forces are properties of the system, not of the team. You cannot resolve them by asking the team to care more.

The essential/accidental complexity distinction, introduced by Brooks and refined by Moseley and Marks, is useful but incomplete. It implies a fixed classification. In practice, accidental complexity converts to essential complexity through the accumulation of dependencies — meaning that the window in which simplification is tractable closes over time. Every month a component exists without being actively managed is a month in which new dependencies accumulate around it and the cost of its eventual removal grows.

The design implication follows directly: simplification mechanisms have to be designed in at the same time as extension mechanisms, while the system is still young enough for them to be effective. Expiry triggers on architectural decisions, designs that are explicitly built to be deleted, dependency ceilings, staged deprecation policies, and funded simplification capacity are not luxuries. They are the structural antidote to a structural problem. Without them, the ratchet will keep clicking forward. The only question is how long before the cost of that becomes the dominant constraint on everything the team is trying to do.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button