Be Software Architecture Wise
Be Wise Apps

Leer en español · Lire en français · Auf Deutsch lesen

Section 3.8 of 3.8 · Intermediate · 25 min read

Simplicity, reversibility and measuring structure

Video lesson

5 min · narrated, with captions
Listen only (audio)
Read the transcript

This last lesson of the chapter is about keeping systems as simple as the problem allows, and measuring structure without fooling yourself.

Some things are hard because the job is hard. A tax return is complicated because tax law is. Other things are hard because of how we chose to do them, like a filing system with seven folders where two would do. Good designers accept the first kind and fight the second. They build what's needed now, keep each fact written down in one place, avoid choices they can't undo, and check their work with a few honest measurements.

Fred Brooks, in his essay No Silver Bullet, separated these two kinds. Essential complexity belongs to the problem: the real rules of VAT, the real ways a delivery can fail. No design can remove it without changing what the system does. Accidental complexity comes from how we build the solution: tools, workarounds, and structure more complicated than the problem needs. Architects place the essential kind behind a clear boundary, such as a tax service, and remove the accidental kind.

Three maxims help. The first, known as KISS, says keep it simple. A design is good if the people who must repair it, perhaps at three in the morning, can understand it. You aren't gonna need it, or YAGNI, warns against building for needs you only presume. You pay to build it, to carry it through every later change, and to repair it if you guessed wrong. But YAGNI applies to features, not to the practices that make change cheap, such as tests and refactoring. And some decisions, such as a data model or a public API, are one-way doors. Thinking ahead there is the architect's job. Don't repeat yourself, or DRY, is about knowledge, not text. Each rule, such as the VAT rate, needs one authoritative home. But merging two functions just because they look alike couples concepts that will change for different reasons. Finally, flexibility is like a financial option. Pay for it when the change is likely, or very expensive without it, and the option is cheap. For the rest, rely on good modularity.

Now, measurement. The oldest widely used code metric is cyclomatic complexity, from Thomas McCabe in 1976. It treats a function as a graph: blocks of code are nodes, and possible jumps between them are edges. This one works out a delivery fee. It has three decisions: is the basket total twenty five pounds or more, is the distance over five, and is it a priority order? There are eight nodes and ten edges. Cyclomatic complexity is edges minus nodes plus two: ten minus eight plus two, which is four. That matches a quick check: three decisions, plus one. Four means four independent paths, so at least four test cases to try each one. McCabe suggested about ten as a reasonable upper limit.

A year later, the function has gained a loop that adds a surcharge for bulky items, and the priority check also asks whether a rider is available. That's the original three decisions, plus the loop, the bulky item check, and the extra and, if your tool counts it: six. So complexity is seven. That's under McCabe's limit, but it has nearly doubled. Testing every path now needs at least seven cases, not four. The code now mixes three pricing concerns, changed by different people. So extract each surcharge into its own small function. Total complexity is similar, but each piece can be tested on its own.

Architects also measure how modules relate. The coupling and stability metrics from Section three point four reveal dependency direction and cycles. Cohesion metrics estimate whether a class holds one responsibility or several. A customer class whose methods fall into three groups that share no fields is really three classes sharing a file. Version control reveals change coupling. If two files change together in seventy per cent of commits, they're coupled, whether or not either imports the other. And hotspots combine change frequency with complexity, to find code that's both hard to work with and often worked on.

But beware Goodhart's law: when a measure becomes a target, it ceases to be a good measure. A hard complexity limit of ten produces functions split at arbitrary points, and logic that's harder to follow. So professionals watch trends rather than thresholds, use metrics to start conversations rather than to grade people, and combine metrics that pull in different directions. Where a structural rule is clear, such as no dependency cycles, they make it a fitness function: an automated check that fails the build, forcing a conscious choice to fix the code or change the rule.

The full section ends with a checklist for reviewing any module's design. That completes Chapter 3. When you're ready, try the optional chapter test, then continue to Chapter 4, on documenting and communicating architecture. Well done.

What you need to know

Two kinds of complexity

