A function can be perfectly implemented and still be easy to call incorrectly. One common cause is a parameter list in which several values have the same representation and their meaning depends mainly on position.

Consider a reporting function that accepts three dates. At the call site, the values may all look valid even when two of them are accidentally reversed. A compiler or type checker often cannot help because each argument still has the expected type.

The useful mental model is: a positional argument carries two pieces of information—the value itself and the meaning assigned to its position. When the value’s type does not communicate that meaning, the caller must remember more of the function’s contract.

This article explains how to recognize that coupling, how to reduce it, and when a simple positional call is still the clearer design.

See the information hidden in position

Start with a small example:

scheduleReminder("order-42", 10, 30)

Suppose the function is defined as:

scheduleReminder(orderId, delayMinutes, retryMinutes)

The string is easy to identify as an order ID. The two numbers are different. Nothing in the call itself, however, tells a reader whether 10 is the initial delay or the retry interval.

Now imagine this accidental call:

scheduleReminder("order-42", 30, 10)

Both numbers are valid integers. Both may also be valid durations. The call can pass basic type checks while expressing the wrong policy.

The problem is not that positional arguments are inherently bad. The problem is that position is doing work that the values and their types do not make visible.

That creates a form of coupling between caller and callee. If the caller must know that the second integer means one thing and the third integer means another, correctness depends on preserving that ordering knowledge.

Measure the risk by how easy arguments are to confuse

Not every multi-parameter function deserves redesign. Compare these calls:

resizeImage(image, 800, 600)
connect(host, port)
move(source, destination)

The first call may be familiar enough in a local context if the convention is clearly width then height. In the second, a string-like host and numeric port are difficult to swap accidentally. In the third, however, source and destination may have exactly the same type and both positions are semantically important.

A useful question is therefore not “How many parameters does this function have?” It is:

If two arguments were exchanged, would the call still look plausible to the language, tooling, and reviewer?

Risk increases when several conditions appear together:

  • adjacent parameters share the same primitive or broad type;
  • valid values overlap, so a swapped value is not rejected;
  • the call is uncommon enough that developers do not remember its order;
  • parameter names are invisible at the call site;
  • the consequences of reversal are delayed or difficult to detect.

For example, swapping width and height may produce an obviously wrong image. Swapping minimumAge and maximumAge could instead produce a subtle rule error that survives until particular data reaches production.

The consequence should influence how much interface design effort is justified.

Make meaning visible at the call site

The smallest useful improvement is often to expose the parameter names, when the language supports named arguments.

Instead of:

createWindow(100, 40, 800, 600)

use a call shaped like:

createWindow(
    x = 100,
    y = 40,
    width = 800,
    height = 600
)

This does not change the underlying values. It changes what a reader can learn from the call without navigating to the function definition.

Named arguments are especially useful when the language treats them as part of the function’s supported calling convention. In languages where parameter names are not stable API surface, or where named arguments are unavailable, another technique may be more appropriate.

Do not imitate named arguments with comments such as this unless there is no better local option:

createWindow(100, 40, 800, 600)  // x, y, width, height

The comment can drift away from the signature during later changes. A mechanism checked by the language or tooling carries more reliable information.

Give different concepts different types when the distinction matters

Names improve readability, but they may not prevent accidental substitution. If two values represent genuinely different concepts, distinct domain types can move part of the contract into the type system.

Suppose an API accepts an account ID and an invoice ID. Both happen to be strings:

applyPayment(accountId, invoiceId, amount)

A caller can accidentally write:

applyPayment(invoiceId, accountId, amount)

If both IDs are plain strings, the call may still satisfy the function signature.

A stronger interface can distinguish the concepts:

applyPayment(AccountId accountId, InvoiceId invoiceId, Money amount)

Now the implementation language may be able to reject a reversed pair before execution, depending on how those domain types are represented.

This technique is valuable when the distinction is important throughout the system, not merely in one function. Creating a new wrapper type for every integer or string can add conversion code and conceptual overhead without meaningful protection.

Use a distinct type when it represents a stable domain distinction that developers should preserve across multiple operations.

Group values when they form one concept

Sometimes several parameters belong together rather than merely needing labels.

Consider:

findOrders(customerId, startDate, endDate, includeCancelled)

