Parameterize from Above: Make Dependencies Explicit
A method can look self-contained while quietly deciding which clock, repository, client, or filesystem implementation the application must use. That hidden construction becomes painful when a test needs a controlled collaborator or production needs a different implementation.
Parameterize from Above is a small design move: instead of constructing a dependency inside the code that uses it, accept that dependency from a caller at a higher level. The caller becomes responsible for choosing and constructing the collaborator.
This article shows how to apply the move without turning every value into a parameter, where to place construction, and when direct construction is still the simpler choice.
The core idea: separate use from construction
Consider an invoice service that creates its repository internally:
class InvoiceService:
send(invoiceId):
repository = SqlInvoiceRepository()
invoice = repository.find(invoiceId)
...send does two different jobs. It performs invoice work, and it chooses a concrete storage collaborator.
That second decision creates a hidden dependency. A reader cannot understand the service’s requirements from its constructor or method signature. A test that wants a small in-memory repository also has no ordinary place to provide one.
Parameterizing from above changes the ownership of that choice:
class InvoiceService:
constructor(repository):
this.repository = repository
send(invoiceId):
invoice = this.repository.find(invoiceId)
...Then a higher-level part of the application performs the construction:
repository = SqlInvoiceRepository()
service = InvoiceService(repository)The service still depends on repository behavior. What changed is the location of the construction decision.
That distinction is the useful mental model:
lower-level code: uses a capability
higher-level code: chooses how that capability is providedThe technique does not remove dependencies. It makes them visible and moves concrete selection toward code that already knows about application assembly.
Start with one dependency that blocks a useful change
The move is most useful when internal construction causes a concrete problem. Testing provides a compact example.
Suppose an expiration checker reads the current time directly:
class SubscriptionService:
status(subscription):
clock = SystemClock()
now = clock.now()
if subscription.expiresAt <= now:
return "expired"
return "active"A test for the expiration boundary depends on the real clock. It may be difficult to express the exact instant the test needs.
Move the clock parameter upward:
class SubscriptionService:
constructor(clock):
this.clock = clock
status(subscription):
now = this.clock.now()
if subscription.expiresAt <= now:
return "expired"
return "active"Production can supply a system clock:
service = SubscriptionService(SystemClock())A test can supply a fixed clock:
clock = FixedClock("2030-04-15T10:00:00Z")
service = SubscriptionService(clock)
subscription.expiresAt = "2030-04-15T10:00:00Z"
assert service.status(subscription) == "expired"The example is intentionally simple. The valuable change is not the test double itself. The service no longer owns the policy decision that a system clock must be constructed at the point of use.
That makes the dependency explicit and gives each caller a controlled place to select an implementation.
Move construction only as high as necessary
“Above” does not mean “at the top of the entire program.” It means a caller with a better reason to own the construction decision.
Imagine this call chain:
application start
|
v
CheckoutController
|
v
CheckoutService
|
v
TaxCalculatorIf TaxCalculator needs a TaxRateProvider, several placements are possible.
Constructing the provider inside TaxCalculator hides the choice. Passing it through every method from application start may expose a detail to layers that do not need to know about it.
A practical option is to construct the provider at the nearest assembly boundary:
taxRateProvider = ConfiguredTaxRateProvider(config)
taxCalculator = TaxCalculator(taxRateProvider)
checkoutService = CheckoutService(taxCalculator)
controller = CheckoutController(checkoutService)Now each object receives only its direct collaborator. CheckoutController does not need a TaxRateProvider parameter because it does not use that capability directly.
This keeps two concerns separate:
- assembly code chooses concrete collaborators and connects them;
- behavior code uses the collaborators it was given.
Assembly code may live in an application entry point, module factory, framework configuration area, or another composition boundary. The exact location depends on the system. The principle is to keep construction decisions near other construction decisions rather than scattering them through business behavior.
Prefer constructor parameters for stable object dependencies
A dependency needed for most or all operations of an object usually belongs in its constructor.
class ReportService:
constructor(reportStore, clock):
this.reportStore = reportStore
this.clock = clockThis shape communicates that a usable ReportService requires both collaborators. The object cannot be created normally without supplying them.
A method parameter is a better fit when the collaborator or policy genuinely varies per operation:
reportService.export(reportId, formatter)Here, callers may select a different formatter for each export. Making formatter a permanent field could imply a lifetime that the design does not require.
The choice is about ownership and lifetime, not a rigid rule. Ask which statement is more accurate:
"This object needs this collaborator to do its job."or:
"This operation needs this input for this call."Use the signature that reflects the actual relationship.
Do not confuse explicit dependencies with interfaces everywhere
Parameterizing from above does not require creating an interface for every class.
If InvoiceService can accept a concrete InvoiceRepository type and there is no useful alternative abstraction, moving construction outward can still improve the design:
repository = InvoiceRepository(connection)
service = InvoiceService(repository)An interface becomes useful when callers need multiple implementations, when a boundary should expose a smaller contract, or when the language’s type system requires an abstraction for the intended substitution.
Creating an interface solely because a constructor now has a parameter can add ceremony without improving the dependency boundary.
The same restraint applies to factories. A factory is useful when object creation itself has meaningful complexity or must happen repeatedly at runtime. If an application creates one repository during startup, direct construction in assembly code is often easier to read.
Watch for parameter forwarding without ownership
A common overcorrection is to pass a low-level dependency through several unrelated layers:
controller.handle(request, clock)
service.process(order, clock)
policy.evaluate(order, clock)Only policy uses the clock, yet every intermediate method carries it.
This makes signatures noisy and couples intermediate layers to a detail they do not use. Instead, supply the clock when constructing the object that owns the time-based decision:
policy = RenewalPolicy(clock)
service = RenewalService(policy)
controller = RenewalController(service)Now the dependency follows the object graph rather than the call stack.
There are exceptions. Request-scoped values such as a cancellation signal, trace context, transaction handle, or caller identity may intentionally travel through several operations. Those are often per-operation context rather than stable object dependencies. Treat them according to their actual lifetime and semantics.
Avoid service locators disguised as convenience
Another tempting design is to replace direct construction with a global registry:
repository = Services.get("invoiceRepository")
clock = Services.get("clock")Concrete construction may have moved elsewhere, but the dependency remains hidden from the object’s public shape. A reader still cannot see what the object needs without inspecting its implementation, and tests must manipulate shared lookup state.
Passing collaborators explicitly keeps requirements visible:
service = InvoiceService(repository, clock)A dependency injection framework can automate object assembly in larger applications, but the framework is not the principle. The useful property is still explicit ownership: behavior code declares what it needs, while assembly code selects what satisfies those needs.
If framework annotations or global lookups obscure that relationship, the system can regain the same maintenance problem under different syntax.
Parameterize decisions, not every implementation detail
Not every object construction deserves to move upward.
Creating a small internal value with no external effects is often fine:
range = DateRange(start, end)
result = range.contains(date)Moving DateRange construction to a caller would expose an implementation detail without giving the caller a meaningful decision.
Internal construction deserves more scrutiny when the constructed object represents a capability or boundary, such as:
- time;
- persistence;
- network communication;
- file access;
- randomness;
- message delivery;
- environment or configuration access.
These collaborators often affect determinism, failure behavior, resource ownership, or deployment configuration. Making them explicit gives callers a useful choice.
Even then, keep the design proportional to the problem. A short script with one fixed output destination may gain nothing from injecting a filesystem abstraction. A long-lived application with several storage implementations may gain a great deal.
Resource ownership needs an explicit answer
Moving construction upward also moves questions about cleanup and lifetime.
Suppose a repository owns a database connection. If assembly code creates the repository, it should also be clear which part of the application closes the repository or the underlying connection.
A useful rule is to make ownership consistent:
creator owns lifetime
|
+-- create
+-- pass to users
+-- close at the matching boundaryThis is not universal. Some runtimes or frameworks manage resources on behalf of application code. In that case, follow the platform’s documented lifecycle.
The broader point is that parameterization should not make resource ownership ambiguous. A design that improves test substitution but leaves two components both assuming the other will close a resource has traded one problem for another.
Common mistakes when moving dependencies upward
The first mistake is moving everything at once. A large constructor with twenty collaborators is not automatically an improvement. It may reveal that one class owns too many responsibilities, but adding parameters alone does not fix that design.
The second is creating abstractions before a need appears. Explicit construction and substitution can often be achieved with existing types. Add interfaces or factories when they represent a real boundary or variation.
The third is letting assembly logic leak back into behavior code:
if environment == "test":
repository = FakeRepository()
else:
repository = SqlRepository()The behavior object is still choosing implementations. Put that choice in assembly code and give the object the selected collaborator.
The fourth is ignoring invalid combinations. If a component requires a repository and a clock, allowing either to be absent can defer configuration errors until much later. Prefer construction that produces a usable object or fails close to the assembly boundary.
When Parameterize from Above is a good fit
Use this move when code constructs a collaborator internally and that construction prevents a caller from making a meaningful engineering choice.
Strong signals include tests that need to control time or external effects, environment-specific implementations, duplicated setup logic, and behavior code that mixes domain decisions with infrastructure construction.
Keep direct construction when the created object is a local implementation detail, has no meaningful variation, and moving it outward would only enlarge the public API.
The goal is not maximum configurability. It is a clear boundary between code that decides what capability is needed and code that decides which concrete object supplies it.
A practical next step
When a class is difficult to test or configure, inspect the places where it uses new, constructors, global accessors, or framework lookups. Pick one dependency that represents a real capability.
Move that construction to the nearest caller that can reasonably own the choice. Pass the collaborator explicitly, keep intermediate layers unaware when possible, and make resource lifetime clear.
That small refactoring often exposes the dependency structure more accurately. If the resulting constructor becomes crowded, treat that as design information: the class may be coordinating more responsibilities than its name suggests.