In "No Silver Bullet: Essence and Accidents of Software Engineering" (presented at the IFIP World Computer Congress in 1986 and published in IEEE Computer in 1987), Fred Brooks separated the difficulties of building software into two kinds. Essential complexity belongs to the problem: the real rules of VAT, the real variety of insurance products, the real ways a delivery can fail. No design can remove it without changing what the system does. Accidental complexity comes from how we build the solution: languages, tools, frameworks, deployment machinery, workarounds, and structure that is more complicated than the problem requires. Brooks argued that high-level languages and other advances had already removed much accidental difficulty, so no single new technique would deliver a tenfold improvement within a decade.

For architects the distinction is a daily tool. Essential complexity must be placed: put it where the people who understand it can work on it, behind a boundary that keeps it from leaking into everything else (a pricing module, a tax service). Accidental complexity must be removed or prevented: every extra service, queue, cache, abstraction layer and configuration option adds some, and each must earn its place by solving a real problem. Ben Moseley and Peter Marks's essay "Out of the Tar Pit" (2006) pushed the idea further, identifying mutable state and control flow as major sources of accidental complexity, which is one reason functional and event-driven designs that limit shared state are popular.

The rest of this section covers the principles that keep accidental complexity down (KISS, YAGNI, DRY understood correctly, reversibility) and the measurements that help you see structure objectively, together with their limits.

KISS and YAGNI

KISS, usually expanded as "keep it simple, stupid", is a design maxim recorded in the US Navy in 1960 and associated with Kelly Johnson, the lead engineer of Lockheed's Skunk Works, who is said to have demanded that jet aircraft be repairable in the field by an average mechanic with a handful of tools. The software reading is the same: a design is good if the people who must operate and repair it, often at 3 a.m. under pressure, can understand it. Simple does not mean easy or naive. It means few concepts, few moving parts and few special cases for the problem being solved.

YAGNI ("you aren't gonna need it") comes from Extreme Programming in the late 1990s: do not build capabilities for needs you only presume. Martin Fowler's analysis is useful because it names the costs of ignoring it. There is the cost of building the presumptive feature; the cost of delay, because something valuable was not built instead; the cost of carrying it, because every later change must work around the extra code; and, if the guess was wrong, the cost of repair. Even when the guess turns out right, the feature was built with less knowledge than you would have had later.

Two qualifications keep YAGNI from becoming an excuse. First, it applies to features and speculative flexibility, not to the practices that make change cheap. Automated tests, refactoring, continuous delivery and clear module boundaries are what make it safe to defer features; dropping them in the name of YAGNI turns deferral into debt. Second, some decisions are one-way doors (Section 1.6). Choosing a data model, a public API shape, a tenancy model or a security boundary is not a feature you can add later at the same cost. For those, thinking ahead is not speculation; it is the architect's job.

DRY means knowledge, not text

Andy Hunt and Dave Thomas introduced DRY in The Pragmatic Programmer (1999; 20th anniversary edition 2019) with the definition: Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. The key word is knowledge. If the VAT rate, the rule for a valid postcode, or the meaning of an order status is written in several places, then changing it requires finding every copy, and the copies will drift. The 2019 edition stresses that DRY is about duplication of knowledge and intent, not about lines of code that happen to look the same.

The common misuse runs the other way. Two functions that look alike are merged because they look alike, even though they represent different knowledge that will change for different reasons: the delivery-fee rule and the restaurant-commission rule both multiply a total by a percentage today, but they belong to different actors (finance and partnerships) and will diverge. Merging them couples two concepts that the domain keeps apart: both now depend on one shared function, so a change requested for one silently changes the other. In the terms of Section 3.6, it creates connascence where the problem has none (sharing one function is the right cure only when both sides genuinely must compute the same thing); it is also a single-responsibility violation (Section 3.5). Sandi Metz's well-known advice is to prefer duplication to the wrong abstraction, because a wrong shared abstraction accumulates parameters and conditionals until nobody dares change it. Many teams use the rule of three: tolerate a second copy, and extract an abstraction when the third appears and the shared knowledge is clear.

