KoderKoder.ai
PricingEnterpriseEducationFor investors
Log inGet started

Product

PricingEnterpriseFor investors

Resources

Contact usSupportEducationBlog

Legal

Privacy PolicyTerms of UseSecurityAcceptable Use PolicyReport Abuse

Social

LinkedInTwitter
Koder.ai
Language

© 2026 Koder.ai. All rights reserved.

Home›Blog›Rich Hickey & Clojure: Simplicity, Immutability, Better Defaults
Jun 27, 2025·8 min

Rich Hickey & Clojure: Simplicity, Immutability, Better Defaults

An accessible look at Rich Hickey’s Clojure ideas: simplicity, immutability, and better defaults—practical lessons for building calmer, safer complex systems.

Rich Hickey & Clojure: Simplicity, Immutability, Better Defaults

Why complexity keeps winning in real projects

Software rarely becomes complicated all at once. It gets there one “reasonable” decision at a time: a quick cache to hit a deadline, a shared mutable object to avoid copying, an exception to the rules because “this one is special.” Each choice looks small, but together they create a system where changes feel risky, bugs are hard to reproduce, and adding features starts taking longer than building them.

Complexity wins because it offers short-term comfort. It’s often faster to wire in a new dependency than to simplify an existing one. It’s easier to patch state than to ask why state is spread across five services. And it’s tempting to rely on conventions and tribal knowledge when the system grows faster than the documentation.

What this article is (and isn’t)

This isn’t a Clojure tutorial, and you don’t need to know Clojure to get value from it. The goal is to borrow a set of practical ideas often associated with Rich Hickey’s work—ideas you can apply to everyday engineering decisions, regardless of language.

Why defaults matter more than you think

Most complexity isn’t created by the code you write deliberately; it’s created by what your tools make easy by default. If the default is “mutable objects everywhere,” you’ll end up with hidden coupling. If the default is “state lives in memory,” you’ll struggle with debugging and traceability. Defaults shape habits, and habits shape systems.

We’ll focus on three themes:

  • Simplicity: not fewer features, but fewer moving parts and fewer special cases.
  • Immutability: treating data as values that don’t change, so you can reason about them.
  • Better defaults: choosing patterns that make the safe, predictable option the easiest one.

These ideas don’t remove complexity from your domain, but they can stop your software from multiplying it.

Rich Hickey and what Clojure set out to fix

Rich Hickey is a long-time software developer and designer best known for creating Clojure and for talks that challenge common programming habits. His focus isn’t trend-chasing—it’s the recurring reasons systems become hard to change, hard to reason about, and hard to trust once they grow.

What Clojure is (high level, no jargon)

Clojure is a modern programming language that runs on well-known platforms like the JVM (Java’s runtime) and JavaScript. It’s designed to work with existing ecosystems while encouraging a specific style: represent information as plain data, prefer values that don’t change, and keep “what happened” separate from “what you show on screen.”

You can think of it as a language that nudges you toward clearer building blocks and away from hidden side effects.

The problems it was designed to reduce

Clojure wasn’t created to make small scripts shorter. It was aimed at recurring project pain:

  • Growing complexity from shared state: when many parts of a system can modify the same data, bugs become timing-dependent and difficult to reproduce.
  • Tight coupling between data and behavior: when information is locked inside objects or classes, reuse gets harder and change ripples through the codebase.
  • Concurrency headaches: as systems add background jobs, queues, and parallel work, “who changed what, when?” becomes a daily problem.

Clojure’s defaults push toward fewer moving parts: stable data structures, explicit updates, and tools that make coordination safer.

Useful even if you never adopt Clojure

The value isn’t limited to switching languages. Hickey’s core ideas—simplify by removing needless interdependencies, treat data as durable facts, and minimize mutable state—can improve systems in Java, Python, JavaScript, and beyond.

Simplicity: not “easy,” but fewer moving parts

Rich Hickey draws a sharp line between simple and easy—and it’s a line most projects cross without noticing.

Simple vs. easy (with everyday examples)