If the date pair repeatedly travels through the code as one range, represent that idea directly:

findOrders(customerId, DateRange(startDate, endDate), includeCancelled)

A DateRange can also own rules that belong to the pair, such as requiring the start not to occur after the end.

The important reason to introduce the object is not to shorten the parameter list. It is to give a recurring concept a home.

This distinction prevents a common overcorrection. Replacing every long parameter list with a generic options object can hide which values are required and can create a bag of unrelated settings. Group parameters when they have cohesion: they change together, are validated together, or represent one concept to callers.

Prefer intention-revealing operations when modes differ

Some positional confusion indicates that one function is serving several distinct intentions.

Imagine:

copyFile(source, destination, true)

If the Boolean means overwriteExisting, the reader must remember both the third parameter’s meaning and what true selects.

An interface might instead expose the decision explicitly:

copyFile(source, destination, overwrite = true)

Or, when overwrite and non-overwrite behavior are meaningful operations with different expectations, the API might offer separate operations:

copyFile(source, destination)
replaceFile(source, destination)

The second design is not automatically superior. Separate operations make sense when they express stable, meaningful actions. If there are many independent options, multiplying function names for every combination becomes worse than a structured options object.

The goal is to expose the caller’s intention, not to eliminate parameters mechanically.

Keep simple calls simple

Reducing positional coupling has costs. Named arguments can make short, familiar calls verbose. Wrapper types require construction and conversion. Parameter objects introduce another abstraction. Separate operations enlarge an API.

A two-argument function such as:

contains(text, substring)

may already be clear because the operation, types, naming conventions, and surrounding context make the roles obvious. Adding TextToSearch and SubstringToFind wrapper types would likely make ordinary code harder to use without preventing an important class of mistakes.

Prefer the smallest mechanism that makes the risky distinction visible.

A practical progression is:

  1. Keep positional arguments when their roles are obvious and hard to confuse.
  2. Use named arguments when labels solve the reading problem.
  3. Introduce domain types when accidental substitution should be rejected and the concepts recur.
  4. Group parameters when they form one coherent value.
  5. Split operations when different argument combinations represent genuinely different actions.

This progression is a decision guide, not a required sequence. A public API with costly misuse may justify stronger types immediately, while a small private helper may need no change at all.

Watch for redesigns that move confusion elsewhere

An interface can look cleaner while preserving the original problem.

A generic map is one example:

scheduleReminder({
    "order": "order-42",
    "delay": 10,
    "retry": 30
})

The labels are visible, but if the map accepts arbitrary keys and values, misspellings or wrong value types may move failures from development time to runtime. A typed options structure can provide labels without giving up structural checks.

Another mistake is introducing one large configuration object shared by unrelated operations. Callers may then construct fields they do not need, and developers can no longer tell from the function signature which inputs are actually required.

Finally, do not treat a shorter signature as proof of better design. This:

process(request)

is not clearer than a four-parameter function if request is an unstructured container whose fields have hidden relationships. The useful question remains whether the interface makes important distinctions visible and enforceable.

Refactor with compatibility in mind

Changing a parameter list can break callers even when runtime behavior stays the same. For an internal function with a few callers, updating them together may be straightforward. A public or widely used API needs more care.

A common migration shape is to introduce the clearer interface while temporarily adapting the old one:

oldScheduleReminder(orderId, delay, retry):
    return scheduleReminder(
        ReminderPolicy(orderId, delay, retry)
    )

Callers can move incrementally, and tests can verify that the adapter preserves the old behavior. After supported callers migrate, the compatibility entry point can be removed according to the project’s versioning policy.

Do not keep both interfaces indefinitely without a reason. Permanent aliases increase the surface area future maintainers must understand.

Use the interface to carry the contract

Positional arguments are compact because they omit labels. That compactness is useful when the omitted information is obvious. It becomes risky when several interchangeable-looking values carry different meanings.

When reviewing a call, look for arguments that could be swapped while still appearing valid. Then choose the lightest design that exposes the distinction: names, domain types, cohesive parameter objects, or separate operations.

The practical goal is not zero positional arguments. It is an interface where a correct call is easy to understand and a plausible mistake is as difficult to express as the engineering context justifies.