Principle of Least Astonishment
debt(d8/e6/b6/t7)
Closest to 'silent in production' (d8), since phpstan/psalm listed in detection_hints cannot detect surprising naming or hidden side effects — these only surface during code review or when developers integrate against the misleading API and hit bugs.
Closest to 'cross-cutting refactor' (e6), because renaming a misleading method like getUser() that has side effects requires updating every caller across the codebase, plus separating the side effect into a properly-named method — quick_fix says 'rename or redesign' which touches many files.
Closest to 'strong gravitational pull' (b6), since applies_to spans web/cli/queue and surprising APIs become load-bearing — every caller works around the astonishment, and the misleading contract shapes how downstream code is written.
Closest to 'serious trap' (t7), grounded in the misconception that POLA only applies to UI/public APIs — developers routinely assume getUser() is a pure read, and the common_mistakes list (constructors doing I/O, inverted booleans) shows the 'obvious' interpretation contradicts actual behaviour.
Also Known As
TL;DR
Explanation
The Principle of Least Astonishment (POLA), also called Principle of Least Surprise, states that a component should behave consistently with users' reasonable expectations. In practice: a method named getUser() should never delete a record, a parameter named $count should accept only positive integers, and a function that returns false should not also send an email. Violations create subtle bugs and erode trust in an API. POLA is closely related to Command Query Separation, Tell Don't Ask, and good naming conventions — together they produce APIs that behave predictably.
Common Misconception
Why It Matters
Common Mistakes
- A method named getUser() that also updates a last_seen timestamp — unexpected side effect.
- A constructor that makes HTTP requests or writes to a database.
- Functions that modify their array argument in place in a language where pass-by-value is expected.
- Boolean parameters that invert expected behaviour: delete($id, true) meaning 'soft delete' and delete($id, false) meaning 'hard delete'.
Code Examples
// Method name suggests read, but performs a write
public function getOrder(int $id): Order {
$order = Order::find($id);
$order->increment('views'); // side effect — astonishing!
return $order;
}
// Constructor with side effects — surprises callers
public function __construct() {
$this->connect(); // unexpected network call
$this->migrate(); // unexpected DB migration
}
// Name accurately describes behaviour
public function findOrder(int $id): Order {
return Order::findOrFail($id); // pure read — no surprises
}
public function recordView(int $id): void {
Order::find($id)->increment('views'); // explicit — callers opt in
}
// Constructor only assigns — side effects via explicit method calls
public function __construct(private readonly DatabaseConfig $config) {}