Skip to main content
Clean Architecture 70 XP · 7 min

Functional Programming: The Value of Immutability

Functional programming disciplines assignment. Immutability eliminates race conditions, deadlocks, and concurrent update issues at the architectural level.

Showing
Ad (728×90)

Why this matters

Functional programming's defining characteristic is the discipline of not assigning values. In a purely functional language, variables are not variable — they are named values that never change after binding. This is not a limitation; it is a guarantee.

Martin's architectural insight is that all the hardest problems in concurrent systems — race conditions, deadlocks, concurrent update bugs — arise from multiple threads mutating shared state. If no state is ever mutated, these problems cannot occur. The architecture benefits even in non-purely-functional code: functions that never modify the data they receive are safe to call from any thread, at any time, any number of times. They are the building blocks of systems that scale without fear. The discipline of immutability is not just a style preference; it is a concurrency strategy built into the architecture.

The problem

Mutating shared data in place silently destroys the original state. In concurrent scenarios, two callers modifying the same list simultaneously produce unpredictable results.

Bad

def add_tax(items: list[dict], rate: float) -> None:
    for item in items:
        item["price"] = item["price"] * (1 + rate)  # mutates caller's list!

cart = [{"name": "Book", "price": 20.0}, {"name": "Pen", "price": 5.0}]
add_tax(cart, 0.20)
# cart is permanently modified — original prices are gone
print(cart)  # [{"name": "Book", "price": 24.0}, ...]
function addTax(items: CartItem[], rate: number): void {
  items.forEach(item => {
    item.price = item.price * (1 + rate); // mutates original array in place
  });
}

const cart = [{ name: "Book", price: 20 }, { name: "Pen", price: 5 }];
addTax(cart, 0.20);
console.log(cart); // original prices destroyed — silent data loss

The solution

A pure function returns a new list without touching the original. The caller decides what to do with the result — the function makes no decisions for them.

Good

def add_tax(items: list[dict], rate: float) -> list[dict]:
    return [
        {**item, "price": round(item["price"] * (1 + rate), 2)}
        for item in items
    ]

cart  = [{"name": "Book", "price": 20.0}, {"name": "Pen", "price": 5.0}]
taxed = add_tax(cart, 0.20)

print(cart)   # unchanged: [{"name": "Book", "price": 20.0}, ...]
print(taxed)  # new list:  [{"name": "Book", "price": 24.0}, ...]
interface CartItem { name: string; price: number; }

function addTax(items: CartItem[], rate: number): CartItem[] {
  return items.map(item => ({
    ...item,
    price: Math.round(item.price * (1 + rate) * 100) / 100,
  }));
}

const cart  = [{ name: "Book", price: 20 }, { name: "Pen", price: 5 }];
const taxed = addTax(cart, 0.20);

console.log(cart);  // unchanged: [{ name: "Book", price: 20 }, ...]
console.log(taxed); // new array: [{ name: "Book", price: 24 }, ...]

Key takeaway

If a function modifies data it didn't create, you lose the ability to reason about state — and every race condition, deadlock, and concurrent update bug in history is a consequence of that loss.

Done with this lesson?

Mark it complete to earn XP and track your progress.