Picture this. Three files. One function name. Five compiler errors.
The function was called searchClientLeads. It existed in a repository interface, in a use case, and in a BLoC. All three files agreed it should take a query string, a page number, and a page size. None of the three files agreed on how those parameters should be passed. The compiler lit up: Too many positional arguments: 1 allowed, but 3 found. Then The named parameter 'query' isn't defined. Then three more. Five errors from a single conceptual mismatch — and understanding why they cascaded is the part that stays with you.
That mismatch was about function signatures. Specifically, about the difference between positional and named parameters in Dart — and what happens when one layer of your architecture defines a function one way and another layer calls it another way.
What a Signature Actually Is
A function signature is a contract. It declares three things: the function's name, the types it expects to receive, and the type it promises to return. When you call a function, you're agreeing to honor that contract. When you define a function, you're committing to it.
Future<Either<Failure, PaginatedClientLeads>> searchClientLeads(
String query, {
int page = 1,
int pageSize = 20,
});This signature says: give me a String, optionally tell me which page and how large pageSize should be, and I'll give you back a Future that resolves to either a Failure or a PaginatedClientLeads. That's the contract. Every caller has to honor it.
The interesting part — the part that caused five errors — is in the details of how you hand those values over.
Positional Parameters: Order Is Everything
The simplest kind of parameter. You pass values in the order they appear in the signature, and Dart maps them by position. No labels. No ambiguity about which value goes where — as long as you remember the order.
String greet(String name, String title) {
return 'Hello, $title $name';
}
greet('Turing', 'Dr.'); // Hello, Dr. Turing
greet('Dr.', 'Turing'); // Hello, Turing Dr. — wrong, but validThe second call compiles without complaint. The compiler sees two strings and hands them over. Whether they land in the right places is entirely your responsibility. This is the tradeoff of positional parameters: they're concise, but they put the burden of remembering the order on whoever calls the function.
In Dart, positional parameters are required by default. If the signature says greet(String name, String title), you must pass both. Skip one and the compiler stops you.
Making Positional Parameters Optional
You can make positional parameters optional by wrapping them in square brackets:
String greet(String name, [String title = 'Mr.']) {
return 'Hello, $title $name';
}
greet('Turing'); // Hello, Mr. Turing
greet('Turing', 'Dr.'); // Hello, Dr. TuringThe square brackets tell Dart that anything inside them can be omitted. If omitted, the default value kicks in — or null if no default is given and the type is nullable.
One constraint: optional positional parameters must come after required ones. You can't put an optional parameter before a required one. ([String? title], String name) is not valid.
Named Parameters: Clarity Over Conciseness
Named parameters flip the deal. Instead of relying on position, you attach a label to each value when you call the function. This makes the call site readable and makes argument order irrelevant.
void createLead({required String name, required String email}) { ... }
createLead(name: 'Alice', email: 'alice@example.com');
createLead(email: 'alice@example.com', name: 'Alice'); // identicalNamed parameters in Dart live inside curly braces {} in the signature. By default, they're optional — you can omit them, and they'll be null (or whatever default you gave them). If you want to require them, you add the required keyword.
// Optional named — can be omitted, defaults to null or the given value
void search({String? query, int page = 1, int pageSize = 20}) { ... }
// Required named — must be provided, in any order
void createUser({required String name, required String email}) { ... }Named parameters shine when a function has several parameters of the same type, or when the meaning of a value isn't obvious from its position alone. Compare these two calls:
// Positional: what does true mean here?
schedule(meeting, true, false);
// Named: no ambiguity
schedule(meeting, sendReminder: true, allowOverlap: false);The named version is self-documenting. Anyone reading it understands what they're looking at without checking the signature. This matters most at layer boundaries — where the caller and the implementation live in different files, maybe written at different times. Positional parameters work fine inside a single file where context is obvious. But page: 2 at a call site three directories away is unambiguous in a way that a bare 2 never will be.
Mixing Positional and Named
Dart allows — and in practice encourages — mixing the two. The canonical pattern is: required, self-evident parameters come first as positional; optional or context-specific parameters follow as named.
Future<Results> searchClientLeads(String query, {int page = 1, int pageSize = 20});query is positional because it's the one thing this function absolutely requires and its meaning is unambiguous from context. page and pageSize are named because they're optional configuration — they have sensible defaults, and labeling them at the call site makes the code clearer.
The call looks like this:
repository.searchClientLeads('active leads', page: 2, pageSize: 50);query goes first, positionally. page and pageSize follow with their labels. This is idiomatic Dart.
The Full Picture
Here's every parameter flavor in one place:
void example(
String required, // positional, required
[String optional = 'default'], // positional, optional
) { ... }
void example2(
String required, { // positional required + named
required String alsoRequired, // named, required
String optional = 'default', // named, optional with default
String? nullable, // named, optional, defaults to null
}) { ... }Two things you cannot do: mix [] and {} in the same signature, and put optional positional parameters before required ones.
What Went Wrong: Three Files, Three Contracts
Back to the five errors.
The repository interface defined the contract correctly — query positional, page and pageSize named:
// repository:
Future<Either<Failure, PaginatedClientLeads>> searchClientLeads(
String query, {
int page = 1,
int pageSize = 20,
});The use case, which wraps the repository, defined its own call method — but with all three parameters as positional:
// use case — wrong:
Future<Either<Failure, PaginatedClientLeads>> call(String query, int page, int pageSize) {
return repository.searchClientLeads(query, page, pageSize); // also wrong
}Two problems here. First, the call signature doesn't match what the BLoC expected. Second, the repository call inside it passes page and pageSize positionally — but the repository signature says they're named. The compiler sees three positional arguments where only one is allowed: Too many positional arguments: 1 allowed, but 3 found. One error.
The BLoC, written with the repository's interface in mind, called the use case with named parameters:
// bloc — assumes named params:
await searchClientLeadsUseCase(
query: _currentSearchQuery!,
page: newFilter.page,
pageSize: newFilter.pageSize,
);The compiler read the use case's call signature — positional — and found zero positional arguments, three undefined named parameters. That's four more errors, for a total of five.
The fix was exactly two changes:
// use case — corrected:
Future<Either<Failure, PaginatedClientLeads>> call(String query, {int page = 1, int pageSize = 20}) {
return repository.searchClientLeads(query, page: page, pageSize: pageSize);
}The call signature now uses named params for page and pageSize, matching what the BLoC expects. The repository call inside uses named params too, matching what the repository contract requires. The three files now agree on the contract. The compiler is satisfied. The five errors disappear.
Why This Matters in Clean Architecture
The scenario above involved three distinct layers: the domain repository interface, the use case, and the BLoC. In clean architecture, each layer has a clearly defined responsibility. The repository defines what data operations exist. The use case defines how they're orchestrated. The BLoC defines when they're triggered.
Each layer communicates with the next through function signatures — and those signatures are the API between your layers. The parameter types, names, and passing style all need to match on both sides. When a signature in the middle layer is inconsistent — accepting parameters one way from above and passing them another way to below — the mismatch cascades in both directions. The layer above can't call it correctly, and the layer below doesn't receive what it expects. One wrong signature in the use case generated errors both upstream and downstream simultaneously.
This is worth a quick distinction. "Layered architecture" and "clean architecture" get used interchangeably, but they're different patterns:
Layered architecture is the traditional approach — think presentation → business logic → data access, stacked top to bottom. Each layer depends on the one directly below it. Dependencies flow downward. The data layer knows nothing about the business layer, and the business layer knows nothing about the presentation. It's straightforward, but it means your business logic depends directly on your data implementation. Change your database, and your business layer feels it.
Clean architecture inverts that dependency. The domain layer sits at the center and depends on nothing. The repository interface lives in the domain — it defines the contract without knowing whether the implementation talks to PostgreSQL, an API, or a JSON file. The use case orchestrates domain logic. The BLoC (or controller, or whatever your framework uses) lives at the outer edge, calling inward. Dependencies point toward the center, not downward.
The bug in this article could only happen in clean architecture, because it requires a repository interface that the use case must honor. In plain layered architecture, the use case would call the data layer's concrete implementation directly — no interface contract to mismatch against.
That extra layer of indirection — the interface — is precisely what caught the error. The compiler checked whether the use case honored the repository's contract and whether the BLoC honored the use case's contract. Two boundaries, two checks, five errors that would have been runtime surprises in a less structured codebase.
This is actually the best-case version of this bug. Dart's type system caught it at compile time. Five errors sounds like a lot for a single conceptual mistake, but that's five problems caught before the app ran, before a user saw wrong search results, before you spent an afternoon debugging why page 2 returns page 1 data. The same inconsistency in a dynamically typed language would compile silently, run with incorrect arguments, and surface as a runtime error — or worse, as subtly wrong data that passes through the system undetected.
There's one more subtlety worth noting. In the use case's broken version, page and pageSize were passed positionally to a method that expected them as named. The compiler flagged the positional overflow — but the named parameters themselves, having defaults, would have silently used page: 1 and pageSize: 20 regardless of what the caller intended. Defaults are convenient, but they can also mask a wiring mistake. If the use case had only passed query positionally and skipped page and pageSize entirely, the code would have compiled and run — always returning page 1 with 20 results, no matter what the BLoC requested. No error, no warning. Just silently wrong pagination.
The verbosity of explicit parameter labels and the required keyword aren't ceremony. They're the compiler's way of making sure that everyone who calls your function has thought carefully about what they're passing and why.