Clean Architecture Part 1: Design and Programming Paradigms
Robert C. Martin’s Clean Architecture builds on the foundations laid in Clean Code, zooming out from functions and classes to the architecture of entire systems. This first part establishes why architecture matters and explores the three programming paradigms that shape how we design software.
Chapter 1: What Is Design and Architecture?
There Is No Difference
Many people try to distinguish between “design” (low-level details) and “architecture” (high-level structure). Martin argues there is no meaningful distinction—they form a continuous fabric.
“The low-level details and the high-level structure are all part of the same whole. They form a continuous fabric that defines the shape of the system.”
A building’s architecture includes both the grand shape and the placement of electrical outlets. Software is the same.
The Goal
The goal of software architecture is to minimize the human resources required to build and maintain the required system.
The measure of design quality is simply the measure of the effort required to meet the needs of the customer. If that effort is low and stays low throughout the lifetime of the system, the design is good. If it grows with each new release, the design is bad.
The Signature of a Mess
Martin presents data from a real system showing how productivity declines as the codebase grows messier:
| Release | Lines of Code | Developer Productivity |
|---|---|---|
| 1 | Low | High |
| 4 | Medium | Medium |
| 8 | High | Very Low |
Eventually, developers spend most of their time fighting the existing code rather than creating new features. The cost per line of code skyrockets.
The Tortoise and the Hare
Developers often think they can go fast by making messes now and cleaning up later. This never works:
“The only way to go fast, is to go well.”
The race is not to the quick-and-dirty. Making messes is always slower than staying clean. Every developer has experienced the difference between working in clean code versus wading through mud.
What Went Wrong?
The familiar developer overconfidence: “We can clean it up later; we just have to get to market first!” But:
- Market pressure never lets up
- “Later” never comes
- The mess slows everything down
- Productivity approaches zero
The Solution
Take software architecture seriously. Start now. Not next release. Now.
Chapter 2: A Tale of Two Values
Every software system provides two values: behavior and structure.
Behavior
Software must make machines behave in ways that make or save money for stakeholders. Programmers are hired to implement requirements and fix bugs.
Many programmers believe this is their entire job. They are wrong.
Architecture (Structure)
Software must be soft—easy to change. When stakeholders change their minds about features, the change should be simple, proportional to the scope of the change.
If the cost of a change is proportional to the size of the change, the architecture is good. If small changes require disproportionately large effort, the architecture is bad.
The Greater Value
Which is more important—behavior or architecture?
Business managers typically say behavior. Developers often agree. Both are wrong.
Consider these extremes:
- A program that works perfectly but is impossible to change becomes useless when requirements change
- A program that doesn’t work but is easy to change can be made to work
The second is more valuable because requirements always change.
Eisenhower’s Matrix
President Eisenhower had a matrix of importance and urgency:
| Urgent | Not Urgent | |
|---|---|---|
| Important | Do first | Schedule |
| Not Important | Delegate | Eliminate |
- Behavior is urgent but not always important
- Architecture is important but rarely urgent
The mistake is letting urgent behavior crowd out important architecture. Business managers are not equipped to evaluate architecture importance—that’s the developers’ responsibility.
Fight for the Architecture
Developers must fight for what they believe is best for the company. That’s part of the job, just like marketing fights for marketing concerns and finance fights for financial concerns.
The development team must advocate for architecture because no one else will.
Chapter 3: Paradigm Overview
We have had three paradigms since programming began, and there are unlikely to be any more.
Structured Programming
Discovered by: Edsger Wybe Dijkstra (1968)
What it removes: Unrestrained goto statements
Dijkstra showed that programs could be constructed from three structures: sequence, selection (if/then/else), and iteration (loops). These can replace goto entirely.
Structured programming enables functional decomposition—breaking problems into smaller, provable units.
Object-Oriented Programming
Discovered by: Ole Johan Dahl and Kristen Nygaard (1966, Simula)
What it removes: Function pointers (unsafe polymorphism)
OO provides a disciplined way to achieve polymorphism—the ability for different objects to respond to the same message differently.
Before OO, function pointers allowed polymorphism but were dangerous and difficult to use consistently. OO makes polymorphism safe and convenient.
Functional Programming
Discovered by: Alonzo Church (1936, lambda calculus)
What it removes: Assignment
Functional programming is based on immutability. Variables don’t vary—once set, they never change.
This has profound implications for concurrency. All race conditions, deadlocks, and concurrent update problems are due to mutable variables. No mutation = no concurrency problems.
Key Insight
Each paradigm removes capabilities. None adds new capabilities.
- Structured programming removes
goto - Object-oriented programming removes function pointers
- Functional programming removes assignment
“Each paradigm tells us what NOT to do, more than what TO do.”
These three paradigms align with the three big concerns of architecture: function, separation of components, and data management.
Chapter 4: Structured Programming
Proof
Dijkstra applied mathematical proof techniques to programming. He discovered that certain uses of goto prevent programs from being decomposed into smaller, provable units.
Other uses of goto correspond to:
- Sequence (simple statements in order)
- Selection (if/then/else)
- Iteration (while loops)
These three structures are sufficient to build any program and allow recursive decomposition into provable units.
The Böhm-Jacopini Theorem
Mathematicians Böhm and Jacopini proved that all programs can be constructed from sequence, selection, and iteration. This is the theoretical foundation of structured programming.
Tests
In practice, we don’t prove programs mathematically. Instead, we test them.
Testing cannot prove correctness—it can only prove incorrectness. A test that fails proves a bug exists. A test that passes proves nothing about untested paths.
“Testing shows the presence, not the absence, of bugs.” — Dijkstra
Science vs. Math
Mathematics is about proving statements true. Science is about proving statements false.
Software is like science—we cannot prove programs correct; we can only fail to prove them incorrect. Programs that survive sufficient testing are “good enough.”
The Role of Structured Programming
Structured programming forces us to recursively decompose programs into small, testable functions. We can then apply tests to try to prove those functions incorrect. If we fail to prove them incorrect, we deem them correct enough for our purposes.
Structured programming enables testability.
Chapter 5: Object-Oriented Programming
What is OO? Many definitions exist: “combination of data and function,” “a way to model the real world,” or “encapsulation, inheritance, polymorphism.”
Encapsulation
OO languages provide easy and effective encapsulation of data and function. But:
- C had perfect encapsulation via header files and implementation files
- C++ and Java actually weakened encapsulation by requiring member variables in headers/class declarations
Encapsulation is not what makes OO special.
Inheritance
OO languages made inheritance convenient. But:
- Inheritance was possible in C through careful struct layouts
- OO just made it safer and more convenient
Inheritance is nice but not the essence of OO.
Polymorphism
This is the key. OO makes polymorphism trivial to use safely.
Before OO, polymorphism required explicit function pointers:
// Pre-OO polymorphism
struct FILE {
void (*open)(char* name);
void (*close)();
int (*read)();
};
// Dangerous: calling through wrong pointer crashes
With OO:
// OO polymorphism
interface Reader {
void open(String name);
void close();
int read();
}
// Safe: compiler enforces interface
The Power of Polymorphism
Consider a program that must work with different devices: printers, screens, files. Without polymorphism, you need explicit switches:
void output(Device* dev, char* data) {
switch(dev->type) {
case PRINTER: print(data); break;
case SCREEN: display(data); break;
case FILE: write(data); break;
}
}
Every new device type requires modifying this switch. Every function that handles devices needs similar switches.
With polymorphism:
interface OutputDevice {
void output(String data);
}
// Main code never changes
void output(OutputDevice dev, String data) {
dev.output(data);
}
New device types are added, not modified.
Dependency Inversion
This is the most important implication of polymorphism.
Traditional dependency: Source code dependencies follow flow of control. High-level modules depend on low-level modules.
UI → Business Logic → Database
With polymorphism: Source code dependencies can point against the flow of control.
UI → BusinessInterface ← Business Logic → DBInterface ← Database
The database implements an interface defined by business logic. Business logic no longer depends on database—database depends on business logic!
This is Dependency Inversion. It gives architects enormous power to control dependencies independent of how data flows at runtime.
The OO Summary
OO is the ability to control the direction of source code dependencies using polymorphism. This enables:
- Plugin architecture: Low-level details plug into high-level policies
- Independent deployability: Components can be compiled and deployed separately
- Independent developability: Teams can work in parallel
Chapter 6: Functional Programming
Squares of Integers
Compare these two implementations:
Java (mutable):
public class Squint {
public static void main(String args[]) {
for (int i = 0; i < 25; i++) {
System.out.println(i * i);
}
}
}
Clojure (immutable):
(println (take 25 (map #(* % %) (range))))
The Clojure version has no assignment. No variable changes state. The range function produces an infinite lazy sequence; map transforms it; take limits it; println outputs it.
Immutability and Architecture
All race conditions, deadlocks, and concurrent update problems are due to mutable variables.
No mutation = no concurrency problems.
If you have infinite storage and processor speed, you could make all variables immutable. We don’t have infinite resources, so we make compromises:
Segregation of Mutability
Separate components into mutable and immutable parts. Push as much processing as possible into immutable components:
[Immutable Components] ←→ [Mutable Components with Transactional Memory]
Use transactional memory or other protection for the mutable parts.
Event Sourcing
Instead of storing state, store transactions (events).
Instead of:
Account Balance: $500
Store:
Deposit $100
Deposit $200
Withdraw $50
Deposit $250
To get the balance, replay all transactions. This eliminates concurrent update problems—nothing is ever updated or deleted, only created.
This requires a lot of storage and processing, but storage is cheap and getting cheaper. Many systems (version control, banking audit trails) already work this way.
Functional Programming Summary
- Variables in functional languages do not vary
- Race conditions cannot happen without mutable variables
- Architects should push as much processing into immutable components as possible
- Event sourcing is an example of eliminating mutable state
Key Takeaways
-
Architecture and design are inseparable. They form a continuous fabric from high-level structure to low-level details.
-
The goal is minimizing effort. Good architecture keeps the cost of change low throughout the system’s lifetime.
-
Structure > Behavior in the long run. A system that can be changed is more valuable than one that works but cannot be modified.
-
Three paradigms, three constraints:
- Structured: No unrestrained goto → enables testability
- OO: Safe polymorphism → enables dependency inversion
- Functional: No mutation → enables concurrency safety
-
Polymorphism is OO’s real contribution. It allows you to invert source code dependencies, creating plugin architectures where details depend on policies.
-
Immutability prevents concurrency bugs. Segregate mutable state and minimize it.