Skip to content

Parameter Object

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.

Parameter object with dependency injection

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);
  }
}

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.

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.