Why are boolean "flag" arguments considered a function-design smell, and what refactorings remove them? Also: what is the practical guidance on how many parameters a function should take?
answer
- Boolean literal at the call site = smell
- Flag = control coupling = function does two things
- Split function; or enum; or parameter object
- 0 > 1 > 2 > 3 ≫ 4+ (niladic → polyadic)
- Data clumps → introduce parameter object
basics
~20 sA boolean parameter means the function does two things — one per branch — and the call site save(user, true) is unreadable. Fix by splitting into two clearly named functions. Prefer 0-2 parameters; 3 is suspicious; 4+ usually means a missing object.
solid answer
~50 sA flag argument is a boolean (or enum-as-switch) parameter whose only job is to select between behaviours inside the function. It's a smell for three reasons: (1) it advertises that the function does at least two things, violating do-one-thing; (2) call sites become opaque — `render(report, true, false)` is unreadable without opening the signature; (3) it couples callers to an internal branch, so adding a third mode grows a boolean matrix. Refactorings: **split the function** into two intention-revealing functions (`renderForScreen` / `renderForPrint`); if the branches share substantial logic, keep a private common helper. Where the flag is genuinely data (a user preference) rather than a mode selector, keep it — but pass it as a named enum or option object, not a bare boolean. On arity: 0 (niladic) is ideal, 1 (monadic) is fine, 2 (dyadic) needs a natural order, 3 (triadic) needs justification, and 4+ signals a missing parameter object or a function doing too much.
code
pseudocode · 16 lines// Smell: opaque call site, two behaviours behind one name.
bookConcert(customer, true)
function bookConcert(customer, isPremium) {
if (isPremium) { reserveFrontRow(customer); addLoungeAccess(customer) }
else { reserveStandardSeat(customer) }
}
// Refactored: Remove Flag Argument / Split Function.
function bookPremiumConcert(customer) {
reserveFrontRow(customer)
addLoungeAccess(customer)
}
function bookStandardConcert(customer) {
reserveStandardSeat(customer)
}go deeper
Say the flag means the function does two things, that f(x, true) is unreadable at the call site, and that the fix is two named functions. Give the 0/1/2/3+ arity ladder.
Name the refactorings (Remove Flag Argument, Replace Conditional with Polymorphism, Introduce Parameter Object), distinguish mode-selecting flags from data booleans, and mention the combinatorial test-matrix growth.
Frame it as control coupling versus data coupling, discuss when a boolean is legitimately data, cover same-typed adjacent parameters as a defect source, and the value-type/named-argument countermeasures.
Discuss API evolution: booleans can't grow to a third mode without a breaking change, so prefer enums/option objects on public surfaces; weigh split-function proliferation against a single well-typed strategy seam, and set the team norm (lint on boolean literals at call sites).
## Definitions - **Flag argument (control-coupling parameter):** a parameter — usually boolean — whose value selects *which behaviour* the function performs, rather than supplying *data* the behaviour operates on. `deleteUser(id, hard)` where `hard` switches between soft-delete and hard-delete. - **Arity:** the number of parameters. Vocabulary from *Clean Code*: **niladic** (0), **monadic** (1), **dyadic** (2), **triadic** (3), **polyadic** (4+). - **Control coupling:** a classic structured-design term for exactly this — one module passing another a value that dictates its internal control flow. It is a stronger, worse coupling than data coupling (passing values that are simply operated on). ## Why flags are a smell 1. **They prove the function does more than one thing.** The body contains `if (flag) A else B`. That is two behaviours under one name, so the name must be vague enough to cover both — `process`, `handle`, `save` — which destroys intention-revealing naming. 2. **They destroy call-site readability.** `notify(user, true, false, true)` requires the reader to jump to the declaration and count positions. Bare booleans at call sites carry zero semantic information. 3. **They multiply combinatorially.** Two booleans = four behaviours; three = eight. Test matrices and defect surface grow with them, and many combinations are never exercised or are semantically invalid. 4. **They leak implementation into the API.** The caller now knows there *is* a branch. Later, if the two behaviours diverge (different return types, different errors, different permissions), the signature can't express it. 5. **They defeat overload/dispatch mechanisms.** Polymorphism and strategy objects exist precisely to select behaviour; a boolean parameter is a hand-rolled, untyped version of that. ## The refactorings, in order of preference 1. **Remove Flag Argument / Split Function.** `book(customer, isPremium)` becomes `regularBook(customer)` and `premiumBook(customer)`. Shared logic moves into a private helper. This is Fowler's *Remove Flag Argument* refactoring. 2. **Replace boolean with an enum or named constant.** When the axis genuinely has modes and callers pick one, `render(report, Format.PRINT)` beats `render(report, true)` — self-documenting and extensible to a third mode without a new parameter. 3. **Introduce Parameter Object / options struct.** When there are several related switches, group them: `render(report, RenderOptions(format = PRINT, includeCover = false))`. Named fields restore call-site readability. Many languages also offer **named/keyword arguments**, which fix readability without an extra type — `render(report, print = true)` — though they don't fix the do-two-things problem. 4. **Replace Conditional with Polymorphism / Strategy.** When the flag selects an algorithm that has its own state or several methods, promote it to an object. ## When a boolean parameter is fine - **It is data, not a mode.** `setActive(true)` or `user.setEmailOptIn(false)` sets a value; there's no behavioural branch to split. - **It mirrors a domain concept the caller already names.** `withRetries(enabled)` on a builder where the caller passes a configured variable, not a literal. - **The function is a thin adapter** whose entire purpose is to forward a flag to an external API you don't control. Rule of thumb: **a boolean literal at the call site** (`f(x, true)`) is the actual smell. If callers always pass a named variable, readability is preserved; if they pass `true`/`false` inline, split or name it. ## Argument count guidance in detail - **Zero (niladic):** ideal. Nothing to explain, nothing to order wrong. - **One (monadic):** the common good cases are (a) ask a question about the argument — `isValid(order)`; (b) transform it — `parse(text)`; (c) an event handler that consumes it — `onOrderPlaced(event)`. - **Two (dyadic):** fine when the two arguments have a natural, unmistakable order or are components of one value — `Point(x, y)`, `assertEquals(expected, actual)`. Dangerous when both are the same type and order is arbitrary — `copy(a, b)`: which is source? Prefer names or distinct types. - **Three (triadic):** hard to remember, hard to order, and orderings get silently swapped. Usually a hidden concept: `circle(x, y, radius)` wants `circle(center, radius)`. - **Four or more (polyadic):** almost always a missing **parameter object**. If several parameters travel together across many calls, they are a concept (Fowler's *Data Clumps* smell) — give it a type. **Why arity matters mechanically:** each parameter is a thing the reader must hold in memory, a thing the test must supply, and a position that can be transposed. Same-typed adjacent parameters are a real defect source — transposing two strings compiles fine and fails at runtime. Countermeasures: parameter objects, value types (a `UserId` type can't be passed where an `OrderId` is expected), and named arguments. ## Output arguments — the worst arity case An **output argument** is a parameter the function mutates so the caller can read the result: `appendFooter(report)` where `report` is modified in place. Readers expect arguments to be inputs, so this forces a signature lookup. Prefer returning a value, or making it a method on the object being changed (`report.appendFooter()`).
- If the two split functions share 90% of their logic, haven't you just duplicated code?No — you extract the shared body into a private helper and let the two public functions differ only in the parts that actually differ. The public API becomes intention-revealing while the implementation stays DRY. If the shared part is nearly everything and the difference is one line, that's a hint the flag may genuinely be data rather than a mode.
- Do named/keyword arguments make flag parameters acceptable?They fix the readability half of the problem — `render(report, print = true)` is legible — but not the design half: the function still contains a behavioural branch and still does two things. Named arguments are a good mitigation when splitting is impractical (e.g. wrapping a third-party API), not a full replacement for the refactoring.
- You said 4+ parameters usually means a missing object. How do you spot which parameters belong together?Look for the data-clumps smell: the same group of parameters appearing together in several signatures, or parameters that are only ever meaningful jointly (x/y/z, start/end, host/port/timeout). Those groups are concepts waiting for a name, and extracting them often reveals behaviour that should move onto the new type.
saying these in an interview costs you the question
- "Booleans are always banned" — a boolean that sets data (setActive(true)) is fine
- Renaming the parameter in the declaration and calling the smell fixed
- Adding more booleans instead of an enum or options object
- Claiming named arguments fully solve it (they fix readability, not the two-things problem)
- Treating output arguments — parameters mutated to return results — as normal