Easy is about how something feels right now. Simple is about how many parts it has and how tightly they’re entangled.

  • Instant noodles are easy. A basic stew can be simple: a few ingredients, one pot, nothing hidden.
  • A remote with 60 buttons might make one feature “easy” (it’s right there), but it isn’t simple. A remote with 6 clear controls is simpler, even if it takes a minute to learn.

In software, “easy” often means “quick to type today,” while “simple” means “harder to break next month.”

How “easy now” creates future complexity

Teams often choose shortcuts that reduce immediate friction but add invisible structure that must be maintained:

  • “Let’s just add a flag.” Now every feature needs to consider that flag.
  • “We’ll store the computed value to save time.” Now you must keep it in sync across code paths.
  • “We can patch it in the UI.” Now the same business rule exists in multiple places.

Each choice may feel like speed, but it increases the number of moving parts, special cases, and cross-dependencies. That’s how systems become fragile without any single dramatic mistake.

Speed isn’t the same as simplicity

Shipping fast can be great—but speed without simplifying usually means you’re borrowing against the future. The interest shows up as bugs that are hard to reproduce, onboarding that drags, and changes that require “careful coordination.”

A quick checklist for accidental complexity

Ask these questions when reviewing a design or PR:

  • Did we introduce new modes, flags, or configuration branches?
  • Are we caching or duplicating data that must be kept consistent?
  • Do multiple modules need to change together for one behavior?
  • Is the rule implemented in more than one place?
  • Would a new teammate predict how this works without extra explanation?

State: the silent multiplier of complexity

“State” is simply the stuff in your system that can change: a user’s shopping cart, an account balance, the current configuration, what step a workflow is on. The tricky part isn’t that change exists—it’s that every change creates a new opportunity for things to disagree.

When people say “state causes bugs,” they usually mean this: if the same piece of information can be different at different times (or in different places), then your code has to constantly answer, “Which version is the real one right now?” Getting that answer wrong produces errors that feel random.

Mutability: change you can’t unsee

Mutability means an object is edited in place: the “same” thing becomes different over time. That sounds efficient, but it makes reasoning harder because you can’t rely on what you saw a moment ago.

A relatable example is a shared spreadsheet or document. If multiple people can edit the same cells at the same time, your understanding can be invalidated instantly: totals change, formulas break, or a row disappears because someone reorganized it. Even if nobody is doing anything malicious, the shared, editable nature is what creates confusion.

Software state behaves the same way. If two parts of a system read the same mutable value, one part can silently change it while the other continues with an outdated assumption.

Why debugging gets so painful

Mutable state turns debugging into archaeology. A bug report rarely tells you “the data was changed incorrectly at 10:14:03.” You just see the end result: a wrong number, an unexpected status, a request that fails only sometimes.

Because state changes over time, the most important question becomes: what sequence of edits led here? If you can’t reconstruct that history, behavior becomes unpredictable:

  • The same action produces different outcomes depending on timing.
  • Fixes “work on my machine” but not in production.
  • Adding logging changes the timing and the bug disappears.

This is why Hickey treats state as a complexity multiplier: once data is both shared and mutable, the number of possible interactions grows faster than your ability to keep them straight.

Immutability explained without computer-science jargon

Immutability simply means data that doesn’t change after it’s created. Instead of taking an existing piece of information and editing it in place, you create a new piece of information that reflects the update.

Think of a receipt: once printed, you don’t erase line items and rewrite totals. If something changes, you issue a corrected receipt. The old one still exists, and the new one is clearly “the latest version.”

Why it reduces surprises

When data can’t be quietly modified, you stop worrying about invisible edits happening behind your back. That makes everyday reasoning much easier:

  • If you hold a value, you can trust it will stay that way.
  • Bugs become easier to reproduce because the same input stays the same.
  • Sharing data between parts of a system is safer because nobody can accidentally “mess it up” for someone else.

This is a big part of why Hickey talks about simplicity: fewer hidden side effects means fewer mental branches to track.

“New versions” vs. “editing in place”

Creating new versions can sound wasteful until you compare it to the alternative. Editing in place can leave you asking: “Who changed this? When? What was it before?” With immutable data, changes are explicit: a new version exists, and the old one remains available for debugging, auditing, or rollback.

Clojure leans into this by making it natural to treat updates as producing new values, not mutations of old ones.

Tradeoffs to be honest about