At architecture scale the trade-off becomes explicit. A shared library that every service must use removes duplicated code but couples the services' release cycles. Many microservice organisations deliberately accept duplicated code (small utilities copied into each service) while insisting on a single owner for duplicated knowledge (the canonical customer record, the tax rules). Ask of any duplication: if this rule changes, must all copies change together? If yes, it is duplicated knowledge and should have one owner. If no, it is coincidental similarity and can stay.

Reversibility and the cost of options

Simplicity has a time dimension. A design that is simple today but impossible to change is not cheap; a design full of flexibility nobody uses is not cheap either. The useful concept is reversibility: how expensive would it be to undo or change this decision later? Section 1.6 introduced one-way and two-way doors. Here the point is that flexibility behaves like a financial option: it has a price now (extra code, extra abstraction, extra operational cost) and a value that depends on how likely and how expensive the change it protects against is.

This gives a rule of thumb that reconciles YAGNI with forward thinking. Pay for an option only when the change is reasonably likely, or when it would be very expensive without the option, and when the option itself is cheap. Wrapping a payment provider behind an interface you own costs a day and protects against a change that is both plausible and expensive: a good option. Building a multi-cloud abstraction layer for a start-up with one customer is an expensive option against an unlikely change. Often the cheapest option is simply good modularity: well-placed boundaries keep many future changes local without anyone having to predict them, which is Parnas's original argument for information hiding (Section 3.1).

Measuring structure: cyclomatic complexity

Judgement about simplicity improves when it is informed by measurement. The oldest widely used code metric is cyclomatic complexity, introduced by Thomas McCabe in "A Complexity Measure" (IEEE Transactions on Software Engineering, 1976). It treats a function as a control-flow graph, with nodes for blocks of code and edges for possible transfers of control, and counts the linearly independent paths through it: M = E − N + 2P, where E is the number of edges, N the number of nodes and P the number of connected components (1 for a single function). For structured code with one entry point this equals the number of decision points plus one.

McCabe suggested 10 as a reasonable upper limit for a module. The later NIST structured testing report by Arthur Watson and McCabe (1996) notes that limits as high as 15 have been used successfully by experienced teams, and recommends a written explanation whenever a module exceeds the chosen limit. Static analysis tools such as radon or the mccabe plugin for flake8 in Python, ESLint's complexity rule in JavaScript and SonarQube compute it automatically. Tools differ in details, for example whether each and or or in a condition counts as an extra decision, so compare numbers from the same tool.

def delivery_fee(total: int, distance_km: float, priority: bool) -> int:
    if total >= 2500:          # decision 1: free delivery over 25 pounds
        return 0
    fee = 299
    if distance_km > 5:        # decision 2
        fee += 100
    if priority:               # decision 3
        fee += 200
    return fee
Three decisions, so cyclomatic complexity 4.

The control-flow graph and the count

The control-flow graph of a delivery-fee function with three decisions, giving a cyclomatic complexity of fourtotal >= 2500?return 0distance > 5?fee += 100priority?fee += 200return feeexityesnoyesnoyesnoCountingNodes N = 8Edges E = 10M = E − N + 2M = 10 − 8 + 2 = 4Check: 3 decisions + 1 = 44 independent pathsso at least 4 test casesfor basis-path testing
Eight nodes and ten edges give M = 10 − 8 + 2 = 4, matching three decisions plus one.

Worked example

Worked example: when the function grows

A year later delivery_fee has gained a loop over the basket and a compound condition. Count its complexity and decide what to do.

New codefor item in items: with if item.bulky: fee += 50 inside, and if priority and rider_available: fee += 200 replacing if priority.
DecisionsOriginal 3, plus the for loop (1), plus the if item.bulky (1), plus the and (1, if the tool counts boolean operators): 6.
Cyclomatic complexity6 + 1 = 7. Below McCabe's limit of 10, but it has nearly doubled.
Testing implicationBasis-path testing needs at least 7 test cases to exercise each independent path once; the old suite of 4 no longer covers every path.
Design readingThe function now mixes three pricing concerns: basket-size threshold, distance and item surcharges, and priority. They change at different requests (marketing, operations, product).
RefactoringExtract distance_surcharge, bulky_surcharge and priority_surcharge, each with complexity 2 or 3, and let delivery_fee sum them. Total complexity is similar, but each piece is small, named and testable on its own.

