Skip to content
Back to skills

Functional Managing State Immutably

ASecurity

Use when code mutates shared data or updates a record in place, when a concurrent update is lost or races, or when parallel work needs cancelling together.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 21, 2026
ai-agentsjavascriptgojavashelldatabase

Security analysis

A100/100

Pro scans all 3 files and shows the line behind each finding

Scanned October 21, 2026

npx -y skills add yutna/functional-design-skills --skill functional-managing-state-immutably --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Functional Managing State Immutably?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Functional Managing State Immutably
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/yutna-functional-managing-state-immutably/badge)](https://www.skillsdirectory.com/skills/yutna-functional-managing-state-immutably)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: functional-managing-state-immutably
description: Use when code mutates shared data or updates a record in place, when a concurrent update is lost or races, or when parallel work needs cancelling together.
license: MIT
metadata:
  pack: functional-design-skills
  version: 3.3.0
---

# Managing State Immutably

**In this pack:** [how much design a task warrants](../functional-design/references/calibration.md),
[the notation](../functional-design/references/notation.md), and
[syntax per language](../functional-design/SKILL.md#language-packs).

## Overview

Assignment is the source of most of the hard problems in software: a
value that changes cannot be reasoned about locally, cannot be shared
safely between threads, and cannot be replayed. Functional programming's
central move is to stop assigning: a transformation produces a new value
and leaves the old one alone.

State does not disappear. It becomes explicit: a value, transformed by
pure functions, held in exactly one place, and replaced under a
controlled swap. That is what makes concurrency safe by construction
rather than by discipline.

## When to use

- Data is mutated in place and read from several places
- A race condition, or a lock protecting shared data
- Deciding how an entity changes over time
- History, undo, or audit is needed
- A cache, counter, or connection pool needs a home

Not for: deciding which effects the system performs, which is
[functional-separating-pure-core-from-shell](../functional-separating-pure-core-from-shell/SKILL.md).

## Core rules

1. Rule. **Transform, do not mutate.** Never mutate a value that anything
   else can observe. A function that changes such a value returns a new
   one, and the caller decides what to keep. A value built inside one
   function and not yet returned is outside this rule; rule 6 says when
   mutating it is worth it.
2. Rule. **State transitions are pure functions.** No I/O inside, no
   clock, no randomness. The shape depends on the job. A transition is
   one function for each event, takes the specific source state, and
   returns the new state, or a `Result` when it can refuse, as
   [functional-modeling-state-machines](../functional-modeling-state-machines/SKILL.md)
   says. `State -> Event -> State` is the fold that replays events
   already accepted, and it cannot refuse.
3. Default. **Keep each piece of mutable state with one owner, at the edge.**
   One reference cell, one database row, one actor, reached through the pure
   update function and owned by one module. Not a field on ten objects.
   Unrelated pieces of state get separate cells. State that lives for one
   operation is not this: it stays on the caller's stack, as a value passed
   along, and needs no cell.
4. Default. **Swap, do not edit.** Replace the whole value atomically; never
   partially update a shared structure.
5. Default. **Share structure instead of copying.** Persistent data structures
   make "a new version" cheap. A small value is simply copied: a record of six
   fields needs no persistent structure. Bring one in when a profile shows that
   copying a large collection costs. See
   [persistent-structures.md](references/persistent-structures.md).
6. Default. **Local mutation is acceptable when invisible.** It applies when the
   value is built inside one function, no reference to it escapes, into a
   closure or a callback included, and a measurement justified it. A transient
   or builder that is frozen before it is returned qualifies. A mutable
   accumulator that survives between calls does not. Where the language has no
   cheap way to add to an immutable collection, as JavaScript has none, building
   locally is the idiom and needs no measurement; the first two conditions still
   hold.
7. Default. **Do not defend with copies.** If values are immutable, defensive
   copying is dead code. Where a value is mutable and cannot be frozen, the copy
   is not dead: hand out a copy or an immutable view, never the internal
   reference.

## Pattern

Mutation shared across readers:

```text
booking.treatments.push(t)   -- who else holds this booking?
booking.total = recalculate booking -- two steps: readers see between
```

Any code holding `booking` sees the change, possibly halfway through.
Under concurrency, two pushes can lose one. Under test, the previous
state is gone.

Transformation:

```text
addTreatment : BookedTreatment -> Booking -> Booking
addTreatment t booking =
  { ...booking, treatments: cons t booking.treatments }

total : Booking -> Satang     -- derived, never stored
```

The old booking still exists and is still valid. Concurrency is handled
once where the booking is stored, not at every mutation site.

## Where the mutable cell lives

Every system has state that outlives a request. Put it in exactly one
kind of place, chosen deliberately:

| Holder              | Suits                                     |
| ------------------- | ----------------------------------------- |
| Database row        | State that must survive a restart         |
| One reference cell  | In-process state: cache, registry, config |
| An actor or process | State with its own lifecycle and mailbox  |
| The caller's stack  | State that lives for one operation        |

Whichever is chosen, the update follows the same shape: read the current
value, compute a new one with a pure function, install it atomically.

```text
updateAtomically :
  Integer -> Reference<S> -> (S -> S) -> AsyncResult<S, Conflict>
```

It reads and writes a shared cell, so it has an effect type. If the
installation fails because someone else changed the value first, the
retry is a bounded loop around the pure function, and the pure function
is unchanged. When the bound is spent the caller receives a `Conflict`.
See
[concurrency.md](references/concurrency.md).

## Modelling change over time

Three representations, in increasing order of what they preserve:

**Current value.** The state as it is now. Cheapest, and enough when
history has no business meaning.

**Value plus version.** The state and a monotonically increasing number.
Enables safe concurrent updates and detects lost updates.

**Events, folded into a value.** Store what happened; derive the current
state by folding. Preserves history, enables audit and replay, and costs
storage and a projection step. See
[functional-folding-over-data](../functional-folding-over-data/SKILL.md).

Choose by what the business asks for. If someone will ever ask "how did
it get into this state", the answer must be stored, not reconstructed
from logs.

## Concurrency

Immutable values cannot be corrupted by concurrent readers, so the entire
class of data races disappears. What remains is the coordination of
updates to the one mutable cell, and that is a small, isolated problem:

- Readers need no locks, ever.
- Writers use an atomic swap with a version or compare-and-set.
- Contention is proportional to how much unrelated state shares one cell,
  which is an argument for small aggregates.
- Work that was spawned has an owner that waits for it or a handle that
  cancels it. Neither one is a leak, and cancellation is a value passed
  in rather than something done to a running computation. See
  [concurrency.md](references/concurrency.md).

## Red flags

- A function that returns nothing but changes its argument
- A lock protecting a data structure rather than a resource
- Defensive copying of values that are already immutable
- A field updated in two steps, with readers in between
- Mutable state reachable from many modules, rather than owned by one
- A cached value stored beside the data it is derived from
- Two threads calling the same "add" and one result disappearing

## Common mistakes

- **Copying everything.** Immutability without structural sharing is slow
  for large structures. Where a profile shows it, use the language's
  persistent collections.
- **Freezing shallowly.** A frozen record holding a mutable list is
  mutable.
- **Storing derived values.** Recompute, unless it must be frozen for a
  business reason, in which case it is a different concept with its own
  name.
- **Mutating inside a "pure" function.** An accumulator that survives
  between calls makes the function untestable in parallel and unsafe to
  memoise. One that is built and frozen inside a single call is rule 6.
- **Using a lock instead of a swap.** Locks compose badly and the value
  is already immutable; the swap is enough.

## Related skills

- [functional-modeling-state-machines](../functional-modeling-state-machines/SKILL.md)
- [functional-separating-pure-core-from-shell](../functional-separating-pure-core-from-shell/SKILL.md)
- [functional-enforcing-consistency-boundaries](../functional-enforcing-consistency-boundaries/SKILL.md)
- [functional-folding-over-data](../functional-folding-over-data/SKILL.md)

## Further reading

- [persistent-structures.md](references/persistent-structures.md)
  explains structural sharing, when copying costs matter, and where each
  language's implementations come from — including the two that ship
  none.
- [concurrency.md](references/concurrency.md) covers atomic updates,
  retries, idempotency, reading two sequences of effects against each
  other, and the failure modes immutability does not fix.

See
[two-schools.md](../functional-choosing-types-or-plain-data/references/two-schools.md)
for where the two schools of functional design agree, immutable data
among them.

Files in this skill

  • SKILL.md9.2 KB
  • references/concurrency.md11.2 KB
  • references/persistent-structures.md7.6 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.