> ## Content Index
> Fetch the complete content index at: https://ricofritzsche.me/llms.txt
> Use this file to discover other available public pages before exploring further.

# Pure Functions and Immutable Data: Simplifying Complexity by Design
- URL: https://ricofritzsche.me/pure-functions-and-immutable-data-simplifying-complexity-by-design/
- Published: 2025-06-02T11:03:09.000Z
- Updated: 2025-06-02T11:08:58.000Z
- Description: Pure functions and immutable data aren't just academic concepts. They're practical tools for building robust, maintainable software.
- Author: Rico Fritzsche
- Tags: Functional Programming, Rust, Pure Functions, Immutable Data, Software Architecture, Domain Modeling

Most complexity in software development doesn't arise from inherently complicated business logic. It emerges from the uncontrolled ways in which we manipulate data and perform side effects. For decades, we've treated data as mutable, stateful objects, living entities with changing behaviors. This object-oriented perspective created an entire ecosystem of accidental complexity, leading developers into tangled states, unpredictable behaviors, and frustrating bugs.

In one of my latest articles on [Functional Core and Imperative Shell](https://ricofritzsche.me/applying-functional-core-and-imperative-shell-in-practice/), I explained how isolating side effects makes your business logic simpler and cleaner. Today, I’m diving deeper into the heart of functional programming: pure functions and immutable data. These two foundational concepts have profoundly reshaped my approach to solving complex domains. By embracing them, we can design software that's simpler, more predictable, and truly maintainable.

## Weekly Tips to Master Software Architecture Newsletter

I provide lean, effective software-architecture strategies that cut through the noise.

Subscribe 

Email sent! Check your inbox to complete your signup. 

Trusted by 2,500+ developers on Medium.

## Why Pure Functions Matter

First, let’s establish clearly what a **pure function** is. A pure function is a function that satisfies two critical conditions:

- **Deterministic**: Given the same inputs, it always produces the exact same outputs.
- **Side-effect free**: It does not alter any external state or depend on any external mutable state.

Consider this simple example:

```Rust
fn add(a: i32, b: i32) -> i32 {
    a + b
}
```

This function is pure. Every time you call the function *add(3, 2),* the answer is *5*. It doesn't matter if it's today, tomorrow, or running concurrently across 100 threads. The result is consistent. Pure functions promise absolute predictability.

Contrast this with a non-pure function:

```Rust
static mut TOTAL: i32 = 0;

fn add_to_total(a: i32) {
    unsafe {
        TOTAL += a;
    }
}

```

Here, each call modifies a global state *TOTAL*. Suddenly, predictability vanishes. Call this from multiple threads, and you're headed straight into race conditions and confusion.

Pure functions remove this entire class of problems from your code.

### Pure Functions Simplify Testing and Debugging

Testing pure functions is straightforward:

- No mocks, no stubs, and no external setup.
- Just inputs and outputs.

Testing the earlier pure function is trivial:

```Rust
assert_eq!(add(2, 3), 5);

```

That’s it. Testing becomes predictable and transparent.

Eric Normand, in his excellent book [*Grokking Simplicity*](https://www.amazon.de/dp/1617296201?ref=ppx%5Fyo2ov%5Fdt%5Fb%5Ffed%5Fasin%5Ftitle), emphasizes that pure functions simplify code precisely because they avoid hidden side effects. 

**Every action is explicit, every outcome predictable.** 

This is critical for controlling complexity. This explicitness makes debugging substantially easier. Bugs frequently originate from unexpected mutations and state changes. By eliminating mutable state from your core logic, your functions become transparent, predictable, and easy to reason about.

## Immutable Data: The Essential Partner to Pure Functions

Pure functions are great but incomplete without immutable data. Data immutability means:

- Once created, data never changes.
- Modifications result in a new data instance; the original remains untouched.

Immutable data forces you to shift your mindset away from *stateful object* towards *data snapshots*.

Here's an immutable data structure in Rust:

```Rust
#[derive(Clone)]
struct User {
    id: i32,
    name: String,
    active: bool,
}

impl User {
    fn deactivate(&self) -> Self {
        Self {
            active: false,
            ..self.clone()
        }
    }
}

```

Check the full code here: [https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1a71ff607802c89ba79b8ae0d8f32b19](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1a71ff607802c89ba79b8ae0d8f32b19&ref=ricofritzsche.me)

The *deactivate* method does not alter the original *User*. Instead, it returns a new instance with slight modifications. The original *User* remains unchanged and stable.

Contrast this with mutable state:

```Rust
struct MutableUser {
    id: i32,
    name: String,
    active: bool,
}

impl MutableUser {
    fn deactivate(&mut self) {
        self.active = false;
    }
}

```

While mutability seems simpler at first glance, it's a trap. Mutable state creates complexity by:

- Obscuring state transitions: What was the previous state?
- Enabling unintended side effects: Multiple parts of code modifying the same data simultaneously.
- Increasing cognitive overhead: Keeping track of the changing states is mentally taxing and error-prone.

Eric Normand puts this succinctly:

> “Mutability is complexity’s favorite hiding spot. Immutability, on the other hand, exposes state clearly, making it impossible for hidden interactions to cause chaos.”

But what is the benefit of immutability?

## Real-world Benefits of Immutable Data and Pure Functions

With immutable data, concurrent programming becomes dramatically simpler. Immutable data is inherently thread-safe, as there’s no chance for simultaneous threads to corrupt shared state.

For instance, using Rust's ownership model:

```rust
use std::sync::Arc;
use std::thread;

let user = Arc::new(User {
    id: 1,
    name: "Alice".into(),
    active: true,
});

let handles: Vec<_> = (0..10).map(|_| {
    let user = Arc::clone(&user);
    thread::spawn(move || {
        println!("User active? {}", user.active);
    })
}).collect();

for handle in handles {
    handle.join().unwrap();
}

```

No locks, no race conditions, and no surprises. Every thread accesses a stable, immutable instance of data.

Check out the prove in the playground: [https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=213075ee1ded41677f1175e42142a5c5](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=213075ee1ded41677f1175e42142a5c5&ref=ricofritzsche.me)

Notice that no thread ever has mutable access to the '*User*'. If you tried to uncomment a line such as *user.active = false* within a thread, Rust would prevent you from doing so by issuing a compile-time error. Since '*active*' is simply a *boolean* value that never changes, each thread reliably prints '*true*'.

This immutability, combined with [Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html?ref=ricofritzsche.me), is precisely how you can guarantee 'no locks, no races, no surprises'.

Immutable data inherently maintains historical states. When data changes result in new instances, past states remain accessible. This property naturally supports features like undo operations, event sourcing architectures and so on.

### Clear, Composable Logic

Pure functions naturally compose, creating clarity even in complex domains. Consider how clear the composition of immutable data transformations becomes:

```rust
fn update_name(user: &User, new_name: &str) -> User {
    User {
        name: new_name.to_string(),
        ..user.clone()
    }
}

fn deactivate_user(user: &User) -> User {
    user.deactivate()
}

// Compose transformations
let user = User { id: 1, name: "Alice".into(), active: true };

let updated_user = deactivate_user(&update_name(&user, "Bob"));

```

Each step clearly defined, predictable, and immutable. No hidden interactions, just pure data transformation.

Try it out here: [https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b3a22512b65112d8dfe62990b3e2da3d](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b3a22512b65112d8dfe62990b3e2da3d&ref=ricofritzsche.me)

## Moving Beyond Stateful Objects

I've fully removed the concept of stateful data classes from my projects. Instead, each feature consumes immutable data and produces new immutable results:

**Immutable data in → Pure functions → Immutable result out**.

![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Screenshot-2025-06-02-at-12.54.24.png)

This approach rejects the traditional OOP perspective that data is a stateful "thing." Data is simply information! Stable snapshots representing facts at given points in time. Recognizing this fact eliminates an entire category of complexity from our domain modeling.

Initially, immutable data structures and pure functions may seem restrictive. However, constraints in design lead to simplicity. By deliberately choosing immutability and purity, we can prevent accidental complexity, and, most importantly, ensure predictable behavior. This makes code simpler to test, understand, and extend.

Again, Eric Normand captures this brilliantly:

> “Complexity doesn't disappear by accident. You tame it by consciously designing simpler solutions. Immutability and pure functions aren’t restrictive; they liberate us from hidden complexity.”

## Conclusion

Shifting to pure functions and immutable data isn’t merely a technical decision. It's a profound philosophical shift in how you approach software design. It challenges deeply ingrained assumptions about data and state. Yet, it promises significant payoffs:

- Clear, predictable business logic
- Safer concurrency
- Simple, mock-free testing
- More maintainable codebases

Complex domains don’t have to lead to complicated code. Pure functions and immutable data structures prove that complex problems can indeed be solved simply, elegantly, and reliably.

As always, the goal remains clear: simplicity. Not simplicity for simplicity's sake, but because simplicity is the strongest ally in the battle against complexity.

*Cheers*!

##