Refactoring rarely lowers total complexity; it redistributes it into pieces that are easier to understand, test and change independently. The essential complexity of the pricing rules remains.

The metric flagged a trend worth a conversation; the decision to split was made by reading the code and asking who changes it.

Measuring structure beyond functions

Cyclomatic complexity sees one function at a time. Architects need measures of how modules relate.

Coupling and stability metrics. Section 3.4 (managing dependencies) introduced afferent and efferent coupling, instability I = Ce / (Ca + Ce), abstractness and distance from the main sequence. These are cheap to compute from import graphs and are good at revealing components that are depended on by many yet depend on volatile code, and at catching dependency cycles.

Cohesion metrics. The LCOM (lack of cohesion of methods) family estimates whether a class holds one responsibility or several. Shyam Chidamber and Chris Kemerer's original version (1994) compares pairs of methods by whether they use any of the same fields. A widely used later variant, LCOM4 by Martin Hitz and Behzad Montazeri, builds a graph in which methods are connected if they share a field or one calls the other, and counts the connected groups. A class with LCOM4 = 1 hangs together; a class with LCOM4 = 3 is three classes sharing a file. For example, a Customer class whose update_address and format_address use the address fields, whose add_points and points_balance use the loyalty fields, and whose send_newsletter uses only the email field has LCOM4 = 3, suggesting address, loyalty and marketing responsibilities.

History-based metrics. Version control records how the system actually changes, which static analysis cannot see. Change coupling finds files or services that are repeatedly committed together: if pricing.py and invoice_template.html change together in 70% of commits, they are coupled whether or not either imports the other, and the coupling may cross a service boundary that was meant to separate them. Hotspots combine change frequency with complexity to find code that is both hard to work with and frequently worked on; Adam Tornhill's Your Code as a Crime Scene (2015; second edition 2024) popularised the technique. Section 1.7 on technical debt showed how hotspots direct repayment to the debt that charges the most interest.

Structural metrics at a glance

MetricScopeWhat it revealsMain blind spot
Cyclomatic complexityFunctionBranching logic that is hard to test and reason aboutSays nothing about naming, data or how functions relate
Ca, Ce, instability, distancePackage or componentDependency direction, stability, cyclesCounts dependencies, not their strength or meaning
LCOM (for example LCOM4)ClassClasses holding several unrelated responsibilitiesSensitive to accessors and language features; noisy on small classes
Change couplingFile, module or serviceHidden coupling that only shows in how the system changesNeeds enough history; bulk commits create noise
HotspotsFile or moduleWhere improvement pays back fastestPrioritises; does not say what is wrong
Connascence (judged by review)AnyThe form and strength of each dependencyLargely manual; few tools measure the dynamic forms

Goodhart's law and the limits of metrics

Every metric above is a proxy for something we care about but cannot measure directly: how easy the system is to understand and change. Goodhart's law, named after the economist Charles Goodhart (1975) and best known in the anthropologist Marilyn Strathern's 1997 phrasing, warns that when a measure becomes a target, it ceases to be a good measure. People optimise the number, not the property.

Software teams see this constantly. A hard limit of cyclomatic complexity 10 produces functions split at arbitrary points, each passing the check while the logic becomes harder to follow. A coverage target of 80% produces tests that execute code without asserting anything. A rule that no class may exceed 200 lines produces classes split into OrderHelper and OrderHelper2. A target of "zero cycles" can be met by moving code into a dumping-ground common package that everything depends on.

Professionals use metrics in three ways that resist gaming. They look at trends rather than absolute thresholds (complexity in the payments module has doubled in six months: why?). They use metrics to start conversations and prioritise reviews, not to grade people. And they combine metrics that pull in different directions, such as complexity with change frequency, or coupling with team ownership, so that no single number can be optimised at the expense of the others.

