Learning Scala: Pure Functions and the First Step Toward Bug-Free Concurrency

In Part 2, we explored how mutable state—especially state that someone once believed was a fact—tends to drift over time, and how this drift destabilizes large systems. Immutability is a corrective measure: if something is a fact, preserve it. But immutability addresses only one dimension of stability. The other dimension concerns the way behavior is expressed—whether the logic we rely on actually does what its name claims, or whether it also does several other things we never quite account for.

When developers talk about pure functions, they often recite the textbook definition: same inputs, same outputs, no side effects. It’s correct, but it undersells the point. Purity is not an aesthetic choice or an academic curiosity. It is a way of reclaiming control over the semantics of your system. It is how you establish that a piece of business logic actually behaves like business logic, rather than a negotiation with global state, shared caches, volatile time checks, and whichever service instance happens to answer the call.

Commerce systems surface this problem bluntly. Consider cart repricing. A cart is a negotiation. Shoppers add and remove items, the system reevaluates promotions, seasonal rules apply or expire, and flash sales may activate in the middle of a session. A change to the cart or its context prompts a recalculation. The mechanics sound straightforward; the reality rarely is. Many pricing engines embed side effects throughout their logic: they reach into global registries of promotions, pull loyalty tiers from shared caches, write audit logs during discount evaluation, and update counters in observability systems. None of these are wrong in isolation, but collectively they entangle pricing with the world around it and make the outcome nondeterministic under concurrency.

Everyone who has worked in this space has seen a cart fluctuate unpredictably: a discount that appears on one request and disappears on the next; a price that depends on which node in the cluster answered; totals that drift because two threads read promotions at different moments. Even the order in which the items are carted may cause discrepancies. The arithmetic is simple; the impurities are not. Let’s examine how those impurities surface in code.

Missed earlier posts in the series?

Catch up on earlier posts to follow along with the Functional Programming Isn’t Just for Academics series:

Each installment dives deeper, illustrating how Scala enables teams to write clean, testable, and scalable applications.

When a Pricing Function Isn’t Really a Function

A simplified but representative Java-style example might look like this:

Java

public Money reprice(Cart cart) {
    // Reads global promotion rules (mutable, time-sensitive)
    List<Promotion> activePromos = PromotionRegistry.getActivePromotions();
    // Writes to an audit log as a side effect
    PromoAudit.log("Repricing cart " + cart.getId());
    // Touches a shared counter (concurrency hazard)
    Metrics.cartRepriced.increment();
    // Checks for a time-based flash sale
    boolean flashSale = FlashSale.isActive();
    Money total = Money.zero();

    for (CartItem item : cart.getItems()) {
        Money price = item.getListPrice();
        for (Promotion p : activePromos) {
            price = p.apply(price, item, flashSale);
        }
        total = total.plus(price.multiply(item.getQuantity()));
    }

    return total;
}
  

Nothing here looks outrageous, yet almost every line compromises determinism. The function’s behavior depends on global mutable state, external side effects, the system clock, and the order in which items and promotions happened to be loaded or refreshed. Under concurrency, these dependencies do not stay stable. A thread that begins computing while a flash-sale toggle is flipping or while promotions are updating may produce a different price from a thread that begins a millisecond later.

This function pretends to compute a price, but it actually conducts a conversation with the universe.

Testing becomes fragile, because the function has more external dependencies than explicit parameters. Reproducibility becomes elusive. Parallel execution becomes unsafe. And when prices shift unexpectedly, engineers struggle to pinpoint the cause because the logic is entangled with operational concerns.

The alternative is to treat repricing as the deterministic calculation it ought to be.

Repricing as a Pure Function

A pure formulation makes all dependencies explicit:

Scala

final case class PricingContext(
  promotions: List[PromotionRule],
  flashSaleActive: Boolean
)

def reprice(cart: Cart, ctx: PricingContext): Money = {
  cart.items.foldLeft(Money.zero) { (total, item) =>
    val base = item.listPrice
    val discounted = ctx.promotions.foldLeft(base) { (price, rule) =>
      rule.apply(price, item, ctx.flashSaleActive)
    }
    total + (discounted * item.quantity)
  }
}
  

Here the repricing logic depends only on the cart and a supplied pricing context. There are no global reads, no shared state updates, no logging, no timers. The logic performs one job: calculate a price.

This version is trivial to test: provide a cart, provide a context, expect a result. It is trivial to reason about because everything influencing the outcome is visible in the function’s signature. It is safe in the presence of concurrency, because nothing inside the function can be interfered with by other threads. And it is reproducible: the same inputs always yield the same outputs.

