Parameter Object
Principle Link
Parameter Object is a pattern that supports Dependency Inversion. It packages related inputs into one contract. Then a use case receives one stable object instead of a long argument list. The same pattern supports Single Responsibility Principle. A method can focus on one behavior step when argument mapping lives in one place.
Core Idea
A long parameter list hides intent. Readers scan many primitive values and guess relationships. One call site can pass values in the wrong order. A small parameter class fixes that risk. It gives one named boundary for input data and collaborators.
A strong parameter object has these traits:
- clear domain name
- immutable fields
- validation near object creation
- no side effects
Conceptual Overview
The shared system context in this handbook is the car intelligence and marketplace platform. Examples in this page follow that context: Hypothetical Scenario.
The recommendation flow in this platform mixes user profile facts, budget limits, ownership cost rules, reliability weights, and market feed data.
A naïve use case receives many primitives such as min_budget, max_budget, city, annual_miles, family_size, risk_tolerance, and ten more values.
That style makes each call fragile.
One new field forces edits across many call sites.
Parameter Object gives a single contract. Callers build the object once. Use case code receives one value with strong names. Then dependency injection passes collaborators through the same object or through adjacent constructor contracts. This pairing creates a clean composition root. The composition root builds all objects and validates input once. Use case code runs domain policy with stable input shape.
The diagram shows a common split. The left side has a long argument list and scattered wiring. The right side has one request object and explicit injected ports.
Computing History
Martin Fowler documented "Introduce Parameter Object" in 1999. The refactoring grouped recurring parameter sets into named value objects. Teams used this move to lower call-site defects and to improve readability in large systems.
Sources: Fowler (1999)
Generic Example: Python
Non compliant shape with long arguments:
class MatchScoreService:
def calculate_score(
self,
profile_id: str,
min_budget: int,
max_budget: int,
annual_miles: int,
city: str,
parking_type: str,
reliability_weight: float,
maintenance_weight: float,
comfort_weight: float,
safety_weight: float,
) -> float:
...
Compliant shape with parameter object and injected ports:
from dataclasses import dataclass
from typing import Protocol
class MarketListingPort(Protocol):
def find_candidates(self, city: str, max_budget: int) -> list[dict]:
raise NotImplementedError
class OwnershipCostPort(Protocol):
def yearly_cost(self, listing_id: str, annual_miles: int) -> float:
raise NotImplementedError
@dataclass(frozen=True)
class MatchScoreRequest:
profile_id: str
min_budget: int
max_budget: int
annual_miles: int
city: str
parking_type: str
reliability_weight: float
maintenance_weight: float
comfort_weight: float
safety_weight: float
class MatchScoreService:
def __init__(self, listings: MarketListingPort, costs: OwnershipCostPort):
self.listings = listings
self.costs = costs
def calculate_score(self, request: MatchScoreRequest) -> list[dict]:
candidates = self.listings.find_candidates(
city=request.city,
max_budget=request.max_budget,
)
ranked = []
for car in candidates:
yearly_cost = self.costs.yearly_cost(car["id"], request.annual_miles)
score = (
car["reliability"] * request.reliability_weight
+ car["comfort"] * request.comfort_weight
+ car["safety"] * request.safety_weight
- yearly_cost * request.maintenance_weight
)
ranked.append({"id": car["id"], "score": score})
return sorted(ranked, key=lambda item: item["score"], reverse=True)
Generic Example: TypeScript
interface MarketListingPort {
findCandidates(city: string, maxBudget: number): Promise<Array<{ id: string; reliability: number; comfort: number; safety: number }>>;
}
interface OwnershipCostPort {
yearlyCost(listingId: string, annualMiles: number): Promise<number>;
}
type MatchScoreRequest = {
profileId: string;
minBudget: number;
maxBudget: number;
annualMiles: number;
city: string;
parkingType: "street" | "garage";
reliabilityWeight: number;
maintenanceWeight: number;
comfortWeight: number;
safetyWeight: number;
};
class MatchScoreService {
constructor(
private readonly listings: MarketListingPort,
private readonly costs: OwnershipCostPort,
) {}
async calculateScore(request: MatchScoreRequest): Promise<Array<{ id: string; score: number }>> {
const candidates = await this.listings.findCandidates(request.city, request.maxBudget);
const ranked: Array<{ id: string; score: number }> = [];
for (const car of candidates) {
const yearlyCost = await this.costs.yearlyCost(car.id, request.annualMiles);
const score =
car.reliability * request.reliabilityWeight +
car.comfort * request.comfortWeight +
car.safety * request.safetyWeight -
yearlyCost * request.maintenanceWeight;
ranked.push({ id: car.id, score });
}
return ranked.sort((a, b) => b.score - a.score);
}
}
Link with Dependency Injection
Parameter Object and Dependency Injection solve two connected concerns. Dependency injection controls object assembly. Parameter Object controls input shape for business behavior.
In the car platform flow, a composition root creates adapters, ports, and use case instances.
The same root can build a MatchScoreRequest from validated HTTP payload and profile context.
The use case then receives both pieces in explicit form:
- collaborators through constructor injection
- domain input through one request object
This split keeps method signatures stable across feature growth. It keeps validation logic out of deep business methods. It keeps dependency wiring out of policy code.
Trade-offs and Failure Modes
Parameter Object gives clarity, but design drift can appear. A team can move unrelated fields into one giant object. That move creates a "god request" that violates cohesion.
Common trade-offs:
- One object can hide unused fields in each use case.
- A broad object can carry stale data across layers.
- Versioning cost grows when many endpoints share one shape.
Control tactics:
- Keep each parameter object tied to one use case.
- Use immutable types and constructor validation.
- Split objects when two change patterns diverge.
- Keep infrastructure handles out of pure data objects.
Foundational Principle Links
This pattern links to Abstraction and Boundaries. The request object is a clear boundary contract.
This pattern links to Modularity and Composition. Composition roots assemble use cases from focused contracts.
This pattern links to Simplicity First. One named object reduces call-site cognitive load.
This pattern links to Correctness and Testing. Tests build one input object and verify one behavior claim.
Practice Checklist
- Name parameter objects after one use case intent
- Keep parameter objects immutable
- Validate invariants at construction time
- Avoid bundling unrelated concerns in one object
- Pair parameter objects with injected ports, not concrete clients
- Track object growth in design reviews
Written by: Pedro Guzmán
See References for complete APA-style bibliographic entries used on this page.