language: en kae3g ← back to index

kae3g 9530: Rich Hickey's "Simple Made Easy" - A Design Philosophy

Phase 1: Foundations & Philosophy | Week 2 | Reading Time: 18 minutes

What You'll Learn

Prerequisites

The Talk That Changed How We Think

In 2011, at Strange Loop conference, Rich Hickey gave a talk: "Simple Made Easy".

Impact:

Why it matters: Hickey named something we all felt but couldn't articulate.

Let's unpack it.

The Core Distinction

Simple (from simplex - "one fold")

Definition: Not intertwined. One role. One task. One concept. One dimension.

Objective: You can measure simplicity (count the braids, count the dependencies).

Examples:

Non-examples:

Easy (from adjacens - "near at hand")

Definition: Familiar. Near to our current understanding. Close at hand.

Subjective: What's easy for you might be hard for me (depends on experience).

Examples:

Non-examples (for most people):

The Confusion

We habitually confuse easy with simple.

Example: "JavaScript is simple!"

Actually: JavaScript is easy (familiar, tons of resources), but not simple (complex scoping rules, this binding, coercion rules, prototype chains, async/await + promises + callbacks...).

Example: "Nix is complex!"

Actually: Nix is simple (pure functions, no hidden state, deterministic), but not easy (unfamiliar paradigm, steep learning curve).

The trap:

"This tool is easy to get started with!" 
    ↓
"I'll use it for my project."
    ↓
"Wait, why is this so complicated now?" 
    ↓
(It was easy, not simple—complexity appears later)

Better:

"This tool is unfamiliar (not easy)."
    ↓
"But it's simple (not intertwined)."
    ↓
"I'll invest time to learn it."
    ↓
"Now it's both simple AND easy (to me)!"

Hickey's point: Prefer simple over easy. Easy is temporary (until complexity emerges). Simple is structural.

Complecting: The Root of Complexity

Complect (from complectere - "to braid together"):

To intertwine, entwine, braid together.

Simple: Separate strands (can reason about each independently).
Complex: Braided strands (must understand all to understand any).

Code Example: Complected

class UserManager:
    def __init__(self):
        self.users = []  # State
        self.db = Database()  # Database
        self.logger = Logger()  # Logging
        self.emailer = Emailer()  # Email
    
    def add_user(self, name, email):
        # Complected! Four concerns braided:
        user = {"name": name, "email": email}
        self.users.append(user)  # State management
        self.db.insert(user)  # Persistence
        self.logger.log(f"Added {name}")  # Logging
        self.emailer.send(email, "Welcome!")  # Email

What's complected?

Code Example: Decomplected

;; Separate concerns (loosely coupled)

(defn add-user [users user]
  (conj users user))  ; Just data transformation

(defn persist-user [db user]
  (insert db user))  ; Just persistence

(defn log-event [logger event]
  (write-log logger event))  ; Just logging

(defn send-welcome [emailer email]
  (send-email emailer email "Welcome!"))  ; Just email

