This article compares four approaches to handling dependencies in Object-Oriented Programming: constructor injection, parameter passing, ThreadLocal, and Kotlin’s new context parameters. Each technique is analyzed for testability, coupling, and readability. While constructor injection remains the standard, Kotlin’s evolving language features, particularly context parameters, introduce an elegant, implicit alternative for dependency management in modern OOP design.This article compares four approaches to handling dependencies in Object-Oriented Programming: constructor injection, parameter passing, ThreadLocal, and Kotlin’s new context parameters. Each technique is analyzed for testability, coupling, and readability. While constructor injection remains the standard, Kotlin’s evolving language features, particularly context parameters, introduce an elegant, implicit alternative for dependency management in modern OOP design.

Understanding Dependency Injection in Object-Oriented Programming

2025/10/16 20:48
4분 읽기
이 콘텐츠에 대한 의견이나 우려 사항이 있으시면 crypto.news@mexc.com으로 연락주시기 바랍니다

In Object-Oriented Programming, objects collaborate. The initial idea of collaboration, first found in Smalltalk, was for object A to send a message to object B. Languages designed later use method calling. In both cases, the same question stands: how does an object reference other objects to reach the desired results?

In this post, I tackle the problem of passing dependencies to an object. I will go through several options and analyze their respective pros and cons.

Constructor injection

For constructor injection, you pass dependencies as parameters to the constructor.

class Delivery(private val addressService: AddressService,                private val geoService: GeoService,                private val zoneId: ZoneId) {      fun computeDeliveryTime(user: User, warehouseLocation: Location): ZonedDateTime {         val address = addressService.getAddressOf(user)         val coordinates = geoService.getCoordinates(location)         // return date time     } } 

Constructor injection is by far the most widespread way to pass to an object its dependencies: for about ten years, every codebase I've seen has constructor injection.

I've a slight issue with constructor injection: it stores dependencies as fields, just like state. Looking at the constructor's signature, it's impossible to distinguish between the state and dependencies without proper typing.

It bugs me. Let's see other ways.

Parameter passing

Instead of storing the dependencies along with the state, we can pass the dependency when calling the method.

class Delivery(private val zoneId: ZoneId) {      fun computeDeliveryTime(addressService: AddressService,                             geoService: GeoService,                             user: User, warehouseLocation: Location): ZonedDateTime {         val address = addressService.getAddressOf(user)         val coordinates = geoService.getCoordinates(location)         // return date time     } } 

The separation of state and dependencies is now clear: the former is stored in fields, while the latter is passed as function parameters. However, the responsibility of handling the dependency is moved one level up the call chain. The longer the call chain, the more unwieldy it gets.

class Order() {      fun deliver(delivery: Delivery, user: User, warehouseLocation: Location): OrderDetails {         // Somehow get the address and the geo services         val deliveryTime = delivery.computeDeliveryTime(addressService, geoService, user, warehouseLocation)         // return order details     } } 

Note that the call chain length is also a problem with constructor injection. You need to design the code for the call site to be as close as possible to the dependency creation one.

ThreadLocal

Legacy design makes use of the ThreadLocal:

\

\ We can rewrite the above code using ThreadLocal:

class Delivery(private val zoneId: ZoneId) {      fun computeDeliveryTime(user: User, warehouseLocation: Location): ZonedDateTime {         val addressService = AddressService.get()         val geoService = GeoService.get()         // return date time     } } 

\ The ThreadLocal can be either set up in the call chain or lazily, on first access. Regardless, the biggest disadvantage of this approach is that it completely hides the dependency. There's no way to understand the coupling by only looking at the class constructor or the function signature; one needs to read the function's source code.

Additionally, the implementation could be a regular singleton pattern, with the same downsides.

Kotlin context

The last approach is Kotlin-specific and has just been promoted from experimental to beta in Kotlin 2.2.

Here's how we can migrate the above code to context parameters:

class Delivery(private val zoneId: ZoneId) {      context(addressService: AddressService, geoService: GeoService)     fun computeDeliveryTime(user: User, warehouseLocation: Location): ZonedDateTime {         // return date time     } } 

And here's how to call it:

context(addressService,geoService) {     delivery.computeDeliveryTime(user, location) } 

Note that the call can be nested at any level inside the context.

Summary

| Approach | Pros | Cons | |----|----|----| | Constructor injection | Testable | Mix state and dependencies | | Parameter passing | Testable | Noisy | | ThreadLocal | | Hides coupling | | Context parameter | Get dependencies on deeply-nested | Limited to Kotlin |

I guess I'll continue to use constructor injection, unless I'm coding in Kotlin. In this case, I'll be happy to use context parameters, even though they are in beta.


Originally published at A Java Geek on October 12th, 2025

면책 조항: 본 사이트에 재게시된 글들은 공개 플랫폼에서 가져온 것으로 정보 제공 목적으로만 제공됩니다. 이는 반드시 MEXC의 견해를 반영하는 것은 아닙니다. 모든 권리는 원저자에게 있습니다. 제3자의 권리를 침해하는 콘텐츠가 있다고 판단될 경우, crypto.news@mexc.com으로 연락하여 삭제 요청을 해주시기 바랍니다. MEXC는 콘텐츠의 정확성, 완전성 또는 시의적절성에 대해 어떠한 보증도 하지 않으며, 제공된 정보에 기반하여 취해진 어떠한 조치에 대해서도 책임을 지지 않습니다. 본 콘텐츠는 금융, 법률 또는 기타 전문적인 조언을 구성하지 않으며, MEXC의 추천이나 보증으로 간주되어서는 안 됩니다.

USD1 Genesis: 0 Fees + 12% APR

USD1 Genesis: 0 Fees + 12% APRUSD1 Genesis: 0 Fees + 12% APR

New users: stake for up to 600% APR. Limited time!