Fitness functions: making structure checkable

Some structural rules are clear enough to automate, and then they stop depending on vigilance. Neal Ford, Rebecca Parsons and Patrick Kua's Building Evolutionary Architectures (2017; second edition 2022 with Pramod Sadalage) calls such checks fitness functions: objective, automated tests of an architectural characteristic that run in the build pipeline or against the running system. Examples for structure include: no dependency cycles between top-level packages; the domain package imports nothing from the web or database packages; no function above complexity 15 without an explicit exemption; no service reads another service's database tables.

Tools such as ArchUnit (Java), import-linter (Python) and dependency-cruiser (JavaScript and TypeScript) express these rules as code; Section 3.4 gave examples. The difference from a dashboard metric is that a fitness function encodes a decision the team has already made, so breaking it fails the build and forces a conscious choice: fix the code or change the rule and record why. The full treatment, including fitness functions for performance, security and other qualities, is in Fitness functions and evolutionary architecture.

[importlinter]
root_package = shop

[importlinter:contract:domain-is-pure]
name = Domain must not depend on infrastructure
type = forbidden
source_modules =
    shop.domain
forbidden_modules =
    shop.infrastructure
    shop.web
An import-linter contract in setup.cfg or .importlinter that fails the build if the domain layer imports infrastructure.

Checklist: reviewing a module's design

Use this when reviewing a new module, component or service, or a significant change to one. It draws on every section of this chapter.

  • Can you state its responsibility in one sentence, and name the actor who asks for its changes?If the sentence needs "and", or several actors change it, consider splitting (modularity, SRP, CCP).
  • Does it hide a design decision that is likely to change?A module should own a secret, such as a data format, a vendor or an algorithm (information hiding).
  • Is the interface small relative to what it hides?Deep modules hide much behind little; pass-through layers add cost without hiding anything.
  • Do its dependencies point towards more stable, more abstract modules?Check for cycles and for stable modules that depend on volatile ones (ADP, SDP, DIP).
  • What does a client need to know to use it correctly?Look for positional parameters, magic values, required call orders and timing assumptions; weaken them or keep them local (connascence).
  • Who owns it, and can that team change and deploy it without coordinating with others?One owning team per module; cross-team interfaces should be explicit and versioned (Conway's law).
  • Is any of its complexity accidental?Question each extra layer, option and generic mechanism: what real requirement does it serve (KISS, YAGNI)?
  • Is any knowledge duplicated, or any coincidental similarity wrongly shared?Duplicated knowledge needs one owner; similar-looking code with different owners can stay separate (DRY).
  • Which decisions in it are hard to reverse, and are they deliberate?Public API shapes, data models and boundaries deserve more scrutiny than internal details.
  • What do the metrics and history say?Check complexity trends, coupling metrics, change coupling and hotspots, and add a fitness function for any rule worth keeping.

Check your understanding

A function has two if statements and one while loop and no boolean operators. What is its cyclomatic complexity?

Three decisions plus one, so 4.

Two services each contain an identical 20-line function for formatting dates. Is that a DRY violation?

Probably not. It is duplicated text rather than duplicated business knowledge, and sharing it would couple the services' releases; duplicated business rules, such as tax calculation, are a different matter.

Why does YAGNI not apply to writing automated tests?

Tests are part of what makes future change cheap and safe; YAGNI is about not building speculative features and flexibility, and it relies on such practices to work.

A team is told that no function may exceed complexity 10, and complexity falls while defects rise. What has probably happened?

Goodhart's law: functions were split mechanically to satisfy the metric, making the logic harder to follow without making it simpler.

End of Chapter 3Take the chapter testOptional: 20 multiple-choice questions and 2 written answers, in the app.Go to the test →

Sources and further reading

Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.

Ask: “Our team wants to enforce a maximum cyclomatic complexity and minimum test coverage in CI. How do we use these metrics without people gaming them?”