Immutability isn’t free. You may allocate more objects, and teams used to “just update the thing” may need time to adjust. The good news is that modern implementations often share structure under the hood to reduce memory cost, and the payoff is typically calmer systems with fewer hard-to-explain incidents.

Concurrency gets easier when data doesn’t change

Build the core app fast
Create a React web app with a Go and PostgreSQL backend without wiring everything by hand.
Create App

Concurrency is just “many things happening at once.” A web app handling thousands of requests, a payment system updating balances while generating receipts, or a mobile app syncing in the background—all of these are concurrent.

The tricky part isn’t that multiple things happen. It’s that they often touch the same data.

Why shared, changeable data creates race conditions

When two workers can both read and then modify the same value, the final result can depend on timing. That’s a race condition: not a bug you can reproduce easily, but a bug that appears when the system is busy.

Example: two requests try to update an order total.

  1. Request A reads total = 100
  2. Request B reads total = 100
  3. A adds 20 and writes 120
  4. B adds 10 and writes 110

Nothing “crashed,” but you lost an update. Under load, these timing windows become more common.

Traditional fixes—locks, synchronized blocks, careful ordering—work, but they force everyone to coordinate. Coordination is expensive: it slows throughput and becomes fragile as the codebase grows.

How immutability cuts coordination to a minimum

With immutable data, a value doesn’t get edited in place. Instead, you create a new value that represents the change.

That single shift removes a whole category of problems:

  • Readers don’t have to worry that the thing they’re looking at will change mid-read.
  • Writers don’t “fight” over the same memory; they produce new versions.
  • The system can choose safe ways to publish the latest version (often with simple, well-tested primitives).

The outcome: predictable behavior under load

Immutability doesn’t make concurrency free—you still need rules for which version is current. But it makes concurrent programs far more predictable, because the data itself isn’t a moving target. When traffic spikes or background jobs pile up, you’re less likely to see mysterious, timing-dependent failures.

What “better defaults” means in practice

“Better defaults” means the safer choice happens automatically, and you only take on extra risk when you explicitly opt out.

That sounds small, but defaults quietly guide what people write on a Monday morning, what reviewers accept on a Friday afternoon, and what a new teammate learns from the first codebase they touch.

Defaults that reduce risk

A “better default” isn’t about making every decision for you. It’s about making the common path less error-prone.

For example:

  • Immutable data by default: instead of “changing the thing,” you create a new version of it. That makes it harder to accidentally affect other parts of the program that were relying on the old value.
  • Pure functions as the normal style: a function takes inputs and returns an output, without secretly changing shared data or depending on hidden global state. That makes behavior easier to predict and test.
  • Explicit state changes: when something must change, it happens through clear, well-defined mechanisms (rather than any part of the code being able to mutate anything).

None of these eliminate complexity, but they keep it from spreading.

How defaults shape teams and code reviews

Teams don’t just follow documentation—they follow what the code “wants” you to do.

When mutating shared state is easy, it becomes a normal shortcut, and reviewers end up debating intent: “Is this safe here?” When immutability and pure functions are the default, reviewers can focus on logic and correctness, because the risky moves stand out.

In other words, better defaults create a healthier baseline: most changes look consistent, and unusual patterns are obvious enough to question.

Maintenance and onboarding

Long-term maintenance is mostly about reading and changing existing code safely.

Better defaults help new teammates ramp up because there are fewer hidden rules (“be careful, this function secretly updates that global map”). The system becomes easier to reason about, which lowers the cost of every future feature, fix, and refactor.

Separate facts from views: time, history, and traceability

A useful mental shift in Hickey’s talks is to separate facts (what happened) from views (what we currently believe to be true). Most systems blur these together by storing only the latest value—overwriting yesterday with today—and that makes time disappear.

Facts are append-only; views are derived

A fact is an immutable record: “Order #4821 was placed at 10:14,” “Payment succeeded,” “Address was changed.” These don’t get edited; you add new facts as reality changes.

A view is what your app needs right now: “What’s the current shipping address?” or “What’s the customer’s balance?” Views can be recomputed from facts, cached, indexed, or materialized for speed.

Why keeping history pays off

