There is one refactoring pattern I apply regularly that rarely gets the attention it deserves.
It does not look impressive.
It also does not drastically change architecture.
Nor does it introduce new abstractions everywhere.
And yet, it prevents codebases from slowly collapsing under their own weight.
That pattern is Introduce Parameter Object.
Previous article from this category: https://codecraftdiary.com/2025/12/07/abstract-factory-pattern-php/
The Code Smell Nobody Fixes Early Enough
You have definitely seen this method signature:
public function createInvoice(
int $customerId,
string $currency,
float $netAmount,
float $taxRate,
string $country,
bool $isReverseCharge,
?string $discountCode,
DateTime $issueDate
): Invoice
PHPAt some point, this method worked fine.
Business requirements grew.
Soon after, regulations appeared.
Eventually, edge cases arrived.
And suddenly, every call to this method feels fragile.
Why Long Parameter Lists Are Dangerous
A long parameter list is not just an aesthetic problem.
From real-world experience, it causes:
- Hard-to-read method calls
- High cognitive load during code reviews
- Frequent parameter misordering bugs
- Changes that ripple through dozens of call sites
- Fear-driven refactoring (“don’t touch it” syndrome)
Most importantly, it hides implicit relationships between parameters.
Some of them clearly belong together — but the code does not express that.
However, the real problem is not the number of parameters.
The real problem is that the method signature fails to express the domain language clearly.
When multiple parameters:
- are always passed together,
- conceptually represent one domain idea,
- and change for the same business reasons,
the code is telling you something important:
there is a missing concept.
Introduce Parameter Object is not about reducing arguments —
it is about naming that concept explicitly.
Step 1: Identify the Concept Hiding in Plain Sight
Look again at the method:
float $netAmount,
float $taxRate,
bool $isReverseCharge,
string $country
PHPThis is not “four parameters”.
This is tax calculation context.
The same applies to:
string $currency,
?string $discountCode
PHPThat is pricing context.
Once you start seeing this, the refactor becomes obvious.
Step 2: Introduce a Parameter Object
Start small. Do not over-engineer.
class TaxContext
{
public function __construct(
public float $netAmount,
public float $taxRate,
public bool $isReverseCharge,
public string $country
) {}
}
PHPAnd optionally:
class PricingContext
{
public function __construct(
public string $currency,
public ?string $discountCode
) {}
}
PHPStep 3: Refactor the Method Signature
Before:
public function createInvoice(
int $customerId,
string $currency,
float $netAmount,
float $taxRate,
string $country,
bool $isReverseCharge,
?string $discountCode,
DateTime $issueDate
)
PHPAfter:
public function createInvoice(
int $customerId,
PricingContext $pricing,
TaxContext $tax,
DateTime $issueDate
): Invoice
PHPThe behavior did not change.
But the meaning of the code did.
What Changes Immediately (And Why It Matters)
1. Method calls become readable
Before:
$service->createInvoice(
$customerId,
'EUR',
1000,
0.21,
'DE',
false,
null,
new DateTime()
);
PHPAfter:
$pricing = new PricingContext('EUR', null);
$tax = new TaxContext(1000, 0.21, false, 'DE');
$service->createInvoice(
$customerId,
$pricing,
$tax,
new DateTime()
);
PHPYou no longer need to decode parameter positions.
The code explains itself.
2. Future changes become local
When a new tax-related requirement appears:
public function __construct(
public float $netAmount,
public float $taxRate,
public bool $isReverseCharge,
public string $country,
public ?string $vatId
)
PHPNo method signature explosion.
No mass refactoring across the codebase.
3. Validation and behavior move closer to data
Once parameters become objects, you can add logic:
class TaxContext
{
public function isTaxApplicable(): bool
{
return !$this->isReverseCharge && $this->taxRate > 0;
}
}
PHPThis is where the pattern starts paying compound interest.
Why This Pattern Scales So Well
In legacy systems, parameter lists tend to grow horizontally.
Introduce Parameter Object allows them to grow vertically instead.
That means:
- Fewer breaking changes
- Better encapsulation
- Lower refactoring cost over time
It is one of the safest refactorings you can apply in large systems.
Common Mistakes to Avoid
1. Creating “dumb bags of data”
If the object never gains behavior, you missed part of the value.
Parameter Objects should eventually own logic related to their data.
2. Refactoring everything at once
Apply this pattern incrementally.
Start with one method.
One concept.
One object.
This is not a big-bang refactor.
3. A Parameter Object that never gains behavior is often a sign of an incomplete refactoring.
The goal is not to move primitives into a class.
The goal is to move responsibility closer to the data it operates on.
If an object cannot reasonably answer domain questions about itself,
it may be a transport structure — not a domain concept.
When I Reach for This Pattern (From Practice)
In theory, people often mention “five or more parameters” as a warning sign.
In practice, that threshold is usually too late.
From real-world codebases, I start considering Introduce Parameter Object when:
- three parameters already form a meaningful domain concept,
- the same group of arguments appears across multiple call sites,
- or a single business change affects several parameters at once.
The exact number is less important than the signal:
the code is struggling to express intent through its interface.
Waiting for five or six parameters often means the opportunity to refactor cheaply has already passed.n it needed to be.
Final Thoughts
Introduce Parameter Object is not flashy.
But it is one of those patterns that quietly improves everything around it:
- Readability
- Maintainability
- Change resilience
- Developer confidence
In mature codebases, these are the refactorings that matter most.
If your methods keep growing and nobody wants to touch them —
this pattern is probably the missing piece.
In the next articles of this series, I will look at similar “quiet” refactoring patterns —
patterns that do not change architecture, but significantly improve how code communicates intent.
Patterns like:
- refactoring temporal coupling,
- extracting domain-specific query objects,
- or moving validation logic into domain concepts.
Small changes. Long-term impact.

