Fundamentals 21 min read

Why Software Complexity Is Killing Developers and How to Tame It

The article explores the nature of software complexity, contrasting rational metrics like McCabe cyclomatic complexity with the subjective view of John Ousterhout, identifies symptoms such as change amplification, cognitive load and unknown unknowns, and proposes strategic design over tactical programming to control architectural decay.

21CTO
21CTO
21CTO
Why Software Complexity Is Killing Developers and How to Tame It

Introduction

A doctor and a civil engineer argue about the oldest profession, while a software engineer asks who created the chaos, setting the stage for a discussion on software complexity.

What Is Complexity?

Complexity can be measured rationally (McCabe’s cyclomatic complexity) or perceived subjectively (John Ousterhout’s view). McCabe introduced cyclomatic complexity in 1976 to quantify code structure, testability, and maintenance cost.

Rational Metrics

McCabe’s metric classifies code by cyclomatic ranges, linking low complexity to clear structure, high testability, and low maintenance cost, while higher ranges indicate increasing difficulty.

Subjective Perception

Ousterhout defines complexity as anything that makes software hard to understand or modify.

Forms of Complexity

Three main symptoms are identified:

Change Amplification : Simple changes require modifications in many places.

Cognitive Load : The amount of knowledge a developer must have to complete a task.

Unknown Unknowns : Unclear which parts of the code need to change.

Examples include a CRM method that checks capacity and would need changes across many scenarios when business rules evolve.

Why Complexity Arises

Factors such as shortcut solutions, lack of craftsmanship, insufficient technical skill, and poor hand‑over practices contribute to growing complexity.

Inherent Software Complexity

Software complexity is an intrinsic property, not accidental, encompassing domain difficulty, development process challenges, flexibility needs, and discrete system behavior.

Architectural Governance

Software architecture evolves from monoliths to micro‑services, aiming to manage complexity. Three programming paradigms—structured, object‑oriented, and functional—are discussed, along with SOLID principles.

Increasing Complexity

Complexity tends to rise unless actively managed. The article lists three viewpoints: ambiguity creates complexity, dependencies spread it, and incremental changes often hide costs.

Tactical Programming

Fast, short‑term solutions (tactical programming) prioritize speed over long‑term quality, leading to “tactical tornadoes” that accelerate decay.

Strategic Programming

Strategic programming focuses on sustainable design, limiting unnecessary complexity, making incremental improvements, and investing 10‑20% of effort in design.

System Dilemma and Evolution

When a system becomes too complex, strategic redesign becomes difficult, and organizations fall back to tactical fixes.

Architectural Myths

Two common myths are examined: that good code is self‑documenting (a myth) and that elegant code is always the goal, ignoring practical constraints.

Final Thoughts

Software engineering is a lifelong challenge of balancing complexity, elegance, and practicality. References to key books are provided.

References

A Philosophy of Software Design

Object‑Oriented Analysis and Design with Applications

Clean Code

Clean Architecture

Patterns of Enterprise Application Architecture

/**
 * 销售捡入客户
 */
public void pick(String salesId, String customerId) {
  // 查询客户总数
  long customerCnt = customerDao.findCustomerCount(salesId);
  // 查询销售库容
  long capacity = capacityDao.findSalesCapacity(salesId);
  // 判断是否超额
  if(customerCnt >= capacity) {
    throw new BizException("capacity over limit");
  }
  // 代码省略 do customer pick
}
@HSFProvider(serviceInterface = AgnDistributeRuleConfigQueryService.class)
public class AgnDistributeRuleConfigQueryServiceImpl implements AgnDistributeRuleConfigQueryService {
    @Override
    public ResultModel<AgnDistributeRuleConfigDto> queryAgnDistributeRuleConfigById(String id) {
        logger.info("queryAgnDistributeRuleConfigById id=" + id);
        ResultModel<AgnDistributeRuleConfigDto> result = new ResultModel<>();
        if(StringUtils.isBlank(id)){
            result.setSuccess(false);
            result.setErrorMsg("id cannot be blank");
            return result;
        }
        try {
            // business logic ...
        } catch (Exception e) {
            logger.error("queryAgnDistributeRuleConfigById error,", e);
            result.setSuccess(false);
            result.setErrorMsg(e.getMessage());
        }
        return result;
    }
}
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

software designsoftware complexitystrategic programmingtactical programmingarchitectural governance
21CTO
Written by

21CTO

21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.