When you retain facts, you gain:

  • Auditability: you can explain why the current value is what it is.
  • Debugging: you can replay the sequence and find the moment things diverged.
  • Traceability: “Who changed this, when, and what was it before?” becomes a data question, not a detective story.

An approachable example: overwrite vs append

Overwriting records is like updating a spreadsheet cell: you only see the latest number.

An append-only log is like a checkbook register: each entry is a fact, and the “current balance” is a view computed from the entries.

Not every system needs full event sourcing

You don’t have to adopt a full event-sourced architecture to benefit. Many teams start smaller: keep an append-only audit table for critical changes, store immutable “change events” for a few high-risk workflows, or retain snapshots plus a short history window. The key is the habit: treat facts as durable, and treat current state as a convenient projection.

Data first: make information durable and flexible

Keep mobile logic clean
Spin up a Flutter mobile app from chat and keep the business logic clear and testable.
Build Mobile

One of Hickey’s most practical ideas is data first: treat your system’s information as plain values (facts), and treat behavior as something you run against those values.

Data is durable. If you store clear, self-contained information, you can reinterpret it later, move it between services, reindex it, audit it, or feed it into new features. Behavior is less durable—code changes, assumptions change, dependencies change.

Values vs actions (without jargon)

  • Data (values): “What is true?” A customer’s email, an order total, a timestamp, a status.
  • Behavior (actions): “What do we do?” Validate, compute discounts, send notifications, decide what a status means.

When you mix these together, systems get sticky: you can’t reuse data without dragging along the behavior that created it.

Less coupling, more reuse

Separating facts from actions reduces coupling because components can agree on a data shape without agreeing on a shared codepath.

A reporting job, a support tool, and a billing service can all consume the same order data, each applying its own logic. If you embed logic inside the stored representation, every consumer becomes dependent on that embedded logic—and changing it becomes risky.

Example: storing clean data vs storing mini-programs

Clean data (easy to evolve):

{
  "type": "discount",
  "code": "WELCOME10",
  "percent": 10,
  "valid_until": "2026-01-31"
}

Mini-programs in storage (hard to evolve):

{
  "type": "discount",
  "rule": "if (customer.orders == 0) return total * 0.9; else return total;"
}

The second version looks flexible, but it pushes complexity into the data layer: you now need a safe evaluator, versioning rules, security boundaries, debugging tools, and a migration plan when that rule language changes.

Why this makes systems evolvable

When stored information stays simple and explicit, you can change behavior over time without rewriting history. Old records remain readable. New services can be added without “understanding” legacy execution rules. And you can introduce new interpretations—new UI views, new pricing strategies, new analytics—by writing new code, not by mutating what your data means.

Applying the ideas to complex systems (without a rewrite)

Most enterprise systems don’t fail because one module is “bad.” They fail because everything is connected to everything else.

The failure modes to watch for

Tight coupling shows up as “small” changes that trigger weeks of retesting. A field added to one service breaks three downstream consumers. A shared database schema becomes a coordination bottleneck. A single mutable cache or singleton “config” object quietly becomes a dependency of half the codebase.

Cascading change is the natural result: when many parts share the same changing thing, the blast radius expands. Teams respond by adding more process, more rules, and more handoffs—often making delivery even slower.

Reduce blast radius with simpler boundaries

You can apply Hickey’s ideas without switching languages or rewriting everything:

  • Prefer immutable data at boundaries. Treat messages, events, and API inputs as “facts” that don’t get edited in place. If something changes, create a new version.
  • Move state to the edges. Keep core logic as pure transformations: input data → output data. Let databases, caches, and UIs handle “current state.”
  • Stop sharing mutable structures. If two modules both write to the same object, they’re coupled. Pass values, not references you plan to mutate.

When data doesn’t change under your feet, you spend less time debugging “how did it get into this state?” and more time reasoning about what the code does.

“Better defaults” across teams

Defaults are where inconsistency sneaks in: each team invents its own timestamp format, error shape, retry policy, and concurrency approach.

Better defaults look like: versioned event schemas, standard immutable DTOs, clear ownership of writes, and a small set of blessed libraries for serialization, validation, and tracing. The result is fewer surprise integrations and fewer one-off fixes.

