A boolean parameter can make an API compact while making each call harder to understand. Consider this call:
save(document, true)What does true mean? Overwrite an existing document? Validate before saving? Publish immediately? The caller knows only if they remember the parameter definition or inspect the function.
The problem becomes more serious when the boolean selects substantially different behavior. A small flag can hide two operations behind one name, spread conditional logic through an implementation, and make later changes awkward.
This article develops a practical rule for recognizing that situation. You will learn when a boolean is ordinary data, when it is really a hidden command, and how to replace the latter with an API that makes the caller’s intention visible.
A boolean can represent data or choose behavior
Not every boolean parameter is a design problem. The important distinction is what the value means.
Suppose a notification record stores whether a message has been read:
notification.setRead(true)Here the boolean represents a property of the notification. true and false are meaningful values in the domain.
Now compare an export function:
exportReport(report, true)Assume true means “include confidential details” and false means “produce the public version.” The boolean is not merely describing data. It chooses between two meaningful operations.
That distinction gives us a useful mental model:
If callers think in verbs but the API asks them for a boolean, the boolean may be hiding an operation.
The question is therefore not “does this function accept a boolean?” It is “does the boolean force a caller to encode an important decision as true or false?”
Start with the smallest useful example
Imagine an invoice service with this interface:
sendInvoice(invoice, true)The second argument controls whether the service sends a reminder or the original invoice:
function sendInvoice(invoice, isReminder):
if isReminder:
subject = "Reminder: " + invoice.number
else:
subject = "Invoice " + invoice.number
sendEmail(invoice.customer, subject)The implementation is not difficult. The problem is at the call site:
sendInvoice(invoice, false)
sendInvoice(overdueInvoice, true)A reader must translate false and true into business meaning before understanding either line.
If callers already think of these as separate actions, expose those actions directly:
sendInvoice(invoice)
sendInvoiceReminder(overdueInvoice)The behavior can still share implementation internally:
function sendInvoice(invoice):
sendInvoiceEmail(invoice, "original")
function sendInvoiceReminder(invoice):
sendInvoiceEmail(invoice, "reminder")This change does not eliminate branching from the program. It moves the choice to a place where its meaning is explicit.
Put the decision where the knowledge lives
Boolean flags often appear because one function is convenient for the implementer. The function knows how both variants work, so adding one parameter feels cheaper than adding another operation.
But API design should also consider what the caller knows.
A caller usually knows its intent:
customer requested a replacement receiptIt should not need to translate that intent into an implementation convention:
printReceipt(order, true)An explicit API preserves the caller’s vocabulary:
printReplacementReceipt(order)This matters because the call site is where developers often review behavior. A descriptive operation lets a reviewer understand the decision without jumping to the callee.
The same idea applies to methods, constructors, service interfaces, command handlers, and other APIs. The syntax changes across languages, but the design question remains the same: can the caller state its intention directly?
Named arguments help readability, but not every design problem
Some languages support named arguments, which can turn an unclear call into something more readable:
exportReport(report, includeConfidentialDetails = true)This is a useful improvement when the boolean genuinely represents an option. The call now explains what true means.
However, naming the argument does not change the underlying abstraction. If the flag selects operations with different rules, permissions, failure modes, or future evolution, one function still owns multiple behaviors.
For example:
closeAccount(account, permanently = true)may look readable, but permanent closure and temporary suspension could have different authorization rules, audit requirements, notifications, and recovery behavior. In that case, these operations communicate the design more accurately:
suspendAccount(account)
closeAccountPermanently(account)Named arguments solve a call-site decoding problem. Separate operations solve an operation-modeling problem. Sometimes you need only the first.
Watch for flags that grow into modes
A boolean has exactly two values, but the decision it represents may not remain binary.
Suppose a file import starts with this API:
importFile(file, strict = true)Later, requirements add a third behavior: collect warnings but continue. A second boolean might appear:
importFile(file, strict = false, collectWarnings = true)Now some combinations may be invalid or unclear. What should strict = true, collectWarnings = true mean?
This is a sign that the original boolean represented a mode rather than independent data. An explicit type can model the valid choices:
ImportMode = STRICT | LENIENT | LENIENT_WITH_WARNINGS
importFile(file, mode)If each mode is still fundamentally the same operation with a policy variation, an explicit mode is often clearer than several separate functions. If the modes develop distinct workflows and contracts, separate operations may become more appropriate.
The goal is not to maximize the number of methods. The goal is to represent meaningful choices without making callers memorize boolean conventions or invalid combinations.
Multiple booleans deserve extra scrutiny
Consider this API:
generateReport(data, true, false, true)Even if every flag is legitimate, the call is difficult to review. Three independent booleans create eight possible combinations. Some may be valid, some redundant, and some impossible in the real domain.
Before replacing them mechanically, ask what each flag represents.
If they are independent formatting options, an options object can make them readable:
generateReport(data, {
includeSummary: true,
includeRawData: false,
compressOutput: true
})If the flags actually describe a small set of named report variants, model those variants instead:
generateExecutiveReport(data)
generateAuditReport(data)If certain combinations are invalid, use a representation that cannot express those combinations where practical.
The right refactoring depends on the meaning of the choices, not on the number of booleans alone.
Separate public intent from shared implementation
A common objection is that separate operations duplicate code. They do not have to.
Public APIs and internal implementation have different responsibilities. The public API should communicate valid intentions. Internal code can share mechanisms where that sharing is useful.
For example:
function publish(article):
changePublicationState(article, "published")
function unpublish(article):
changePublicationState(article, "draft")
function changePublicationState(article, targetState):
validateTransition(article.state, targetState)
article.state = targetState
save(article)The two public operations are explicit even though they share the transition mechanism.
This separation also gives each operation room to evolve. If publishing later requires a review check while unpublishing requires an audit reason, the API already has natural places for those rules.
Do not create a shared helper merely to avoid a few repeated lines. Share implementation when the behavior is genuinely the same mechanism and is likely to change together.
When a boolean parameter is the simpler choice
Replacing every boolean with a new operation would make many APIs worse. A boolean is often appropriate when it represents ordinary state or an independent option whose meaning is clear.
Examples include:
cache.setEnabled(false)
checkbox.setChecked(true)
render(page, showLineNumbers = true)In these cases, the two values naturally describe one concept. Creating renderWithLineNumbers and renderWithoutLineNumbers may add API surface without adding useful meaning, especially if line numbers are one of several independent presentation options.
A boolean can also be reasonable in a private helper where the call is local, named clearly, and unlikely to be misunderstood. Public or widely used APIs deserve more scrutiny because ambiguous conventions spread to more callers.
The cost of an abstraction matters. Prefer the smallest design that makes important decisions clear.
Common refactoring mistakes
The first mistake is treating the rule as “booleans are bad.” They are not. The problem is hidden intent, not the data type itself.
The second mistake is creating pairs of methods for every configurable option. That can produce a large API full of combinations such as renderCompactWithHeader and renderCompactWithoutHeader. Independent options usually belong in named configuration rather than a combinatorial set of operations.
The third mistake is keeping the boolean in the new public methods:
function publish(article):
updateArticle(article, true)
function unpublish(article):
updateArticle(article, false)This can be acceptable as a small private implementation detail, but only if updateArticle remains understandable and the two branches truly share a mechanism. If the helper grows into two unrelated workflows, the hidden split has simply moved inward.
The fourth mistake is choosing names that restate implementation rather than intent. processWithFlagEnabled is more explicit than process(x, true), but it still tells the caller about a flag. Prefer a domain action such as sendReminder, archiveOrder, or publishDraft when that is what the operation means.
A practical decision test
When reviewing a boolean parameter, ask four questions:
- Does the boolean describe a property, or does it select an action?
- Can a reader understand the call without looking up what
trueandfalsemean? - Do the two branches have different rules, permissions, side effects, or likely reasons to change?
- Is the choice likely to grow beyond two modes or combine with other flags?
One “yes” does not automatically require a refactoring. The questions reveal where the design pressure is.
If the value is ordinary data, keep the boolean. If it is an independent option, make its name visible through named arguments or an options object. If it selects a meaningful operation, consider separate operations. If it represents one choice among several modes, consider an explicit mode type.
Conclusion
Boolean parameters are inexpensive in syntax, but some of them make callers encode meaningful decisions as true and false. That cost appears at every call site: readers must decode the flag, reviewers must remember its convention, and future changes can turn one function into several workflows hidden behind conditions.
The useful rule is not to remove booleans. It is to make important choices explicit. Keep booleans when they represent genuine binary data or simple independent options. When a boolean is really choosing a verb, give that verb a name.