Purity does not remove complexity; it places it in the open, where it can be understood, controlled, and scaled.

Read More: Why Functional Programming Matters for the Systems We Build Today

Referential Transparency: Why Purity Matters Beyond “Clean Code”

When a function is pure, we gain referential transparency—the ability to replace a function call with its result without altering program behavior. This sounds theoretical, but it is the property that makes reasoning compositional. When referential transparency holds, you do not need to know when or where a function was called, or which node handled the request; you only need the inputs. This dramatically simplifies:

  • retry logic, because repeating a pure computation cannot cause harm;

  • parallel and distributed execution, because the evaluation order does not matter;

  • caching and memoization, because the output is stable and keyed only by explicit inputs;

  • testing, because there is no environment to simulate;

  • debugging, because the function either returns the correct value or it does not—there is nothing hidden around it.

In commerce, where price, discount, and eligibility computations run millions of times per hour, these properties translate directly into reliability and cost efficiency.

Purity and Edge Caching: Why Predictability Scales

Digital commerce is dynamic by nature. Promotions change because marketing wants them to. Loyalty tiers change because customers earn or lose status. Inventory and assortment shift by the minute. Flash sales activate for short windows. Every one of these changes challenges a system’s ability to cache results at the edge, where speed matters most.

Teams typically rely on heuristics: cache for a few minutes, or a few seconds, or not at all. They do this because they cannot be certain when cached results are still valid. The pricing function depends implicitly on global state, and those dependencies are not visible in the function’s signature. Without a clear dependency model, cache invalidation becomes guesswork.

Pure functions solve this elegantly. When your pricing logic’s dependencies are explicit—promotions, flash-sale switches, loyalty attributes, timestamps—each of those inputs becomes part of the cache key. If none have changed, the cached price is correct. If one has changed, the cache must be bypassed or refreshed. There is no guesswork, because the shape of the function defines the shape of the invalidation strategy.

Edge caching becomes a mechanical extension of the domain model rather than a probabilistic optimization. Akamai, Cloudflare, Fastly, and others become almost embarrassingly effective when the application provides a deterministic calculation. And because pure repricing functions are safe to run on any node, the edge can shoulder far more work without compromising correctness.

Purity does not eliminate dynamism; it allows the system to respond to it predictably.

Why Java Makes This a Struggle

Java developers can write pure functions, but the language and its surrounding ecosystem do not encourage it. Frameworks default to global configuration, dependency injection boundaries blur the distinction between construction and execution, and most libraries assume mutability. The language does not prevent accidental side effects or hidden dependencies; it leaves the burden of discipline on developers and reviewers. In small teams, that discipline holds. In organizations with dozens of contributors, it frays. 

Concurrency and testing magnify the cost of those fractures. Consider your CI/CD process and how much of it is dedicated to linting and testing for developer compliance… too much, not enough? How would you change it to meet your organizations needs and how might that be different if pure functions were the rule?

Why Scala Makes Purity Natural

Scala makes pure functions the intuitive choice. Immutable data structures are the default; methods tend to be referentially transparent unless you explicitly introduce side effects; function composition is expressive; and domain models are concise enough that purity does not require elaborate scaffolding. The language exposes, rather than conceals, a function’s dependencies. And because of that, concurrency comes almost for free.

The distinction is not that Scala prevents impurity. It is that Scala makes purity the simplest way to express your domain’s logic. The cleaner the logic, the cleaner the concurrency story around it.

Read More: Immutability by Default and the Foundation of Reliable Systems

The Architectural Insight

Pure functions do not solve every problem. But they solve the class of problems that make pricing, promotions, and eligibility logic unpredictable under load. They enable caching strategies that are rational rather than heuristic. They make concurrency manageable without locks. They remove whole categories of bugs rather than patching them reactively. And when combined with immutable data, they create systems whose behavior is a function of their inputs—not an artifact of their circumstances.

Purity is how you take the drift out of behavior. It is how you make logic portable, testable, and naturally scalable. And it is how you prepare a system to operate confidently at the edge as well as at the core.  This is the foundation for the next part of the series, where we will look more closely at effects—the operations that are not pure—and how isolating them allows an architecture to remain predictable even when the world around it is not.

 
Previous
Previous

Learning Scala: Thinking in Expressions Instead of Statements

Next
Next

Learning Scala: Immutability by Default and the Foundation of Reliable Systems