;; Compose at call site:
(defn onboard-user [systems user]
  (let [users' (add-user (:users systems) user)]
    (persist-user (:db systems) user)
    (log-event (:logger systems) {:type :user-added :user user})
    (send-welcome (:emailer systems) (:email user))
    (assoc systems :users users')))

What's decomplected?

Trade-off: More functions (looks like more code). But each function is simple (easier to understand, test, modify).

Constructs vs Artifacts

Hickey distinguishes:

Constructs (Things we make)

We can choose how to construct our systems.

Simple constructs:

Complex constructs (complected):

Artifacts (Things we use)

We're stuck with some complexity (can't eliminate):

But: We can isolate artifact complexity. Build simple constructs that manage complex artifacts.

Example:

Dimensions of Simplicity/Complexity

Hickey identifies complecting dimensions:

DimensionSimpleComplex (Complected)
StateValues (immutable)Variables (mutation braided with logic)
OrderQueues, declarativeImperative sequences (step 1 must happen before step 2)
TimeFunctions (timeless)Objects (state changes over time)
IdentityExplicit referencesHidden in this pointers
ModulesNamespaces/packagesObjects (braid structure with behavior)
LogicRules, pure functionsConditionals scattered throughout code
DataGeneric structures (maps, lists)Classes (specific to one use case)

Design strategy: For each dimension, choose the simple option unless complexity is essential (rare!).

Testing Simplicity

How to know if something is simple?

The Questions

1. Can you change one thing without changing another?

;; Simple: change validation without changing persistence
(defn validate [user] ...)  ; Independent
(defn persist [user] ...)   ; Independent

;; Complex: changing validation requires changing UserManager class
class UserManager {
  validate() { ... }
  persist() { ... }  // Tangled with validate via shared state
}

2. Can you understand one part without understanding the whole?

# Simple: understand grep without understanding sort
grep "error" | sort

# Complex: understand method A requires understanding entire class hierarchy
class.methodA()  # Calls super.methodB(), which calls this.methodC()...

3. Can you test one part independently?

;; Simple: test each function alone
(= (add 2 3) 5)  ; No setup needed

;; Complex: test requires mocking entire system
UserManager.add_user("Alice")  
; Needs: mock DB, mock logger, mock emailer, setup state...

If the answer is "no" to any: You have complecting. Refactor to separate concerns.

Achieving Simplicity

Hickey's strategies:

1. Choose Simple Constructs

Prefer:

Example:

;; Simple construct: pure function
(defn calculate-tax [income]
  (* income 0.25))

;; Complex construct: stateful object
class TaxCalculator {
  private config;  // State!
  private history; // State!
  
  calculateTax(income) {
    this.history.push(income);  // Side effect!
    return income * this.config.rate;
  }
}

2. Abstract with Data

Don't create a class for everything. Use generic data structures:

;; Good: use maps
(def user {:name "Alice" :age 30 :role :admin})
(def product {:name "Widget" :price 10 :stock 100})

;; Both are maps—same operations work on both!
(:name user)     ; => "Alice"
(:name product)  ; => "Widget"

;; Bad: create classes (each needs unique methods)
class User { getName() {...} getAge() {...} }
class Product { getName() {...} getPrice() {...} }

Benefits of data:

3. Separate Policy from Mechanism

Mechanism: How something works.
Policy: What it should do.

Example:

;; Mechanism: generic validation function
(defn validate [rules data]
  (every? (fn [[key rule]] (rule (get data key))) rules))

;; Policy: specific rules for users
(def user-rules
  {:age #(>= % 18)
   :email #(re-matches #".+@.+\..+" %)})

;; Compose:
(validate user-rules {:age 30 :email "alice@example.com"})

Mechanism (validate) is reusable. Policy (user-rules) is configurable.

Not complected: Can change policy without changing mechanism.

Real-World Examples

Example 1: Simple vs Complex Build Systems

Complex (Maven):

<!-- pom.xml: 200 lines of XML -->
<project>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>11</source>
          <target>11</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <!-- Another 100 lines -->
  </dependencies>
</project>

What's complected?

Simple (Nix):

{ stdenv, jdk11 }:

stdenv.mkDerivation {
  name = "my-app";
  src = ./.;
  buildInputs = [ jdk11 ];
  buildPhase = "javac *.java";
}

What's simple?

Trade-off: Nix is not easy (unfamiliar). But it's simple (not complected).

Example 2: Simple vs Complex State Management

Complex (React with classes, pre-hooks):

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };  // State
    this.increment = this.increment.bind(this);  // Binding!
  }
  
  increment() {
    this.setState({ count: this.state.count + 1 });  // Mutation!
  }
  
  render() {
    return <button onClick={this.increment}>{this.state.count}</button>;
  }
}

What's complected?

Simple (React with hooks):

function Counter() {
  const [count, setCount] = useState(0);
  
  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

What's simple?

Even simpler (Svelte):

<script>
  let count = 0;
</script>

<button on:click={() => count += 1}>{count}</button>

Simplest: No framework ceremony. Just reactive variables.

The Benefits of Simplicity

1. Easier to Understand

Simple code: Read one function, understand it.

(defn average [numbers]
  (/ (reduce + numbers)
     (count numbers)))

One concept: Sum divided by count. Done.

Complex code: Read one method, must understand entire class.

class Statistics {
  private List<Double> data;
  private boolean sorted = false;
  
  public double average() {
    if (!sorted) sort();  // Side effect!
    // ... more braiding ...
  }
}

Intertwined: Average depends on sorted state, which depends on data, which might be modified by other methods...

2. Easier to Test

Simple:

(deftest test-average
  (is (= (average [1 2 3]) 2)))

;; No setup, no mocking, just call it

Complex:

@Test
public void testAverage() {
  Statistics stats = new Statistics();
  stats.add(1);
  stats.add(2);
  stats.add(3);
  assertEquals(2.0, stats.average(), 0.001);
}

// Setup required, state management, potential for test pollution

3. Easier to Change

Simple: Change one strand, others unaffected.

;; Change validation (doesn't affect persistence)
(defn validate [user]
  (and (>= (:age user) 18)
       (.contains (:email user) "@")))  ; Added email check

;; Persistence unchanged:
(defn persist [db user]
  (insert db user))

Complex: Change one thing, break another.

class UserManager:
    def add_user(self, name, email):
        # Validation braided with persistence
        if len(name) < 3:  # Change this...
            raise ValueError
        self.db.insert({"name": name, "email": email})  # ...might break this

4. More Reliable

Dijkstra:

"Simplicity is prerequisite for reliability."

Why?

Real-world: seL4 microkernel (Essay 9954) is formally verified because it's simple enough to prove correct (~10,000 lines).

Linux kernel (28 million lines): impossible to formally verify (too complex, too many interactions).

Identifying Complexity in Your Code

Warning Signs

1. "To understand this, you need to know..."

If explaining one function requires explaining five others: complected.

2. "This depends on global state X"

Global state braids all code that touches it.

3. "You can't change this without changing that"

Tight coupling = complecting.

4. "The tests are really complicated"

Tests reflect complexity. Simple code = simple tests.

5. "I'm not sure what will happen if..."

Intertwined behavior = unpredictable emergence.

Refactoring Strategy

When you find complecting:

  1. Identify the braids: What concerns are intertwined?
  2. Separate them: Create functions for each concern
  3. Compose explicitly: Make dependencies visible at call site
  4. Test independently: Each function should be testable alone

Example refactor:

# Before (complected)
def process_order(order):
    validate(order)  # Raises exception on error
    charge_card(order.card)  # Side effect!
    update_inventory(order.items)  # Side effect!
    send_email(order.email)  # Side effect!
    return order

# After (decomplected)
def validate_order(order):
    return errors if invalid else None

def process_order(order):
    # Make dependencies explicit, handle errors explicitly
    errors = validate_order(order)
    if errors:
        return {:status :invalid :errors errors}
    
    charge_result = charge_card(order.card)
    inventory_result = update_inventory(order.items)
    email_result = send_email(order.email)
    
    return {:status :success
            :charge charge_result
            :inventory inventory_result
            :email email_result}

Now:

Simple Made Easy (Over Time)

Hickey's crucial insight:

What's unfamiliar (not easy) can become familiar (easy) through learning.

What's complex (complected) doesn't become simple through familiarity.

Example:

Year 0: Haskell is not easy (unfamiliar) and simple (pure functions, no mutation).

Year 1: Haskell is easier (you've learned it) and still simple.

vs

Year 0: Java is easy (familiar) and complex (OOP, inheritance, mutable state).

Year 1: Java is still easy (familiar) but complex (familiarity didn't fix the braiding).

Conclusion: Invest in learning simple tools. The difficulty is temporary. The simplicity is permanent.

Simplicity in the Valley

How we apply this:

1. Prefer Immutability

Mutable state complects value with time.

;; Simple: values don't change
(def v1 {:count 0})
(def v2 (assoc v1 :count 1))

;; v1 and v2 coexist (no timeline complexity)

2. Pure Functions

Side effects complect logic with environment.

;; Simple: no side effects
(defn calculate-total [items]
  (reduce + (map :price items)))

;; Complex: side effects braided with logic
(defn calculate-and-log-total [items]
  (let [total (reduce + (map :price items))]
    (log "Total: " total)  ; Side effect!
    total))

Refactor: Separate calculation from logging.

3. Data Orientation

Objects complect data with behavior.

;; Simple: data + functions
(def user {:name "Alice" :age 30})
(defn adult? [user] (>= (:age user) 18))

;; Complex: data braided with methods
class User {
  private age;
  public boolean isAdult() { return age >= 18; }
}

4. Explicit Over Implicit

Implicit dependencies complect code with hidden context.

;; Simple: explicit
(defn process [db config user]
  ...)  ; Dependencies visible in signature

;; Complex: implicit
(defn process [user]
  ... (use global-db) ...  ; Where did this come from?
  ... (use global-config) ...
  )

Practical Exercises

Exercise 1: Identify Complecting

Review code you've written. Find examples of:

For each: How would you separate them?

Exercise 2: Braid Count

Take a function. Count how many concerns it handles:

def process_user(name, email):
    # 1. Validation
    if len(name) < 3:
        raise ValueError
    
    # 2. Transformation
    user = {"name": name.upper(), "email": email.lower()}
    
    # 3. Persistence
    db.insert(user)
    
    # 4. Logging
    log.info(f"Added {name}")
    
    # 5. Notification
    send_email(email, "Welcome!")
    
    return user

Count: 5 concerns (validation, transformation, persistence, logging, notification).

Braid count: 5.

Target: 1 concern per function (braid count = 1).

Exercise 3: Decomplect Something

Take a complected function (braid count > 1).

Refactor to separate functions:

(defn validate-user [user] ...)
(defn normalize-user [user] ...)
(defn persist-user [db user] ...)
(defn log-user-added [logger user] ...)
(defn send-welcome-email [emailer email] ...)

;; Compose at call site
(defn onboard-user [systems user]
  (when-let [errors (validate-user user)]
    (return {:status :error :errors errors}))
  
  (let [normalized (normalize-user user)]
    (persist-user (:db systems) normalized)
    (log-user-added (:logger systems) normalized)
    (send-welcome-email (:emailer systems) (:email normalized))
    {:status :success :user normalized}))

Result: Each concern is isolated. All dependencies explicit.

The Simplicity Toolkit

Hickey recommends specific simple constructs:

For State

Don't: Mutable variables everywhere
Do: Managed references (Clojure atoms, refs)

;; Explicit state management
(def app-state (atom {:users [] :products []}))

;; Changes are explicit
(swap! app-state update :users conj new-user)

For Polymorphism

Don't: Inheritance hierarchies (complex!)
Do: Protocols/interfaces (simple)

;; Define interface (simple)
(defprotocol Storage
  (save [this data])
  (load [this id]))

;; Implement for different backends (not braided)
(extend-type FileStorage
  Storage
  (save [this data] ...file logic...)
  (load [this id] ...file logic...))

(extend-type DBStorage
  Storage
  (save [this data] ...db logic...)
  (load [this id] ...db logic...))

For Time

Don't: Stateful objects changing over time
Do: Values + transformations

;; Simple: explicit versions
(def user-v1 {:name "Alice" :age 30})
(def user-v2 (assoc user-v1 :age 31))
(def user-v3 (assoc user-v2 :role :admin))

;; Can inspect all versions (time travel!)

Try This

Exercise 1: Watch the Talk

Watch "Simple Made Easy" (1 hour)

Take notes:

Exercise 2: Audit Your Dependencies

List your project's dependencies.

For each, ask:

Example:

Dependency: lodash (utility library, 100+ functions)
- Simple? No (does many things)
- Alternative: Native JS (simpler, but less convenient)
- Complexity essential? Depends (for big apps: maybe. For small: probably not)

Exercise 3: Simplicity Kata

Write a function to validate a user (name, email, age).

Version 1: All concerns in one function (complected).

Version 2: Separate functions for each validation rule (decomplected).

Compare: Which is easier to test? Which is easier to extend?

Going Deeper

Related Essays

External Resources

For the Philosophically Curious

Reflection Questions

  1. What's something you thought was "simple" that's actually just "easy"? (Familiar but complected?)
  2. Can you think of a time when "easy" won over "simple" in a decision? (What was the long-term cost?)
  3. Is it worth learning unfamiliar (not easy) tools if they're simpler? (Investment in understanding vs ongoing complexity cost)
  4. How do you balance "ship now" (choose easy) vs "build right" (choose simple)? (Pragmatism vs idealism)
  5. Can you identify complecting in systems you use daily? (Where are things braided that shouldn't be?)

Summary

Simple vs Easy:

Complecting:

Benefits of Simplicity:

Achieving Simplicity:

Rich Hickey's Gift:

In the Valley:

Next: We'll explore types and sets—the mathematical foundations that let us reason about programs formally. Simplicity meets mathematics!

Navigation:
← Previous: 9520 (functional programming basics) | Phase 1 Index | Next: 9540 (types sets mathematical foundations)

Bridge to Narrative: For Hickey's wisdom embodied, see 9949 (The Wise Elders)!

Metadata:

Copyright © 2025 kae3g | Dual-licensed under Apache-2.0 / MIT
Competitive technology in service of clarity and beauty


← back to index