Loading learning content...
If method names are promises, parameters are the contract details. They specify exactly what information a method needs to fulfill its promise. Well-designed parameters make correct usage obvious and incorrect usage difficult. Poorly-designed parameters create landmines—invitations to subtle bugs that slip past code reviews and tests.
Consider this call:
createUser("John", "Doe", "john@example.com", true, false, true, null);
Without checking documentation, can you determine what those three booleans mean? What does null represent? This call site is a comprehension nightmare. Now compare:
createUser(UserCreationRequest.builder()
.firstName("John")
.lastName("Doe")
.email("john@example.com")
.enableNotifications(true)
.requireEmailVerification(false)
.grantAdminAccess(true)
.build());
Every piece of information is labeled. Mistakes are unlikely. The call is self-documenting.
Parameter design is not an afterthought—it's a core aspect of API usability that directly impacts correctness, maintainability, and developer experience.
By the end of this page, you will understand how to design parameters that prevent misuse, enhance readability, and scale gracefully as requirements evolve. You'll learn parameter count heuristics, typing strategies, ordering conventions, and the power of parameter objects.
How many parameters should a method have? This seemingly simple question has profound implications for API usability.
The Cognitive Load of Parameters
Each parameter adds cognitive load:
This load compounds multiplicatively. Two parameters means considering two things; five parameters means considering five things and their interactions.
| Count | Assessment | Recommendation |
|---|---|---|
| 0 | Perfect for pure queries | Preferred when no input needed |
| 1 | Excellent | Natural for single-entity operations |
| 2 | Good | Often optimal: subject and modifier |
| 3 | Acceptable | Upper limit for positional clarity |
| 4+ | Problematic | Consider parameter object refactoring |
| 5+ | Serious smell | Almost certainly requires refactoring |
The 'Three' Threshold
Three parameters is the practical threshold for positional clarity. Beyond three, developers frequently mix up parameter order, especially when multiple parameters share the same type:
// Dangerous: which is source, which is destination?
copyFile(path1, path2, true, false);
// Even worse: all strings
scheduleMeeting("10:00", "11:00", "Room A", "Team Sync", "Weekly standup");
When you find yourself adding a fourth, fifth, or sixth parameter, stop and reconsider the design.
Boolean parameters are especially problematic because true and false carry no semantic meaning at the call site. copyFile(src, dst, true) tells you nothing. Every boolean parameter should trigger consideration of alternative designs: separate methods, enums, or builder patterns.
When multiple parameters are unavoidable, their ordering matters. Consistent ordering creates predictability; inconsistent ordering creates confusion.
Standard Ordering Principles
copyFile(source, destination), the source is the primary subject.send(destination, message) puts destination first, then receive(destination, ...) should too.12345678910
// Inconsistent: sometimes destination first,// sometimes message firstfunction send(destination, message) { }function receive(message, source) { }function forward(recipient, originalMsg) { }function reply(message, sender) { } // Caller constantly guesses:send(server, data);receive(response, server); // Wait, is this right?12345678910
// Consistent: endpoint always first,// message always secondfunction send(destination, message) { }function receive(source, buffer) { }function forward(recipient, originalMsg) { }function reply(recipient, responseMsg) { } // Caller develops intuition:send(server, data);receive(server, response); // Intuitive!Language-Specific Patterns
Some languages have established ordering idioms:
**kwargs come last// Go convention: context first
func GetUser(ctx context.Context, userID string) (*User, error)
func CreateOrder(ctx context.Context, order Order) error
// JavaScript convention: options/callback last
function readFile(path: string, options?: ReadOptions): Promise<Buffer>
function setTimeout(callback: () => void, delay: number): number
Respect your language's conventions. If Go puts context first, put context first. If JavaScript puts callbacks last, put callbacks last. Fighting established idioms creates friction for developers who carry expectations from mainstream usage patterns.
Parameter types are your first line of defense against misuse. Strong, specific types catch errors at compile time rather than runtime.
The Primitive Obsession Problem
Primitive obsession—using strings, integers, and booleans where domain types are appropriate—is one of the most common API design mistakes:
1234567891011121314151617
// Primitive obsession: everything is a stringfunction transferMoney( fromAccount: string, toAccount: string, amount: number, currency: string): void // Easy to make mistakes:transferMoney( "USD", // Oops, currency first "ACC123", 100, "ACC456" // Oops, account); // Compiler won't catch this!123456789101112131415161718
// Strong types: domain conceptstype AccountId = { _brand: 'AccountId'; value: string };type Money = { amount: number; currency: Currency }; function transferMoney( from: AccountId, to: AccountId, amount: Money): void // Mistakes caught at compile time:transferMoney( Currency.USD, // ERROR: not AccountId accountId("ACC123"), money(100, Currency.USD)); // Type system protects you!Strategies for Type Safety
UserId vs OrderId, even when both are strings underneath.boolean with enum when there are exactly two options: EmailVerification.REQUIRED | OPTIONAL instead of requireVerification: boolean.Money, EmailAddress, PhoneNumber, DateRange.Optional<User>, Result<T, Error>, or explicit None types.Source = FileSource | UrlSource | StreamSource.1234567891011121314151617181920
// Boolean: meaning unclear at call sitefunction createUser(name: string, isAdmin: boolean, sendWelcome: boolean): UsercreateUser("Alice", true, false); // What do these booleans mean? // Enums: meaning crystal clearenum UserRole { STANDARD, ADMIN }enum WelcomeEmail { SEND, SKIP } function createUser(name: string, role: UserRole, welcome: WelcomeEmail): UsercreateUser("Alice", UserRole.ADMIN, WelcomeEmail.SKIP); // Self-documenting! // Even better: use a config object for multiple optionsinterface CreateUserConfig { name: string; role: UserRole; welcomeEmail: WelcomeEmail; emailVerification: EmailVerification;} function createUser(config: CreateUserConfig): UserStrong types require more upfront code—type definitions, converters, validators. This is not waste; it's investment. The cost is paid once at design time; the benefit is reaped at every call site, every code review, and every debugging session.
When parameter counts grow or when many parameters are optional, parameter objects provide a superior alternative to long parameter lists.
The Parameter Object Pattern
A parameter object groups related parameters into a single cohesive type:
12345678910111213141516171819202122232425262728293031323334353637383940
// Before: Long parameter listfunction sendEmail( to: string, cc: string[], bcc: string[], subject: string, body: string, isHtml: boolean, attachments: Attachment[], replyTo: string, priority: Priority, trackOpens: boolean, trackClicks: boolean): Promise<void> // After: Parameter objectinterface EmailRequest { to: string; cc?: string[]; bcc?: string[]; subject: string; body: string; format?: 'text' | 'html'; attachments?: Attachment[]; replyTo?: string; priority?: Priority; tracking?: TrackingOptions;} function sendEmail(request: EmailRequest): Promise<void> // Call site is clean and self-documentingawait sendEmail({ to: "user@example.com", subject: "Welcome!", body: "<h1>Hello</h1>", format: 'html', priority: Priority.HIGH, tracking: { opens: true, clicks: false }});The Builder Pattern
For complex objects with many optional fields and validation requirements, the Builder pattern provides a fluent construction API:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
// Builder pattern for complex request objectspublic class HttpRequest { private final String url; private final HttpMethod method; private final Map<String, String> headers; private final Body body; private final Duration timeout; private final RetryPolicy retryPolicy; private HttpRequest(Builder builder) { this.url = Objects.requireNonNull(builder.url); this.method = Objects.requireNonNull(builder.method); this.headers = Map.copyOf(builder.headers); this.body = builder.body; this.timeout = builder.timeout != null ? builder.timeout : Duration.ofSeconds(30); this.retryPolicy = builder.retryPolicy != null ? builder.retryPolicy : RetryPolicy.none(); } public static Builder builder() { return new Builder(); } public static class Builder { private String url; private HttpMethod method = HttpMethod.GET; private Map<String, String> headers = new HashMap<>(); private Body body; private Duration timeout; private RetryPolicy retryPolicy; public Builder url(String url) { this.url = url; return this; } public Builder method(HttpMethod method) { this.method = method; return this; } public Builder header(String name, String value) { this.headers.put(name, value); return this; } public Builder body(Body body) { this.body = body; return this; } public Builder timeout(Duration timeout) { this.timeout = timeout; return this; } public Builder retryPolicy(RetryPolicy policy) { this.retryPolicy = policy; return this; } public HttpRequest build() { // Validation happens here if (url == null || url.isBlank()) { throw new IllegalStateException("URL is required"); } return new HttpRequest(this); } }} // Usage: fluent, readable, type-safeHttpRequest request = HttpRequest.builder() .url("https://api.example.com/users") .method(HttpMethod.POST) .header("Content-Type", "application/json") .header("Authorization", "Bearer " + token) .body(Body.json(userData)) .timeout(Duration.ofSeconds(10)) .retryPolicy(RetryPolicy.exponentialBackoff(3)) .build();build() method can validate and fail fast.Use builders when: (1) there are more than 4-5 parameters, (2) most parameters are optional, (3) construction requires validation, or (4) the object must be immutable. For simpler cases, a plain parameter object (record, data class) suffices.
Optional parameters introduce flexibility but also complexity. Different strategies suit different scenarios.
Method Overloading for Optional Parameters
Languages like Java and C# support method overloading—multiple methods with the same name but different parameter lists:
// Overloaded methods with progressive parameter addition
public User getUser(UserId id) {
return getUser(id, FetchOptions.defaults());
}
public User getUser(UserId id, FetchOptions options) {
// Implementation
}
// Overloaded methods for different use cases
public List<User> findUsers() {
return findUsers(Query.all());
}
public List<User> findUsers(Query query) {
return findUsers(query, Pagination.unpaged());
}
public List<User> findUsers(Query query, Pagination page) {
// Implementation
}
Guidelines for Overloading:
Use overloading when you have 1-2 optional parameters and clear progression. Use default values when your language supports them and defaults are truly universal. Use options objects when you have 3+ optional parameters or expect to add more options later. Builders are for complex construction with validation requirements.
Sometimes methods need to accept a variable number of items. Languages provide different mechanisms for this.
Varargs (Variable Arguments)
Varargs allow a method to accept zero or more arguments of the same type:
123456789101112131415161718
// Java varargspublic static <T> List<T> listOf(T... elements) { return Arrays.asList(elements);} // UsageList<String> names = listOf("Alice", "Bob", "Charlie");List<Integer> numbers = listOf(1, 2, 3, 4, 5);List<String> empty = listOf(); // Zero args works too // TypeScript rest parametersfunction sum(...numbers: number[]): number { return numbers.reduce((a, b) => a + b, 0);} // Usageconst total = sum(1, 2, 3, 4, 5); // 15const zero = sum(); // 0Varargs Guidelines
Integer... nums, not int... nums for generic use.List<T> for callers who already have a collection.Collection Parameters: List vs Array vs Iterable
When accepting a collection of items, choose the most appropriate type:
| Type | When to Use |
|---|---|
Array / T[] | When fixed size is expected, or for varargs |
List<T> | When order matters, random access needed |
Set<T> | When uniqueness is the semantic |
Collection<T> | When size and iteration are sufficient |
Iterable<T> | Maximum flexibility, lazy evaluation okay |
Defensive Copying
When accepting mutable collections, consider whether to make a defensive copy:
// Dangerous: caller can mutate the list after passing it
public void setItems(List<Item> items) {
this.items = items; // Alias to caller's list!
}
// Safe: defensive copy
public void setItems(List<Item> items) {
this.items = List.copyOf(items); // Immutable copy
}
Decide and document whether null elements are permitted in collection parameters. Prefer rejecting nulls: Objects.requireNonNull() each element or use List.of() style factories that reject nulls. Allowing nulls complicates implementation and creates ambiguity.
Parameters represent the preconditions of a method. Validating these preconditions early—at method entry—prevents errors from propagating into difficult-to-debug states.
The Fail-Fast Principle
When a parameter is invalid, fail immediately with a clear error rather than allowing the method to proceed with bad data and fail later in cryptic ways.
1234567891011121314
// Fail slow: validation happens during usepublic BigDecimal calculateTax(BigDecimal price) { // No validation at entry // ... 50 lines of code ... // Null pointer exception here, stack trace // points to this line, not the real cause return price.multiply(TAX_RATE);} // Caller sees:// NullPointerException at Line 52// Where did null come from? 🤔123456789101112131415
// Fail fast: validate at method entrypublic BigDecimal calculateTax(BigDecimal price) { Objects.requireNonNull(price, "price"); if (price.compareTo(BigDecimal.ZERO) < 0) { throw new IllegalArgumentException( "price must not be negative: " + price); } // Safe to proceed return price.multiply(TAX_RATE);} // Caller sees:// IllegalArgumentException: price must not be negative: -5// Crystal clear! 🎯Common Validation Patterns
1234567891011121314151617181920212223242526272829303132333435363738394041
// Validation utility functionsfunction requireNonNull<T>(value: T | null | undefined, name: string): T { if (value === null || value === undefined) { throw new Error(`${name} must not be null`); } return value;} function requireNonEmpty(value: string, name: string): string { if (!value || value.trim().length === 0) { throw new Error(`${name} must not be empty`); } return value;} function requirePositive(value: number, name: string): number { if (value <= 0) { throw new Error(`${name} must be positive: ${value}`); } return value;} function requireInRange(value: number, min: number, max: number, name: string): number { if (value < min || value > max) { throw new Error(`${name} must be between ${min} and ${max}: ${value}`); } return value;} // Usage in a methodfunction createAccount(name: string, balance: number, type: AccountType): Account { requireNonEmpty(name, 'name'); requireNonNull(type, 'type'); // Balance can be zero or positive for new accounts if (balance < 0) { throw new Error(`Initial balance cannot be negative: ${balance}`); } // Validation passed, safe to proceed return new Account(name, balance, type);}IllegalArgumentException, NullPointerException, or custom validation exceptions.Validate most strictly at system boundaries—where external input enters your system (HTTP handlers, message consumers, file readers). Internal method calls between trusted components can sometimes rely on the boundary validation, but public API methods should always validate.
Parameter design is where the rubber meets the road in API usability. Well-designed parameters make correct usage obvious and incorrect usage difficult. Let's consolidate the principles:
What's Next
With method names and parameters mastered, we turn to the other side of the signature: return types. The next page explores how to design return values that are intuitive, safe, and expressive—including handling null, errors, and complex result structures.
You now understand the principles and patterns for designing method parameters. These skills will help you create APIs that are safe, intuitive, and resistant to misuse. Next, we'll explore return type design—how to communicate results and handle edge cases elegantly.