Long Parameter List: The Chai Order That Took Ten Instructions
Long Parameter List code smell made simple — why methods with too many arguments cause bugs, and how parameter objects make calls short, clear, and safe.
🎯 "Bhaiya, My Usual!" — The Tale of Two Chai Orders
Outside the NexByte office in Hyderabad there is a tapri — a small roadside tea stall run by Shankar bhaiya, who has been making chai on that corner for fifteen years.
Every evening, Montu, the new intern at NexByte, places his order like this:
"Bhaiya, one tea. Less sugar. Extra ginger. No cardamom. Full milk. Small glass. Very hot. Strong leaves. No cream on top. And in a paper cup, takeaway."
Ten instructions. Every. Single. Day. Some days he forgets one, and the chai comes wrong. Once he said "extra sugar, less ginger" instead of "less sugar, extra ginger" — the order swapped in his mouth — and he drank a syrupy disaster.
Now watch Ravi, the senior developer who mentors Montu. He walks up and says two words: "Bhaiya, usual!"
Shankar bhaiya nods. He already knows Ravi's combination — it is stored, named, and trusted. One word carries all ten instructions, perfectly, every time. And when Ravi decided last month to switch from sugar to jaggery, he told Shankar bhaiya once. Every future "usual" automatically updated.
The story does not end at the stall. Shankar bhaiya's son is computerising the tapri, and guess who volunteered to write the ordering app? Montu. And guess how Montu wrote the main function? Exactly the way he orders: ten loose parameters, in exact sequence, every call. When Ravi reviews the code the next morning, he laughs out loud. "Montu, you have written your own chai order into the codebase. This smell has a name — Long Parameter List."
In code, a Long Parameter List is the intern's order: a function that demands ten loose values, in the right sequence, every single time it is called. And the cure is Ravi's trick: bundle the instructions into one named thing, and pass that. This lesson follows Ravi and Montu through one code review that fixes the tapri app — and Montu's evenings.
💡 What is this smell?
A quick reminder of our golden rule: a code smell is not a bug. Code with a twelve-parameter function can run perfectly. The smell is a warning that calling, reading, changing, and testing this code will keep getting more painful. Long Parameter List is one of the "Bloater" smells from Martin Fowler's Refactoring — here it is the signature of the method that has bloated.
A Long Parameter List is a method signature with so many parameters that:
- callers struggle to supply them in the correct order,
- readers cannot tell what a call means without opening the declaration, and
- every new piece of data forces edits to many signatures and call sites.
How many is "so many"? Common guidance says the alarm starts ringing at four or more. Robert C. Martin, in Clean Code, is even stricter: the ideal number of arguments is zero, one and two are fine, three should already make you pause, and more than three requires very special justification. You need not follow these numbers like court orders — but they tell you which direction is healthy.
The smell is rarely about the count alone. The real questions are: do these parameters belong together? Do the same groups repeat across methods? Are same-typed neighbours easy to swap? Count is the alarm; relationships are the disease.
College corner: A parameter list is part of a function's interface, and interfaces are contracts that should hide decisions, not expose them — this is Parnas's information hiding principle from 1972. Ten loose parameters expose ten decisions to every caller. A parameter object re-hides them behind one named concept, so the contract survives change: adding a field to the object does not break a single signature.
👃 How to spot it
Ravi opens Montu's pull request and narrates his checklist out loud. Scan your function signatures with it:
- A method takes four, five, or more parameters, and call sites are long rows of mystery values.
- Several parameters share the same type —
transfer(fromAccount, toAccount)ormove(x1, y1, x2, y2)— inviting silent swaps. - Some parameters are just passed through untouched to another function — the method is a courier for data it never uses.
- Boolean flags appear:
render(doc, true, false, true)— what on earth do the booleans control? - The same cluster of parameters appears in method after method (street, city, PIN... again and again).
- Adding one new piece of related data means editing many signatures and every caller.
| Symptom | What it tells you |
|---|---|
f(a, b, c, d, e, f) at call sites | Nobody can read the call without the declaration open in another window |
| Neighbouring parameters of one type | A transposition bug is waiting; the compiler will not catch the swap |
| Parameters forwarded untouched | The data wants to travel as one object, not as loose pieces |
true, false, true in calls | Hidden behaviour switches; the method may secretly be two methods |
| Same group repeats across signatures | A Data Clump — a missing class — is knocking on the door |
| One new field touches 14 files | The loose values have spread; change has no single home |
Montu's orderChai ticks five of six boxes. The only one it misses is the pass-through — and only because the app has just one function so far.
⚠️ Why it is a problem
- Error-prone calls. Positional arguments of the same type are silently swappable.
createBooking("Delhi", "Mumbai")versuscreateBooking("Mumbai", "Delhi")— both compile, one is a refund waiting to happen. - Unreadable call sites.
setupAccount("Asha", 14, true, false, 2500, "B")carries no meaning. Readers must keep the signature open beside every call, like reading a letter with a dictionary in the other hand. - Brittle signatures. Add one parameter and every caller breaks. Reorder two and some callers break silently. The method becomes expensive to evolve, so people stop evolving it.
- Testing friction. Every test must build the full argument list even to exercise one behaviour. Test files fill with noise that hides the actual point of each test.
- It hides deeper smells. A long list usually contains Data Clumps (groups that belong together) and is often fed by Primitive Obsession (everything passed as loose strings and numbers). And a method that needs ten inputs is often doing too much — a Long Method wearing a long signature.
How does the list grow so long? The same way all bloaters grow — one harmless step at a time:
Notice the loop at the bottom of the figure — that is the trap. Once the list is long and the call sites are many, adding one more parameter is always cheaper today than redesigning. So the list only ever grows.
Ravi then dissects Montu's actual signature on the whiteboard. What kind of parameters fill the list? Mostly the most dangerous kinds:
Six booleans out of ten parameters. Every neighbouring pair of them is a silent swap waiting to happen. And the cost of reading a call site grows sharply with each added parameter, because readers must match positions, not names:
Eighty seconds to decode one line — and code is read far more often than it is written.
🧪 Montu's evening, in two diagrams
Before fixing the code, Ravi shows Montu that he already knows both designs — he lives them daily at the tapri. First, the smelly protocol:
Same stall, same chaiwala, completely different error rates. The difference is the shape of the message: loose positional values versus one named, stored bundle. Montu's daily experience as a journey:
"Your function's callers," Ravi says, "are all living your evening. Every call site recites ten values and prays."
📊 Which signatures should you fix first?
NexByte's codebase has dozens of long signatures. Ravi teaches Montu to rank them on two axes: how many parameters, and how many call sites. Many parameters at many call sites is an emergency; a long list called once is merely untidy.
orderChai is the app's most-called function and its longest signature — top right corner, fix it today.
🧪 A real-life code example
Here is the function exactly as Montu wrote it. The stall's ordering app, with the intern's nightmare at its core:
function orderChai(
customerName: string,
sugarSpoons: number,
milkLevel: string, // "none" | "half" | "full"
ginger: boolean,
cardamom: boolean,
strong: boolean,
cupSize: string, // "small" | "regular"
veryHot: boolean,
takeaway: boolean,
paperCup: boolean,
): string {
let price = cupSize === "small" ? 10 : 15;
if (ginger) price += 2;
if (cardamom) price += 3;
if (takeaway && paperCup) price += 1;
let desc = customerName + ": " + cupSize + " chai, " +
sugarSpoons + " sugar, " + milkLevel + " milk";
if (ginger) desc += ", ginger";
if (cardamom) desc += ", cardamom";
if (strong) desc += ", strong";
if (veryHot) desc += ", very hot";
if (takeaway) desc += paperCup ? ", takeaway (paper cup)" : ", takeaway";
return desc + " = Rs." + price;
}
// The intern's daily call. Can YOU spot which boolean is which?
orderChai("Intern", 1, "full", true, false, true, "small", true, true, true);Read that call site again: (true, false, true, "small", true, true, true). Five booleans in a row. Swap any two and the order changes silently — exactly like saying "extra sugar, less ginger" by mistake. And imagine the menu adds a new option, say tulsi leaves. That is one more parameter, and every call site in the app must be edited, in the right position. The tests for this function are just as miserable: ten arguments to arrange for every single test case.
A row of boolean parameters is the smelliest version of this smell. Each boolean is a hidden switch, and the call site shows only true, false, true — meaning nothing to anyone. Flags also hint that one function is secretly several functions.
🛠️ Cleaning it up, step by step
Step 1: Bundle the recipe with Introduce Parameter Object. Ravi's first question to Montu: "What do these ten values describe, together?" The answer names the missing type — a chai preference:
interface ChaiPreference {
sugarSpoons: number;
milkLevel: "none" | "half" | "full";
spices: { ginger: boolean; cardamom: boolean };
strong: boolean;
cupSize: "small" | "regular";
veryHot: boolean;
serving: "dine-in" | "takeaway-glass" | "takeaway-paper";
}
function orderChai(customerName: string, pref: ChaiPreference): string {
const price = priceOf(pref);
return describe(customerName, pref) + " = Rs." + price;
}Two parameters. And look at a bonus improvement we got while bundling: takeaway: boolean plus paperCup: boolean allowed a nonsense combination (takeaway = false, paperCup = true — dine-in... in a paper cup?). The single serving field with three named values makes that impossible state unrepresentable. Bundling parameters often reveals such hidden rules.
The call site now explains itself:
orderChai("Montu", {
sugarSpoons: 1,
milkLevel: "full",
spices: { ginger: true, cardamom: false },
strong: true,
cupSize: "small",
veryHot: true,
serving: "takeaway-paper",
});Every value is labelled. Swapping is impossible. Adding tulsi later touches the type and the makers of chai — not every caller.
Step 2: "My usual!" with Preserve Whole Object. Regular customers already have a saved preference. So do not pull fields out of the customer just to pass them in — pass the customer's stored object:
class Customer {
constructor(
readonly name: string,
readonly usual: ChaiPreference,
) {}
}
function orderUsual(customer: Customer): string {
return orderChai(customer.name, customer.usual); // "Bhaiya, usual!"
}When Ravi switches to jaggery, his usual is updated in one place, and every future order follows — exactly like telling Shankar bhaiya once.
Step 3: Drop parameters the method can find itself, with Replace Parameter with Method Call. Suppose the original function also took todaysMilkPrice: number, passed by every caller. If the function can ask the price service directly, remove the parameter — fewer things for callers to fetch and forward:
// Before: every caller fetches the price and forwards it
function priceOf(pref: ChaiPreference, milkPrice: number): number { /* ... */ }
// After: the function asks for what it needs
function priceOf(pref: ChaiPreference): number {
const milkPrice = priceBoard.currentMilkPrice();
/* ... */
}The refactored design, drawn as the diagram Montu adds to the pull request:
And the flow, before and after:
One caution on Preserve Whole Object: pass the whole object only when the function reasonably belongs to that object's world. Handing a giant Customer to a function that needs just one number creates an unnecessary dependency. Like all medicine, dosage matters.
College corner: A parameter object is not just a bag — it is a candidate abstraction. The moment ChaiPreference exists, behaviour starts migrating to it: priceOf naturally becomes pref.price(), and validation moves into its constructor. Fowler notes this ripple effect: introducing the object is often the first step toward discovering a missing domain class. The bag becomes a citizen.
🔄 The life cycle of this smell
A signature's life follows the same arc as every bloater — and the longer you wait, the more call sites you must touch on the way back:
The cheapest arrow, as always, is the early one: bundle at parameter four, when there are five call sites — not at parameter ten, when there are fifty.
🧰 The same smell in C#
A school admission system, before and after. The smelly signature:
public void EnrollStudent(
string firstName, string lastName, int age,
string street, string city, string pinCode,
string guardianName, string guardianPhone,
bool needsBus, string busRoute)
{
// ...
}
// Call site - good luck reading this in a code review:
EnrollStudent("Asha", "Verma", 12, "14 MG Road", "Pune", "411001",
"R. Verma", "9876501234", true, "Route 7");After introducing parameter objects (note how the groups were already visible in the parameter names — they were Data Clumps all along):
public record StudentName(string First, string Last);
public record Address(string Street, string City, string PinCode);
public record Guardian(string Name, string Phone);
public record BusPlan(string Route); // absent = no bus needed
public void EnrollStudent(
StudentName name, int age, Address address,
Guardian guardian, BusPlan? bus)
{
// ...
}
EnrollStudent(
new StudentName("Asha", "Verma"), 12,
new Address("14 MG Road", "Pune", "411001"),
new Guardian("R. Verma", "9876501234"),
new BusPlan("Route 7"));Ten parameters became five, every value is labelled by its type, and the strange needsBus + busRoute pair (what did a route mean when needsBus was false?) collapsed into one nullable BusPlan. C# named arguments (age: 12) can also improve readability at call sites — a good band-aid, though the parameter object remains the real cure.
🔍 Where this smell hides in real projects
- Constructors of big classes. A class with twelve fields often grows a twelve-parameter constructor. This is so common that the Builder pattern exists largely to manage it — though a constructor that needs twelve values is often a Large Class confessing.
- Service and helper methods in business apps.
CreateInvoice(customerId, name, email, street, city, pin, amount, tax, discount, dueDate, notes)— enterprise codebases are full of these, and refactoring guides regularly use exactly this shape as their warning example. - Report and search functions.
searchProducts(keyword, minPrice, maxPrice, category, brand, inStock, sortBy, page, pageSize)— filter criteria are the classic case for a singleSearchCriteriaparameter object. - Legacy C APIs and their wrappers. Older procedural libraries pass everything explicitly; wrappers around them inherit the long lists unless someone deliberately bundles.
- UI component props. A component taking twenty separate props for one visual concept (colour, border, shadow, radius...) is the front-end version — design systems bundle them into theme/style objects.
- Test helper functions.
makeTestOrder(a, b, c, d, e, f, g)— ironically, helpers written to simplify tests often grow the longest lists of all.
Linters can watch this for you: ESLint's max-params rule, SonarQube's parameter-count rules, and most code-quality tools let teams set a threshold (commonly 4) and flag offending signatures automatically.
🤔 When it is okay to ignore
| Situation | Ignore the smell? | Why |
|---|---|---|
| 3-4 genuinely independent, unrelated parameters | ✅ Yes | The method is honestly stating its inputs; a fake "parameter bag" only adds indirection |
| The same cluster repeating across many methods | ❌ No | That is a Data Clump — bundle it once and everywhere benefits |
| Value-object constructors (e.g. a Date taking year, month, day) | ✅ Usually | Each parameter is a distinct component of one value; bundling them is circular |
| Same-typed neighbours like (from, to) or (x, y) | ❌ Risky | Transposition bugs are silent; consider a small typed pair like Route or Point |
| Languages with named/optional arguments, list of 4 readable params | ✅ Sometimes | Named arguments remove the reading problem; watch that the group does not keep recurring |
| Boolean flags controlling behaviour | ❌ No | Split into separately named methods instead of bundling the flags |
The honest summary: a few unrelated parameters are fine. The cure is most valuable when parameters cluster, share types, or are merely forwarded — those are signs of a missing object, not just an untidy signature.
💊 Which refactorings cure it
| Refactoring | When to use it |
|---|---|
| Introduce Parameter Object | A recurring group of parameters belongs together — bundle it into one named type |
| Preserve Whole Object | The caller already holds an object containing the values — pass the object, not its pieces |
| Replace Parameter with Method Call | The method can derive or look up a value itself — stop making callers fetch and forward it |
| Extract Method | The method needs many inputs because it does many jobs — split it, and each part needs fewer |
| Replace Data Value with Object | Loose primitives inflate the list — typed values shrink and clarify it |
🧠 The whole smell on one page
Montu's revision sketch, taped above his desk next to a photo of the tapri:
📦 Quick revision box
+------------------------------------------------------------------+
| LONG PARAMETER LIST - CHEAT SHEET |
+------------------------------------------------------------------+
| What : A method demanding many loose values in exact |
| order (the 10-instruction chai order) |
| Family : Bloaters |
| Spot it : 4+ params, same-typed neighbours, boolean flags, |
| repeated clusters, pass-through values |
| Costs : Swap bugs, unreadable calls, brittle signatures, |
| painful tests |
| Main fix : Introduce Parameter Object ("Bhaiya, usual!") |
| Helpers : Preserve Whole Object, Replace Parameter with |
| Method Call |
| Ignore : Few, truly independent inputs; value-object ctors |
| Mantra : "Name the bundle, then pass the bundle." |
+------------------------------------------------------------------+✍️ Practice exercise
Ravi's parting homework for Montu — now yours. This function books a school picnic bus. The signature is a monster — tame it.
function bookPicnicBus(
className: string,
section: string,
studentCount: number,
teacherName: string,
teacherPhone: string,
destination: string,
distanceKm: number,
departHour: number,
returnHour: number,
needVeg: boolean,
needJain: boolean,
ac: boolean,
): number {
let cost = distanceKm * 30;
if (ac) cost += 500;
if (returnHour - departHour > 8) cost += 300; // long day surcharge
const meals = studentCount * (needJain ? 60 : needVeg ? 50 : 55);
return cost + meals;
}
// A real call from the app:
bookPicnicBus("7", "B", 42, "Mrs. Iyer", "9812345678",
"Science City", 35, 8, 17, true, false, true);Your tasks:
- Find the hidden groups. (Hints: which parameters describe the class group? Which describe the trip? Which describe the meal plan? Which pair always travels together as a time window?)
- Apply Introduce Parameter Object: create
ClassGroup,Trip(with aTimeWindow), andMealPlantypes. Rewrite the function to take three or four parameters. - The pair
needVeg/needJainallows a nonsense combination (needVeg = false, needJain = true?). Replace the two booleans with one field:"standard" | "veg" | "jain". - Bonus:
departHourandreturnHourare both numbers — swap-prone! Does yourTimeWindowtype validate that return comes after departure? - Extra challenge: sketch the before/after as your own version of Figure 8. How many arrows did the bundle remove?
When your call site reads like a sentence and no two arguments can be silently swapped, you have earned your "usual" at the tapri — Shankar bhaiya keeps it ready. Next lesson: the deeper pattern behind these recurring groups — Data Clumps.
Frequently asked questions
- How many parameters are too many?
- Most guides draw the warning line at four or more. Robert C. Martin in Clean Code goes further: zero arguments is best, one or two are fine, three should make you think, and more than three needs very special justification. Treat these as smell detectors, not strict laws.
- Are boolean flag parameters really that bad?
- Usually, yes. A call like render(doc, true, false) tells the reader nothing, and a flag often means one method is secretly two. Prefer two well-named methods, like renderDraft and renderFinal, or at least a named options object.
- What is a parameter object?
- A small class or type that bundles parameters which belong together. Instead of passing street, city, and PIN separately to every function, you pass one Address. The call site becomes shorter, and new fields can be added without changing every signature.
- Isn't passing a whole object wasteful compared to passing two values?
- No — in nearly all languages you pass a reference, not a copy, so the cost is the same. The bigger question is dependency: only pass the whole object if the method genuinely belongs to that object's world.
- Do named arguments fix this smell?
- They help the readability problem — callers can see what each value means. But they do not fix the deeper issue: the same group of values still travels loose through many signatures. A parameter object fixes both the reading and the design.
Further reading
Related Lessons
Data Clumps: The Friends Who Always Travel Together
Data Clumps code smell for beginners — learn to spot groups of values that always travel together and bundle them into one class, like a student ID card.
Primitive Obsession: When Everything Is Just a String or a Number
Primitive Obsession explained simply — why plain strings and numbers hide bugs, and how value objects like Money and Address make code safe and clear.
Long Method: When One Function Tries to Do Everything
Learn the Long Method code smell with simple stories, TypeScript and C# examples, and step-by-step refactoring using Extract Method. Beginner friendly guide.
Large Class: The School Bag That Carries Everything
Understand the Large Class code smell — why god classes grow, how to spot low cohesion, and how Extract Class splits them into small, focused classes.