Incremental adoption that works in existing codebases

Start where change is already happening:

  1. Wrap existing modules with an API that accepts/returns immutable data.
  2. Convert one high-churn workflow to event-style “append-only” records.
  3. Replace shared mutable caches with recomputable views from durable facts.

This approach improves reliability and team coordination while keeping the system running—and keeps the scope small enough to finish.

Where platforms and tooling can reinforce “better defaults”

Share a review build
Use a custom domain to share your build with teammates for faster review and iteration.
Add Domain

It’s easier to apply these ideas when your workflow supports fast, low-risk iteration. For example, if you’re building new features in Koder.ai (a chat-based vibe-coding platform for web, backend, and mobile apps), two features map directly onto the “better defaults” mindset:

  • Planning mode encourages you to make boundaries and data shapes explicit before implementation—often the difference between simple data flow and accidental coupling.
  • Snapshots and rollback make changes safer to ship, because you can quickly revert when an “easy” shortcut turns into a complexity spike.

Even if your stack is React + Go + PostgreSQL (or Flutter for mobile), the core point remains the same: the tools you use every day quietly teach a default way of working. Choosing tools that make traceability, rollback, and explicit planning routine can reduce the pressure to “just patch it” in the moment.

Tradeoffs, limits, and avoiding ideology

Simplicity and immutability are powerful defaults, not moral rules. They reduce the number of things that can unexpectedly change, which helps when systems grow. But real projects have budgets, deadlines, and constraints—and sometimes mutability is the right tool.

When mutability is acceptable

Mutability can be a practical choice in performance hotspots (tight loops, high-throughput parsing, graphics, numeric work) where allocations dominate. It can also be fine when the scope is controlled: local variables inside a function, a private cache hidden behind an interface, or a single-threaded component with clear boundaries.

The key is containment. If the “mutable thing” never leaks out, it can’t spread complexity across the codebase.

Bound complexity with ownership and interfaces

Even in a mostly functional style, teams still need clear ownership:

  • One module owns a piece of state and exposes a small API.
  • Data crosses boundaries as plain values (not live objects with secret behavior).
  • Side effects are pushed to the edges (I/O, databases, time).

This is where Clojure’s bias toward data and explicit boundaries helps, but the discipline is architectural, not language-specific.

What Clojure won’t solve

No language fixes poor requirements, an unclear domain model, or a team that can’t agree on what “done” means. Immutability won’t make a confusing workflow understandable, and “functional” code can still encode the wrong business rules—just more neatly.

Avoid dogma: reduce risk with the smallest change

If your system is already in production, don’t treat these ideas as an all-or-nothing rewrite. Look for the smallest move that lowers risk:

  • Replace shared mutable structures with immutable data at module boundaries.
  • Add event logs or append-only history where auditability matters.
  • Wrap legacy state behind a narrow interface and stop it from spreading.

The goal isn’t purity—it’s fewer surprises per change.

A practical checklist to move toward simpler software

This is a sprint-sized checklist you can apply without changing languages, frameworks, or team structure.

3–5 things to try next sprint

  1. Make your “data shapes” immutable by default. Treat request/response objects, events, and messages as values you create once and never modify. If something must change, create a new version.

  2. Prefer pure functions in the middle of workflows. Start with one workflow (e.g., pricing, permissions, checkout) and refactor the core into functions that take data in and return data out—no hidden reads/writes.

  3. Move state to fewer, clearer places. Pick one source of truth per concept (customer status, feature flags, inventory). If multiple modules keep their own copies, make that an explicit decision with a sync strategy.

  4. Add an append-only log for key facts. For one domain area, record “what happened” as durable events (even if you still store current state). This improves traceability and reduces guesswork.

  5. Define safer defaults in APIs. Defaults should minimize surprising behavior: explicit timezones, explicit null handling, explicit retries, explicit ordering guarantees.

Design review questions (use these verbatim)

  • What are the mutable parts here, and who is allowed to change them?
  • Can we model this as “facts” plus a derived view instead of overwriting fields?
  • If two requests run at the same time, what breaks?
  • Which defaults are we relying on (time, ordering, retries, caching), and are they documented?
  • What would we need to debug this three months from now?

