Fundamentals 22 min read

Why Software Gets Complex and How to Measure & Tame It

This article examines the root causes of software complexity, explains how to quantify it using metrics such as Halstead, cyclomatic and Ousterhout complexity, showcases real code examples, and offers practical strategies—including high cohesion, low coupling, strategic design, documentation, and refactoring—to prevent and reduce complexity in large systems.

Alibaba Cloud Developer
Alibaba Cloud Developer
Alibaba Cloud Developer
Why Software Gets Complex and How to Measure & Tame It

Introduction

The essence of large‑scale systems is complexity; hundreds of micro‑services interact, evolve, and depend on each other, creating a dynamic, hard‑to‑understand environment. Engineers often joke, “when things work, nobody knows why.”

Causes of Software Complexity

Complexity stems from many sources. At a macro level, continuous requirement iteration inevitably accumulates complexity. Key contributors include:

Code rot and the tendency to tolerate it.

Lack of rigorous quality safeguards such as strict code reviews.

Insufficient knowledge‑transfer mechanisms (e.g., missing design documents).

Increasingly intricate requirements that layer on top of existing features.

At the micro level, two main factors dominate:

Dependencies – changes in one module cascade to many others.

Obscurity – code that is hard to understand or locate bugs in.

These manifest as modification ripple effects, cognitive load, and unknown unknowns that increase defect risk.

Software Complexity Metrics

Professor Manny Lehman introduced the concept that software complexity grows exponentially with the number of internal interconnections. Several metrics have been proposed:

Halstead Complexity

Halstead measures operators and operands in a program. The required counts are:

Number of distinct operators.

Number of distinct operands.

Total occurrences of operators.

Total occurrences of operands.

Using these values, various derived measures can be calculated (see the illustration).

Example code snippet and its operator/operand counts are shown in the table below.

@Override
public boolean isAllowed(Long accountId, Long personId, String featureName) {
    boolean isPrivilegeCheckedPass = privilegeCheckService.isAllowed(accountId, personId, featureName);
    return isPrivilegeCheckedPass;
}

Cyclomatic Complexity

Cyclomatic complexity counts linearly independent paths in a control‑flow graph. Values 1‑10 indicate clear code, 10‑20 moderate, 20‑30 high, and >30 very complex and hard to test.

int calculate(int v1, int v2);

Ousterhout’s Complexity Definition

John Ousterhout defines complexity as the cognitive burden and development effort required for a module. The overall system complexity C is the weighted sum of each module’s complexity multiplied by its development‑time weight.

How to Avoid Complexity

Complexity cannot be eliminated entirely, but it can be mitigated through three phases:

Before development: Conduct thorough requirement analysis and produce architectural/design documentation for knowledge transfer.

During development: Emphasize clear layered architecture, high cohesion, low coupling, and comprehensive code comments.

Maintenance: Refactor problematic code, applying strategic design rather than tactical shortcuts.

Strategic programming prioritizes long‑term architecture over short‑term fixes. For example, replacing a tangled series of if‑else branches with a strategy pattern reduces future ripple effects.

public void receiveMessage(Message message, MessageStatus status) {
    // ...
    if (StringUtils.equals(authType, OnetouchChangeTypeParam.IC_INFO_CHANGE.getType())
        || StringUtils.equals(authType, OnetouchChangeTypeParam.SUB_COMPANY_CHANGE.getType())) {
        if (StringUtils.equals("success", authStatus)) {
            oneTouchDomainContext.getOneTouchDomain().getOnetouchEnableChangeDomainService()
                .notifySuccess(userId.toString(), authRequestId);
        }
    } else if (StringUtils.equals(authType, AUTH_TYPE_INTL_CGS_ONSITE)) {
        // ...
    }
    // ...
}

Adopt the principle “keep it simple for callers, hide complexity inside.” This leads to high cohesion and low coupling.

High Cohesion & Low Coupling Design

Design modules that perform a single responsibility (high cohesion) and minimize dependencies on other modules (low coupling). This reduces the spread of changes and eases maintenance.

Encapsulation of Implementation Details

Expose only necessary information through interfaces, keeping internal logic hidden. This improves module cohesion and reduces system coupling.

General‑Purpose Interface Design

When multiple implementations share similar capabilities, extract a common interface and differentiate via business type identifiers.

public List<RightE> getRights(RightQueryParam rightQueryParam) {
    checkParam(rightQueryParam);
    Locale locale = LocaleUtil.getLocale(rightQueryParam.getLocale());
    RightHandler rightHandler = rightHandlerConfig.getRightHandler(rightQueryParam.getMemberType());
    if (rightHandler == null) {
        log.error("getRightHandler error, not found handler, rightQueryParam:{}", rightQueryParam);
        throw new BizException(ErrorCode.NOT_EXIST);
    }
    return rightHandler.getRights(rightQueryParam.getAliId(), locale);
}

Layered Architecture

Separate concerns into distinct layers (e.g., presentation, application, domain, infrastructure). Hexagonal (port‑adapter) and Onion architectures further isolate core business logic from external concerns, preventing “leakage” of infrastructure details into the domain.

Documentation & Refactoring

Comments and documentation are essential knowledge‑transfer tools that aid understanding and future maintenance. Regular refactoring of legacy code improves readability and reduces hidden complexity.

public ReportDetailDto getDetail(ReportQueryParam queryParam) {
    if (queryParam == null) {
        log.error("queryParam is null");
        throw new BizException(PARAM_ERROR);
    }
    Long aliId = queryParam.getAliId();
    if (aliId == null) {
        if (StringUtils.isBlank(queryParam.getToken())) {
            log.error("aliId and token are both null. queryParam: {}", JSON.toJSONString(queryParam));
            throw new BizException(PARAM_ERROR);
        }
        aliId = recommendAssistantServiceAdaptor.getAliIdByToken(queryParam.getToken());
        if (aliId == null) {
            log.error("cannot get aliId by token. queryParam: {}", JSON.toJSONString(queryParam));
            throw new BizException("ALIID_NULL", "aliId is null");
        }
    }
    // Convert and return result
    return convertModel(itemEList);
}

Summary

The article outlines personal reflections on software complexity, analyzes its causes, presents measurement techniques (Halstead, cyclomatic, Ousterhout), and proposes practical ways to avoid complexity through strategic design, high cohesion, low coupling, documentation, and continuous refactoring.

References

System Dilemmas and Software Complexity

A Philosophy of Software Design – https://www.amazon.com/-/zh/dp/173210221X/

Clean Architecture – https://detail.tmall.com/item.htm?id=654392764249

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 designrefactoringcyclomatic complexitysoftware complexitycode metricsHalstead
Alibaba Cloud Developer
Written by

Alibaba Cloud Developer

Alibaba's official tech channel, featuring all of its technology innovations.

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.