Software Design

The Principle of Least Astonishment: Why getUser Shouldn’t Send Email

Have you ever encountered a function like this during a code review?

3 min read
Cover image for The Principle of Least Astonishment: Why getUser Shouldn’t Send Email

Have you ever encountered a function like this during a code review?

It was named getUser(), but inside it looked up a user, updated the last-login time, refreshed the cache, and even sent a notification email if the account was dormant.

If you called it inside a loop thinking, “It’s just a lookup, so it should be safe,” based only on the name, wouldn’t you end up sending an email bomb to your users?

The principle we’re covering today is the Principle of Least Astonishment, which helps catch code like this. It’s part of our series on development principles covering KISS, DRY, YAGNI, and SOLID—and today’s principle has an especially fun name.

The Principle of Least Astonishment, or POLA, says this:

Code should behave the way its readers expect. If a design surprises its users, rethink the design.

It’s an old principle from system design in the 1960s and 1970s, but it applies broadly to everything from UI and API design to everyday code.


Why Is Astonishment Costly?

In programming, “astonishment” is not an emotional issue; it’s a cost issue.

When developers read code, they form hypotheses based on names and conventions. getUser probably only performs a lookup, and isValid probably returns a Boolean. When the code behaves as expected, it reads smoothly.

But the moment a hypothesis breaks, you have to stop reading, jump into the function, and verify what it really does. Even with only ten such functions in a codebase, distrust sets in: “I can’t trust the names in this code.” From then on, you open every function as you read it. Your reading speed is cut in half.

The worst case is using the code according to your hypothesis without checking and causing an incident—like the email bomb above.


Where Astonishment Appears in Code

Let me organize this around patterns I’ve encountered in practice.

1. Functions Whose Names Don’t Match Their Behavior

// The name says lookup, but it changes state
func getUser(id: Int) -> User {
    let user = db.find(id)
    user.lastSeenAt = Date()  // Astonishment 1: Side effect
    db.save(user)
    if user.isDormant {
        mailer.send(to: user.email, message: "Dormant-account reactivation notice")  // Astonishment 2: External call
    }
    return user
}

Hidden writes in lookup functions are a classic source of astonishment. get should only read; if it changes state, use a name like update or touch.

2. Return Values That Betray Conventions

If one function returns nil when it can’t find something, another throws an exception, and another returns an empty object, callers have to gamble every time. It’s important to standardize on one approach.

3. Silent Failures

func parseConfig(_ json: Data) -> Config {
    guard let config = try? JSONDecoder().decode(Config.self, from: json) else {
        return Config()  // Astonishment: The configuration is corrupted, but an empty configuration is returned as if nothing happened
    }
    return config
}

If a configuration file is corrupted and the system quietly falls back to defaults, users suffer later, asking, “Why didn’t my configuration apply?” Failures are less surprising when they are reported loudly.

When names and behavior differ, readers pay the price
When names and behavior differ, readers pay the price

How to Write Less Surprising Code

Here are the practices I try to follow.

Practice Details
A name is a contract Do only what the name promises; if you do more, change the name
Follow conventions Don’t depart from the existing style of the language, framework, or team without a reason
Make side effects explicit Make it clear from the name when a function changes state
Document anything surprising If unusual behavior is unavoidable, call it out prominently in comments and documentation

The second practice is the most powerful. Across an entire team, boring standard approaches always beat clever personal ones. It’s exactly the same idea as in the previous KISS installment, where I said an ordinary loop is better than an “impressive one-liner.”

Boring, predictable code wins in the end
Boring, predictable code wins in the end

Wrapping Up

There are three things to remember about the Principle of Least Astonishment.

  • Code should behave as expected based on its names and conventions. Code that violates expectations is a breeding ground for bugs.
  • Don’t hide side effects in lookup functions, and don’t silently swallow failures.
  • On a team, boring and predictable code beats clever code.

If I had to choose one criterion for good code, it would be this: code that doesn’t surprise its readers.

If you recently read some code and thought, “Huh? Why does this work that way?”, that was a moment when this principle was violated.