Suggested topics to revisit from Hickey’s talks

Look for material on simplicity vs ease, managing state, value-oriented design, immutability, and how “history” (facts over time) helps debugging and operations.

Simplicity isn’t a feature you bolt on—it’s a strategy you practice in small, repeatable choices.

FAQ

Why does complexity keep “winning” in real projects?

Complexity accumulates through small, locally reasonable decisions (extra flags, caches, exceptions, shared helpers) that add modes and coupling.

A good signal is when a “small change” requires coordinated edits across multiple modules or services, or when reviewers must rely on tribal knowledge to judge safety.

What’s the difference between “simple” and “easy” in software?

Because shortcuts optimize for today’s friction (time-to-ship) while pushing costs into the future: debugging time, coordination overhead, and change risk.

A useful habit is to ask in design/PR review: “What new moving parts or special cases does this introduce, and who will maintain them?”

How do programming language and framework defaults create accidental complexity?

Defaults shape what engineers do under pressure. If mutation is the default, shared state spreads. If “in-memory is fine” is the default, traceability disappears.

Improve defaults by making the safe path the path of least resistance: immutable data at boundaries, explicit timezones/nulls/retries, and well-defined state ownership.

Why is state described as a “complexity multiplier”?

State is anything that changes over time. The hard part is that change creates opportunities for disagreement: two components can hold different “current” values.

Bugs show up as timing-dependent behavior (“works locally,” flaky production issues) because the question becomes: which version of the data did we act on?

What does immutability mean in practical, non-academic terms?

Immutability means you don’t edit a value in place; you create a new value that represents the update.

Practically, it helps because:

  • Readers can trust data won’t change mid-use.
  • Reproducing bugs is easier (inputs remain stable).
  • Sharing data across threads/modules is safer.
When is mutability acceptable (or even preferable)?

Not always. Mutability can be a good tool when it’s contained:

  • Local variables inside a function
  • Performance hotspots (tight loops, parsing, numeric work)
  • Private caches behind a narrow interface

The key rule: don’t let mutable structures leak across boundaries where many parts can read/write them.

How does immutability help with concurrency under load?

Race conditions typically come from shared, mutable data being read and then written by multiple workers.

Immutability reduces the surface area of coordination because writers produce new versions instead of editing a shared object. You still need a rule for publishing the current version, but the data itself stops being a moving target.

What does it mean to separate “facts” from “views,” and how can I apply it incrementally?

Treat facts as append-only records of what happened (events), and treat “current state” as a view derived from those facts.

You can start small without full event sourcing:

  • Add an audit table for critical changes
  • Record change events for one high-risk workflow
  • Keep snapshots plus a limited history window for replay/debugging
What is “data-first” design and why does it reduce coupling?

Store information as plain, explicit data (values), and run behavior against it. Avoid embedding executable rules inside stored records.

This makes systems more evolvable because:

  • Old records remain readable when code changes
  • New consumers can reuse the same data shape
  • You can change logic without rewriting history
What are the first 3 concrete changes to try next sprint to reduce complexity?

Pick one workflow that changes often and apply three steps:

  1. Make boundary data immutable: treat API inputs/outputs, messages, and events as “create once, don’t mutate.”
  2. Refactor the core into pure transformations: input data → output data; push I/O and side effects to the edges.
  3. Reduce shared mutable structures: one owner per piece of state, exposed via a small interface.

Measure success by fewer flaky bugs, smaller blast radius per change, and less “careful coordination” in releases.

Contents
Why complexity keeps winning in real projectsRich Hickey and what Clojure set out to fixSimplicity: not “easy,” but fewer moving partsState: the silent multiplier of complexityImmutability explained without computer-science jargonConcurrency gets easier when data doesn’t changeWhat “better defaults” means in practiceSeparate facts from views: time, history, and traceabilityData first: make information durable and flexibleApplying the ideas to complex systems (without a rewrite)Where platforms and tooling can reinforce “better defaults”Tradeoffs, limits, and avoiding ideologyA practical checklist to move toward simpler softwareFAQ
Share
Koder.ai
Build your own app with Koder today!

The best way to understand the power of Koder is to see it for yourself.

Start FreeBook a Demo