Swift

Swift Struct mutating Keyword Explained: When Value Types Need to Modify Themselves

By Seren  |  26 Sep, 2026  |  Leave a comment


I stopped thinking of Swift’s mutating keyword as a compiler formality after building a checkout model with several valid stages. A cart could be edited while shopping, locked before payment, marked as paid, or reset. Letting outside code assign those properties directly made invalid combinations too easy to create.

I moved the transitions into a struct and used mutating methods to express the operations that change it. The keyword did not make the design correct by itself, but it made every state-changing method visible and allowed the type to protect its own rules. The downloadable project recreates this checkout flow and verifies both successful and rejected transitions.

The Problem Was an Invalid Checkout, Not a Keyword

The sample checkout contains product lines and a stage. While the session is shopping, a caller can add a new product or increase the quantity of an existing one. Preparing for payment requires at least one line. Once payment preparation begins, ordinary cart changes are locked. Payment can finish only from the ready state.

These rules are small enough to understand, but they are also easy to bypass if lines and stage have public setters. The example exposes them as private(set) and gives callers named operations instead. Each operation can validate the current stage before changing stored data.

This is the real reason the keyword matters in the project. It marks methods that may change the value represented by a struct. Readers can distinguish a query such as totalInCents from a transition such as prepareForPayment() without opening the implementation.

Why the Compiler Requires mutating

A method on a struct cannot assign to its stored properties unless that method is declared mutating. This would fail to compile:

struct Counter {
    private(set) var value = 0

    func increment() {
        value += 1
    }
}

Changing the declaration to mutating func increment() tells the type system that calling the method changes the value. That permission belongs in the method signature; it is not a runtime switch and does not mean callers lose control over whether their binding can change.

It is tempting to explain this entirely through stack and heap storage, but that model is unreliable. A struct is a value type regardless of whether optimization, capture, generics, or surrounding storage places its bytes somewhere particular. mutating describes value semantics at the language level, not a promise about one physical memory location.

The Variable Holding the Value Must Also Be Mutable

A mutating method can be called only when the value is stored in a mutable context. This works:

var checkout = CheckoutSession()
try checkout.add(
    productID: "mug",
    unitPriceInCents: 2400,
    quantity: 1
)

If checkout were declared with let, the call would be rejected because the binding promises that its value will not change. The method’s mutating declaration advertises the change, while the caller’s var declaration permits it. Both sides participate in the rule.

This differs from a class stored in a let constant. The constant prevents replacing the reference with a different instance, but the referenced object’s variable properties may still change through methods. That is a consequence of reference semantics, not proof that classes are simply mutable structs stored elsewhere.

Use Methods to Protect Cart Invariants

The checkout’s add operation checks the current stage and validates the requested quantity before changing the array:

mutating func add(
    productID: String,
    unitPriceInCents: Int,
    quantity: Int
) throws {
    guard stage == .shopping else {
        throw CheckoutError.changesLocked
    }
    guard quantity > 0 else {
        throw CheckoutError.invalidQuantity
    }

    // Merge an existing line or append a new one.
}

The method throws instead of silently ignoring an invalid transition. A user interface can turn that error into a message, while a test can compare the exact error case. More importantly, failed validation occurs before a stored property changes, so the session is left in its previous valid state.

Inside the existing-line branch, the code calls another mutating method on an array element. Swift provides mutable access to that element for the duration of the operation. This keeps the positive-quantity rule inside CartLine rather than duplicating raw assignments throughout the checkout model.

A Copy Keeps Its Own State

Before moving the active session to payment, the executable creates a copy:

let savedShoppingCopy = checkout
try checkout.prepareForPayment()

// checkout is ready for payment.
// savedShoppingCopy is still shopping.

The test confirms that changing the original variable does not change the saved copy’s stage. That is the behavior I want from a value used as a snapshot, draft, reducer state, or test fixture. The compiler and standard library may optimize how copies are represented, but the observable semantics remain those of independent values.

Copying is not automatically the right answer for every domain. A shared identity with many observers may fit a reference type, an actor, or a dedicated store better. I choose a struct here because a checkout snapshot is useful as a complete value and all transitions can return or modify one coherent state.

A Mutating Method Can Replace self

A mutating method is not limited to changing one stored property. It may assign a new value to self. The reset operation uses that ability:

mutating func reset() {
    self = CheckoutSession()
}

This is clearer than remembering to empty the lines, restore the stage, and reset every future property separately. The default initializer already defines a fresh session, so assigning it to self keeps reset behavior aligned with initialization.

Replacing self is also useful for enums whose mutating method performs a state transition. I use it when the new value is easier to describe as a whole than as several individual assignments. If replacement hides important work, an explicit new-value-returning function can be easier to test and review.

Protocol Requirements Need to Admit Mutation

The sample defines a small reset contract:

protocol Resettable {
    mutating func reset()
}

The protocol must declare the requirement as mutating so a value-type conformer is allowed to change itself. CheckoutSession then marks its implementation accordingly. A class can satisfy the same requirement without writing mutating, because that keyword is not used on class instance methods.

This detail matters when designing protocols intended for both value and reference types. Leaving mutating off a requirement prevents a struct implementation from changing stored state to fulfill it. Adding the keyword admits that behavior without forcing class syntax to change.

When I Return a New Value Instead

A mutating API is convenient when a local variable naturally evolves through several operations. It is not the only way to model a transition. A method can return a modified copy:

func preparedForPayment() throws -> CheckoutSession {
    var copy = self
    try copy.prepareForPayment()
    return copy
}

The returned-value style works well in transformation pipelines and makes the before-and-after values explicit. The mutating style avoids repetitive reassignment during a sequence of local changes. I choose the API that makes ownership and failure behavior clearest at the call site rather than treating one style as universally safer.

Frequent mutation is not by itself a reason to replace a struct with a class. The more useful questions are whether the model has shared identity, whether copies should be independent, who owns changes, and whether observers need one shared instance.

Checks I Use for Mutating Value Types

  • Does the method actually change a stored property or replace self?
  • Can invalid input be rejected before any state is changed?
  • Should callers be able to assign stored properties directly?
  • Does a copied value need to remain independent?
  • Would returning a new value make the transition clearer?
  • Does a protocol requirement permit value-type mutation?
  • Is shared identity important enough to justify a reference type?
  • Do tests cover both valid and rejected transitions?

In this checkout model, mutating is useful because the type owns a small set of state transitions and callers work with complete snapshots. The strongest part of the design is not the keyword; it is keeping validation and mutation together so invalid states are harder to construct.

Download and Run the Example

The complete project is a Swift Package for macOS and requires Swift 5.9 or later. It has no third-party dependencies or API credentials.

git clone https://github.com/2252408699/swift-mutating-checkout-session.git
cd swift-mutating-checkout-session
swift run

A successful run prints nine PASS lines followed by All mutating checkout checks passed. The checks cover line merging, quantity changes, totals, independent copies, transition errors, payment, resetting self, and an empty-cart failure.

Complete source code: swift-mutating-checkout-session on GitHub

Seren
Seren

A developer exploring iOS, Objective-C, Swift, and other tech stacks. Here to document my learning process, pitfalls, and growing journey across new technologies.

Your email address will not be published. Required fields are marked *