DEV Community

Cover image for Introduce Parameter Object: A Refactoring Pattern That Scales
CodeCraft Diary
CodeCraft Diary

Posted on • Originally published at codecraftdiary.com

Introduce Parameter Object: A Refactoring Pattern That Scales

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.

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
Enter fullscreen mode Exit fullscreen mode

At 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

Enter fullscreen mode Exit fullscreen mode

This is not "four parameters".
This is tax calculation context.

The same applies to:

string $currency,
?string $discountCode
Enter fullscreen mode Exit fullscreen mode

That 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
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

And optionally:

class PricingContext
{
    public function __construct(
        public string $currency,
        public ?string $discountCode
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Step 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
)

Enter fullscreen mode Exit fullscreen mode

After:

public function createInvoice(
    int $customerId,
    PricingContext $pricing,
    TaxContext $tax,
    DateTime $issueDate
): Invoice
Enter fullscreen mode Exit fullscreen mode

The 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()
);
Enter fullscreen mode Exit fullscreen mode

After:

$pricing = new PricingContext('EUR', null);
$tax = new TaxContext(1000, 0.21, false, 'DE');

$service->createInvoice(
    $customerId,
    $pricing,
    $tax,
    new DateTime()
);
Enter fullscreen mode Exit fullscreen mode

You 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
)
Enter fullscreen mode Exit fullscreen mode

No 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

This 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.

Top comments (0)